Here's another alternative. I haven't tested the performance as I don't have a setup to do that, and I don't know if it will be similar to Jeff's in query plan. It does avoid the COUNT(DISTINCT...) that I read a while back (can't find the linky) was sometimes bad (something about rewinds).

EDIT: Eh, nevermind... you can ignore this one... Did a small test and it's between 5 and 10x slower than other methods with about 2x the reads.Second Edit: BUT, it's easier to adjust if you know that the situation is a random number of "Purchased these" with a single "but didn't purchase this". Most of the other methods rely on replicating a block of SQL for each item in the list of "Purchased". I think both Jeff's (and mine) work without that restriction.

SELECT b.CustomerIDFROM ( SELECT a.CustomerID FROM ( SELECT DISTINCT CustomerID, Product FROM Purchase WHERE Product IN ('A','B')) AS a GROUP BY a.CustomerID HAVING COUNT(Product) = 2 ) AS b LEFT OUTER JOIN Purchase p ON b.CustomerID = p.CustomerID AND p.Product = 'C'WHERE p.Product IS NULL