views:

232

answers:

7

I have a table ALPHA with 2 fields GroupId,Member:

GroupId | Member;
A1----------A;
A1----------B; 
A1----------C;
A2----------A;
A2----------B;
A3----------A;
A3----------D;
A3----------E;

Objective: Given the input of - A,B,C - I have to query the table to find if a GroupId exists for this exact set of members. So, this is what I plan to do:

  1. Query the table for all GroupIds whose count is 3 (since my inpt is A,B,C ..I knw its 3)
  2. This will give me A1,A3. Now, query this set for exact matching Member values..which will give me A1.

I plan to write a Stored Procedure and would achieve the objective somehow. But, my question can this be achieved in a single query...a single self-join perhaps.

Clarification: The set of (A,B,C) is unique to A1. And if give an input of (A,B,C,D) the query should NOT return A1.

+1  A: 

select * from ALPHA where Member in( select Member from ALPHA group by Member having COUNT(*) = 3)

rdkleine
That doesn't limit the members to just A, B and C, does it?
Jonathan Leffler
True. Add a where clause: Where Member in ('A','B','V')
rdkleine
select * from ALPHA where Member in( select Member from ALPHA where Member in ('A','B','C') group by Member having COUNT(*) = 3)
rdkleine
If a group has "A,B,C,D" The WHERE clause in the sub query excludes the "D", gives a count of 3 and gives a match. It should actually exclude that group due to the existance of "D"
Dems
Yep, good one.
rdkleine
+4  A: 
SELECT GroupID
  FROM ALPHA
 WHERE Member IN ('A', 'B', 'C')
 GROUP BY GroupID
HAVING COUNT(*) = 3

This relies on you writing out the list of members in the IN clause and setting the number of (distinct) entries in the member list in the HAVING clause. If you can't generate the SQL thusly, then you have to work harder.


As noted in an early comment, this also relies on the interpretation that you want the groups where all three of A, B, C (and possibly some others) are members of the group. One way, not necessarily the best way, of getting 'where the group contains exactly three people, namely A, B, C', is to use:

SELECT GroupID
  FROM ALPHA A1
 WHERE Member IN ('A', 'B', 'C')
   AND 3 = (SELECT COUNT(*) FROM ALPHA A2 WHERE A2.GroupID = A1.GroupID)
 GROUP BY GroupID
HAVING COUNT(*) = 3

This explicitly checks that the total number of people in the group is 3 and that the members are A, B, and C (assuming that there is a unique constraint on Alpha(GroupID, Member) so that a member can't be listed twice as belonging to the same group).

Jonathan Leffler
I had type that exact same thing when the notification came in. :)
BoltBait
@BoltBait - well, that's two of us with the same idea, then :D. On re-reading the question, I'm not sure whether the total population of the group must be 3 and the members must only be A, B, C, or whether the total population must be at least three and must include each of A, B, C (along with possibly some others). Our query answers the second variant.
Jonathan Leffler
This doesn't work. It gives a match for the set "A,B,C,D" but the existance of "D" should exclude that group...
Dems
who voted for this? it won't even work: __Column 'ALPHA.GroupID' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.__
KM
Oh yeah, doesn't even have a GROUP BY! That's just a typo though... But it still doesn't function as the question requires...
Dems
@Dems, I didn't even notice the A,B,C,D, thing, the sample data does not contain that condition. I have solved that in my answer, and eliminate the hardcoded ('A', 'B', 'C') ans =3 (without dynamic sql)
KM
Guys, for the specific scenario I am discussing....if the set is (A,B,C,D) then count would be 4. The set determines the count...so I think this solution is good. Thoughts?
Chouette
it depends on your needs. Your question wanted an exact match, if that's not needed "HAVING COUNT(*) = 3" is all you need. If you do need an exact match you need to be slightly smarter.
Dems
@Dems: I DO need a exact match. My TABLE takes care that (A,B,C) is only of group A1. So...there is no way that "HAVING COUNT(*) = 3" is going to return anything else right? "If you do need an exact match you need to be slightly smarter" --> how does this solution negate EXACT match?
Chouette
If GroupID A4 has members "A,B,C,D" then you search for members "A,B,C" then this query will give A4 as a match. It is not exact match, the search members are a sub set of the group, but not an exact match. (There WHERE clause filters out the 'D' from GroupID A4, then the HAVING gives a COUNT of 3, making it look like a match)
Dems
@Dems: Spot on...your solution is the apt one. Thanks!
Chouette
The revision seems a good'un to me. +1
Dems
+1  A: 

