never_paid DESC, channel ASC명단에서 '숙제 안 낸 사람' 찾기. 쉬워 보이는데 함정이 하나 있고, 그것만 보는 문제다.
이게 함정이다.
SELECT 5 WHERE 5 NOT IN (1, 2, NULL) |
0행 |
왜 그런가 — 이 식은 이렇게 풀린다.
5≠1(참) 그리고 5≠2(참) 그리고
5≠NULL(모름)
마지막을 모르니까 전체가 '모름' 이 되고, 조건에서 '모름' 은 거짓 취급이라
전부 탈락한다. 한 줄도 안 나온다.
NOT EXISTS (SELECT 1 FROM payments p WHERE p.user_id = u.user_id)p.user_id IS NULL).SUM(CASE WHEN p.user_id IS NULL THEN 1 ELSE 0 END).NOT IN 에 빈칸이 하나라도 섞이면 결과가 전부 사라진다. 본 데이터에는 빈칸이 없어서 통과하고, 다른 데이터셋에서 터진다. 실제 시험이 데이터셋을 여러 개 쓰는 이유가 이것이다.NOT EXISTS 는 이 문제에 안전하다. 빈칸인 줄은 '같다' 비교가 성립하지 않아서 알아서 무시된다. '없는 것 찾기' 는 NOT EXISTS 를 기본으로 삼아라.IN 은 괜찮다. 하나만 맞으면 참이니까. 부정형만 문제다.COUNT(*) 로 세면 그 채널 전체 인원이 나온다. SUM(CASE WHEN … IS NULL THEN 1 ELSE 0 END) 이 맞다.묻는 것 · '존재하지 않는 것 찾기'. 쉬워 보이지만 NOT IN 을 쓰면 데이터에 NULL 이 하나 섞인 순간 결과가 0행이 된다. 이 함정 하나만 아는지 보는 문제다.
users LEFT JOIN 그 목록 → 붙지 않은 유저가 '결제 없음' 이다.SUM(CASE WHEN p.user_id IS NULL THEN 1 ELSE 0 END) 으로 센다. COUNT(*) 로 세면 전체 유저 수가 나온다.WHERE 로 걸러내지 말고 CASE 로 센다.-- 방법 1. LEFT JOIN ... IS NULL (0명인 채널도 행이 남는다)
SELECT u.channel,
SUM(CASE WHEN p.user_id IS NULL THEN 1 ELSE 0 END) AS never_paid
FROM users u
LEFT JOIN (SELECT DISTINCT user_id FROM payments WHERE user_id IS NOT NULL) p
ON p.user_id = u.user_id
GROUP BY u.channel
ORDER BY never_paid DESC, u.channel ASC;
-- 방법 2. NOT EXISTS — NULL 에 면역이라 IS NOT NULL 을 안 써도 된다
SELECT u.channel,
SUM(CASE WHEN NOT EXISTS (SELECT 1 FROM payments p
WHERE p.user_id = u.user_id)
THEN 1 ELSE 0 END) AS never_paid
FROM users u
GROUP BY u.channel
ORDER BY never_paid DESC, u.channel ASC;NOT IN + NULL = 결과 전멸. 이게 이 문제의 핵심이다.WHERE u.user_id NOT IN (SELECT user_id FROM payments) 는 payments 에 NULL 이 한 행이라도 있으면 0행을 돌려준다.5 NOT IN (1, 2, NULL) 은 5<>1 AND 5<>2 AND 5<>NULL 이고, 마지막 항이 NULL 이라 전체가 NULL → WHERE 에서 거짓 취급 → 전부 탈락.NOT EXISTS 는 NULL 에 면역이다. p.user_id = u.user_id 가 NULL 이면 매칭 실패로 처리되어 그 행은 조용히 무시된다. '존재하지 않는 것 찾기' 는 NOT EXISTS 를 기본값으로 삼으면 이 사고가 안 난다.IN 은 멀쩡하다. 하나만 맞으면 참이므로 NULL 이 섞여도 된다. 문제는 부정형(NOT IN)뿐이다.COUNT(*) 로 세면 안 된다. LEFT JOIN 후 COUNT(*) 는 그 채널의 전체 유저 수다. SUM(CASE WHEN ... IS NULL THEN 1 ELSE 0 END) 이 정확하다.DISTINCT 를 넣어라. 안 넣으면 한 유저의 결제가 여러 건일 때 LEFT JOIN 으로 행이 불어난다. 여기서는 IS NULL 만 세니 결과가 같지만, 금액을 같이 구하는 변형에서 바로 깨진다.“이탈 유저 찾기”, “한 번도 로그인 안 한 유저”, “재구매 없는 유저” 전부 같은 패턴. 반대로 “결제한 유저만”이면 EXISTS 또는 INNER JOIN + DISTINCT.