유저별 결제 요약 (NULL 표시와 정렬 분리)

난이도 보통 · 권장 12분 · 출력 컬럼 user_id, total_amount, first_paid, plan_label
이 문제 직접 풀어보기 →

문제

유저별 결제 요약을 상위 20명만 출력하세요. user_id total_amount 결제액 합계. 결제 이력이 없으면 0 first_paid 첫 결제일. 결제 이력이 없으면 NULL 그대로 둡니다 plan_label 마지막 결제 plan. 결제 이력이 없으면 문자열 '없음' · total_amount 내림차순 → first_paid 오름차순 → user_id 오름차순으로 정렬합니다. · first_paid 오름차순에서 NULL 은 '가장 작은 값' 으로 봅니다 (앞에 옵니다). MySQL·SQLite 의 기본 동작입니다. · 정렬한 뒤 상위 20행만 남기세요.
정렬 total_amount DESC, first_paid ASC(NULL 먼저), user_id ASC / LIMIT 20

쉽게 설명하면

성적표에 '미응시' 라고 써 넣는 것은 된다. 그런데 그걸로 줄을 세우면 망한다.

COALESCE(날짜, '없음') 을 쓰면 날짜 칸이 글자 칸으로 바뀐다. 그걸로 줄을 세우면 사전순이 된다.

원래 (날짜)글자로 바꾸면
2026-01-05'2026-01-05'
2026-01-20'2026-01-20'
(빈칸)'없음' ← 글자라서 날짜들보다 뒤로 간다

숫자에서도 똑같다. 글자로 바뀌면 '9' 가 '20' 보다 크다. 사전순이니까 첫 글자 9 와 2 를 비교하는 것이다.

이렇게 푼다

  1. 유저 명단 왼쪽에 두고 결제를 붙여서 합계·첫 결제일·마지막 결제일을 만든다.
  2. 금액은 COALESCE(SUM(금액), 0) — SUM 밖에 씌운다.
  3. 첫 결제일은 빈칸 그대로 둔다. 글자로 바꾸지 않는다.
  4. 플랜 이름은 원래 글자라서 COALESCE(…, '없음') 이 안전하다.
  5. 줄 세우기는 원래 칸으로 한다.

이것만 피하면 된다

자세히

묻는 것 · 토스 쿼리 테스트에 나온 '데이터 타입 문제' 가 정확히 이 유형이다. 보여 줄 값과 정렬할 값을 분리할 수 있는지를 본다.

풀이 순서

  1. users LEFT JOIN payments + GROUP BY user_id 로 유저별 합계·첫 결제일·마지막 결제일을 만든다.
  2. 금액은 COALESCE(SUM(p.amount), 0) — SUM 밖에 씌운다.
  3. 첫 결제일은 NULL 을 그대로 둔다. 문자열로 바꾸면 정렬이 깨진다.
  4. 마지막 plan 은 paid_date = MAX(paid_date) 로 다시 조인해 가져오고 COALESCE(..., '없음') — 이 컬럼은 원래 문자열이라 안전하다.
  5. 정렬은 원본 컬럼으로 한다: ORDER BY total_amount DESC, first_paid ASC, user_id ASC.

정답 (MySQL)

WITH s AS (
  SELECT u.user_id,
         COALESCE(SUM(p.amount), 0) AS total_amount,
         MIN(p.paid_date)           AS first_paid,
         MAX(p.paid_date)           AS last_paid
  FROM users u
  LEFT JOIN payments p ON p.user_id = u.user_id
  GROUP BY u.user_id
)
SELECT s.user_id, s.total_amount, s.first_paid,
       COALESCE(MAX(lp.plan), '없음') AS plan_label
FROM s
LEFT JOIN payments lp
       ON lp.user_id = s.user_id AND lp.paid_date = s.last_paid
GROUP BY s.user_id, s.total_amount, s.first_paid
ORDER BY s.total_amount DESC, s.first_paid ASC, s.user_id ASC
LIMIT 20;

함정

이렇게 변형된다

“결제 없는 유저를 맨 뒤로”면 ORDER BY (first_paid IS NULL), first_paid. “마지막 결제 금액까지”면 재조인에서 MAX(lp.amount) 를 같이 가져온다. “N일 이상 결제 없는 유저”면 DATEDIFF(기준일, MAX(paid_date)) >= N 에 MAX(paid_date) IS NULL 케이스를 따로 처리한다.

← Q19. 한 번도 결제하지 않은 유저 수