Try this one:

SELECT GroupId
  FROM ALPHA
 GROUP BY GroupId
HAVING SUM(CASE WHEN Member='A' THEN 1.0
                WHEN Member='B' THEN 2.0
                WHEN Member='C' THEN 4.0
                ELSE 7.31415
          END) = 7.0
najmeddine
Again, if there is a "D" this gives a match. You just need to replace "ELSE 0" with "ELSE 8" though :)
Dems
I understood that each group has a maximum of 3 members. Fixed.
najmeddine
+2  A: 
SELECT DISTINCT aa.GroupId
FROM Alpha aa
JOIN Alpha ab ON (aa.GroupId = ab.GroupId)
JOIN Alpha ac ON (aa.GroupId = ac.GroupId)
LEFT OUTER JOIN Alpha ax ON (aa.GroupId = ax.GroupId AND ax.Member NOT IN ('A', 'B', 'C')
WHERE aa.Member = 'A' AND ab.Member = 'B' AND ac.Member = 'C'
 AND ax.GroupId IS NULL;

There's also solutions involving GROUP BY but I find the JOIN solution often has better performance. I usually work in MySQL, and I understand MS SQL Server is better at grouping queries. So try both solutions and see what works best for the brand of RDBMS you use.

Bill Karwin
If the group includes a member "D" the poster wants that group excluded. This query doesn't do that.
Dems
@Dems: OK, I edited the query to handle that case.
Bill Karwin
Quite hard to generalise to "searching for x differnet members", but it looks like it works to me. +1.
Dems
can you make this work from a passed in parameter @List containing an unknown number of values?
KM
only with dynamic sql, you need a join for each search item, a dynamic IN list, and a dynamic where clause. But the question didn't actually ask for a such a generalisation...
Dems
A: 

My suggestion is to parse that delimited string into a temp table, and then try something like this.

create table #temp(member varchar(10))

create table #groups
(
groupID varchar(2),
member char(1)
)

--#temp holds the members from your delimited string.
--#groups holds your relationships.



select distinct groupID
from #groups
where 
 (select count(*) from #groups i, #temp t
  where i.member = t.member and i.groupID = #groups.groupID) = 
(select count(*) from #temp)
Hythloth
If the group is A,B,C,D this would give a match. The question wants it to Not give a match. Thus this does not work.
Dems
+2  A: 

Answers given so far assume that the Member field is unique for any given GroupID. In work I have done this isn't the case. And also if the group has what you're looking for, plus some extra, you need to exlucde that group.

SELECT
   [Alpha].GroupID
FROM
   [Alpha]
GROUP BY
   [Alpha].GroupID
HAVING
       SUM(CASE WHEN [alpha].Member IN ('A','B','C') THEN 1 ELSE 0 END) = 3
   AND MIN(CASE WHEN [alpha].Member IN ('A','B','C') THEN 1 ELSE 0 END) = 1


You can also replace the IN clause with a join on to a table holding the members you are searching for...

SELECT
   [Alpha].GroupID
FROM
   [Alpha]
LEFT JOIN
   [Search]
       ON [Search].Member
GROUP BY
   [Alpha].GroupID
HAVING
       SUM(CASE WHEN [alpha].Member = [search].Member THEN 1 ELSE 0 END) = (SELECT COUNT(*) FROM [search])
   AND MIN(CASE WHEN [alpha].Member = [search].Member THEN 1 ELSE 0 END) = 1
Dems
This matches my question perfectly.What is the significance of MIN condition....I know it ensure atleast one match but what scenario does this address?
Chouette
The SUM makes sure you get the three matches. The MIN makes sure ther are no rows in the group that don't match (Thus excluding a group with members 'A,B,C,D').
Dems
If you want the group 'A,B,C,D' to be a valid match when searching for 'A,B,C', then the SUM/MIN business isn't needed. Just COUNT(*) will do.
Dems
Thank you Dems!
Chouette
+2  A: 

try this:

declare @YourTable table (GroupID char(2),Member char(1))

insert into @YourTable values ('A1','A')
insert into @YourTable values ('A1','B')
insert into @YourTable values ('A1','C')
insert into @YourTable values ('A2','A')
insert into @YourTable values ('A2','B')
insert into @YourTable values ('A3','A')
insert into @YourTable values ('A3','D')
insert into @YourTable values ('A3','E')
insert into @YourTable values ('A5','A')
insert into @YourTable values ('A5','B')
insert into @YourTable values ('A5','C')
insert into @YourTable values ('A5','D')
SELECT t1.GroupID
    FROM @YourTable t1
        LEFT OUTER JOIN @YourTable t2 ON t1.GroupID=t2.GroupID AND t2.Member NOT IN ('A', 'B', 'C') 
    WHERE t1.Member IN ('A', 'B', 'C') 
        AND t2.GroupID IS NULL
    GROUP BY t1.GroupID
    HAVING COUNT(*) = 3

OUTPUT:

GroupID
-------
A1

(1 row(s) affected)

Here is a complete solution:

Before you use my function, you need to set up a "helper" table, you only need to do this one time per database:

CREATE TABLE Numbers
(Number int  NOT NULL,
    CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Number ASC)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
DECLARE @x int
SET @x=0
WHILE @x<8000
BEGIN
    SET @x=@x+1
    INSERT INTO Numbers VALUES (@x)
END

use this function to split your string, which does not loop and is very fast:

CREATE FUNCTION [dbo].[FN_ListToTable]
(
     @SplitOn              char(1)              --REQUIRED, the character to split the @List string on
    ,@List                 varchar(8000)        --REQUIRED, the list to split apart
)
RETURNS
@ParsedList table
(
    ListValue varchar(500)
)
AS
BEGIN

/**
Takes the given @List string and splits it apart based on the given @SplitOn character.
A table is returned, one row per split item, with a column name "ListValue".
This function workes for fixed or variable lenght items.
Empty and null items will not be included in the results set.


Returns a table, one row per item in the list, with a column name "ListValue"

EXAMPLE:
----------
SELECT * FROM dbo.FN_ListToTable(',','1,12,123,1234,54321,6,A,*,|||,,,,B')

    returns:
        ListValue  
        -----------
        1
        12
        123
        1234
        54321
        6
        A
        *
        |||
        B

        (10 row(s) affected)

**/



----------------
--SINGLE QUERY-- --this will not return empty rows
----------------
INSERT INTO @ParsedList
        (ListValue)
    SELECT
        ListValue
        FROM (SELECT
                  LTRIM(RTRIM(SUBSTRING(List2, number+1, CHARINDEX(@SplitOn, List2, number+1)-number - 1))) AS ListValue
                  FROM (
                           SELECT @SplitOn + @List + @SplitOn AS List2
                       ) AS dt
                      INNER JOIN Numbers n ON n.Number < LEN(dt.List2)
                  WHERE SUBSTRING(List2, number, 1) = @SplitOn
             ) dt2
        WHERE ListValue IS NOT NULL AND ListValue!=''



RETURN

END --Function FN_ListToTable

you can now use that function like this to query for any list:

DECLARE @List varchar(100)
SET @List='A,B,C'
declare @YourTable table (GroupID char(2),Member char(1))

insert into @YourTable values ('A1','A')
insert into @YourTable values ('A1','B')
insert into @YourTable values ('A1','C')
insert into @YourTable values ('A2','A')
insert into @YourTable values ('A2','B')
insert into @YourTable values ('A3','A')
insert into @YourTable values ('A3','D')
insert into @YourTable values ('A3','E')
insert into @YourTable values ('A5','A')
insert into @YourTable values ('A5','B')
insert into @YourTable values ('A5','C')
insert into @YourTable values ('A5','D')

SELECT t1.GroupID
    FROM @YourTable t1
        LEFT OUTER JOIN @YourTable t2 ON t1.GroupID=t2.GroupID AND t2.Member NOT IN (SELECT ListValue FROM dbo.FN_ListToTable(',',@List))
    WHERE t1.Member IN (SELECT ListValue FROM dbo.FN_ListToTable(',',@List))
        AND t2.GroupID IS NULL
    GROUP BY t1.GroupID
    HAVING COUNT(*) = (SELECT COUNT(*) FROM dbo.FN_ListToTable(',',@List))

OUTPUT:

GroupID
-------
A1
KM
This also returns a match for the group A,B,C,D. But as phrased, the question says such a group should be excluded.
Dems
@Dems, thanks, the sample data did not contain that condition, I fixed it now.
KM
Bent my head to understand what it does, but looks like it works to me. +1
Dems
@Dems, cut/paste the code into your DB and give it a try. ask any questions you have
KM
I don't have access to a DB on my netbook :) I have to work it through in my head *grin*
Dems