There are three clean ways. The simplest to read for a single table is conditional aggregation: count how many A rows and how many B rows each customer has, and keep customers with A above zero and B equal to zero.
Conditional aggregation
SELECT customer_id FROM purchases GROUP BY customer_id HAVING SUM(CASE WHEN product = 'A' THEN 1 ELSE 0 END) > 0 AND SUM(CASE WHEN product = 'B' THEN 1 ELSE 0 END) = 0;
One pass over the table. It scales well, and adding conditions like "A and C but not B" only needs more lines in HAVING.
EXISTS and NOT EXISTS
SELECT DISTINCT p.customer_id
FROM purchases p
WHERE p.product = 'A'
AND NOT EXISTS (
SELECT 1 FROM purchases b
WHERE b.customer_id = p.customer_id AND b.product = 'B'
);Reads exactly like the sentence in the question. Needs DISTINCT, because a customer who bought A three times appears three times.
Set operation
SELECT customer_id FROM purchases WHERE product = 'A' EXCEPT SELECT customer_id FROM purchases WHERE product = 'B';
EXCEPT (called MINUS in Oracle) returns distinct rows from the first query that are not in the second. Short, and it removes duplicates for you.
What to avoid
NOT IN (SELECT customer_id ... WHERE product = 'B') fails if any customer_id in the subquery is NULL: it returns no rows. This is the NOT IN trap. Mention it, since the interviewer is likely to listen for it.
Also be careful with WHERE product = 'A' AND product <> 'B' on one table. A single row cannot be both, so it returns nothing useful. The logic has to look across rows of the same customer.
Choosing
If you only need the ids, EXCEPT is shortest. If the table is large and the question might grow, use conditional aggregation. If readability matters most, use NOT EXISTS.