tags:

views:

79

answers:

5

Hi,

I have a table linking customers to previous purchases:

RecID   CustID   ProdID
  1       20      105
  2       20      300
  3       31      105
  4       45      105
  5       45      300
  6       45      312

I'd like to get a list of CustIDs that bought Item 105, but did NOT but Item 300.

In this case, CustID 31.

I can't seem to do it with selects and joins. I am stumped!

I sure would appreciate some help from experienced SQL folks.

Thank you!

+5  A: 

There are three common approaches. Here is a LEFT JOIN approach:

SELECT T1.CustID
FROM yourtable T1
LEFT JOIN yourtable T2 ON T1.CustID = T2.CustID AND T2.ProdId = 300
WHERE T1.ProdId = 105
AND T2.ProdId IS NULL

Here is NOT IN:

SELECT CustID
FROM yourtable 
WHERE ProdId = 105
AND CustID NOT IN
(
    SELECT CustID
    FROM yourtable 
    WHERE ProdId = 300
)

Or you could use NOT EXISTS:

SELECT CustID
FROM yourtable T1
WHERE ProdId = 105
AND NOT EXISTS
(
    SELECT NULL
    FROM yourtable T2
    WHERE T2.ProdId = 300
    AND T2.CustID = T1.CustID
)

Which has better performance depends on which database and version you are using.


For SQL Server the best is to use either NOT IN or NOT EXISTS:

In SQL Server, NOT EXISTS and NOT IN predicates are the best way to search for missing values, as long as both columns in question are NOT NULL. They produce the safe efficient plans with some kind of an Anti Join.

LEFT JOIN / IS NULL is less efficient, since it makes no attempt to skip the already matched values in the right table, returning all results and filtering them out instead.

Source:

Mark Byers
@Mark - you might want to point out that the second version may be VERY slow on large data sets, compared to thge others
DVK
@DVK: That's not always true. It depends on the database. *LEFT JOIN / IS NULL and NOT IN are best used to implement an anti-join in MySQL if the columns on both sides are not nullable.* Source: http://explainextended.com/2010/05/27/left-join-is-null-vs-not-in-vs-not-exists-nullable-columns/
Mark Byers
@Mark - Yes, this is true in Sybase for sure and IIRC Oracle. No experience with MySQL, but the linky's explanation sounds plausible.
DVK
@DVK: *Oracle‘s optimizer is able to see that NOT EXISTS, NOT IN and LEFT JOIN / IS NULL are semantically equivalent as long as the list values are declared as NOT NULL.* Source: http://explainextended.com/2009/09/17/not-in-vs-not-exists-vs-left-join-is-null-oracle/
Mark Byers
@Mark - Thanks!
DVK
+3  A: 
SELECT CustID from T T1
WHERE  ProdID = 105
  AND  NOT EXISTS (SELECT 1 from T T2
                   WHERE  T2.ProdID = 300
                   AND    T2.CustID = T1.CustID)
DVK
+1 for the NOT EXISTS
p.campbell
+1 - beat me by a few second
LittleBobbyTables
I think I should start worrying when my SQL is complimented on by someone named BobbyTables :)
DVK
+1  A: 

A couple of different approaches.

select CustID from t where ProdID=105
except
select CustID from t where ProdID=300

or

select CustID 
from t 
where ProdID in (105,300)
group by CustID 
having max(ProdID)=105
Martin Smith
A: 
SELECT PPT.CustID FROM PreviousPurchasesTable PPT
WHERE PPT.ProdID = 105
AND PPT.CustID NOT IN (SELECT PPT2.CustID FROM PreviousPurchasesTable PPT2 WHERE PPT2.ProdID = 300)
Madison
+1  A: 

Thanks!

I am the original author of the question.

Mark Byers second example with NOT IN works great! (I did not try others after this one worked for me).

His first example with LEFT JOIN did not return any CustIDs...I think I copied it correctly and used the proper table names and column names. So I don't know why it did not work for me.

Thanks again to all who were so kind as to take the time to write out some SQL for me.

I had to create a new account to leave a comment (I could not login with the account where I created this yesterday, and the password recovery said it couldn't find me)

Glenn
I have fixed my first answer with the LEFT JOIN - there was a missing join condition. Sorry about that!
Mark Byers