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 를 비교하는 것이다.
COALESCE(SUM(금액), 0) — SUM 밖에 씌운다.COALESCE(…, '없음') 이 안전하다.ORDER BY 에는 원래 칸을 쓴다.ORDER BY (칸 IS NULL), 칸 으로 직접 정해라.MAX(plan) 로 뽑으면 틀린다. 그건 알파벳 순으로 가장 큰 글자다 ('pro' > 'basic'). '마지막' 은 날짜로 찾아야 한다.묻는 것 · 토스 쿼리 테스트에 나온 '데이터 타입 문제' 가 정확히 이 유형이다. 보여 줄 값과 정렬할 값을 분리할 수 있는지를 본다.
users LEFT JOIN payments + GROUP BY user_id 로 유저별 합계·첫 결제일·마지막 결제일을 만든다.COALESCE(SUM(p.amount), 0) — SUM 밖에 씌운다.paid_date = MAX(paid_date) 로 다시 조인해 가져오고 COALESCE(..., '없음') — 이 컬럼은 원래 문자열이라 안전하다.ORDER BY total_amount DESC, first_paid ASC, user_id ASC.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;COALESCE(first_paid, '없음') 을 쓰면 반환 타입이 공통 타입인 문자열로 승격되고, 그걸로 정렬하면 사전순이 된다. MySQL·SQLite 는 이 변환을 오류 없이 조용히 하므로 에러도 안 나고 결과만 틀린다. (PostgreSQL·DuckDB 는 오류를 낸다.)ORDER BY 에는 원본 컬럼을 쓴다.MySQL·SQLite → NULL 을 가장 작은 값으로 본다 (ASC 면 맨 앞, DESC 면 맨 뒤)PostgreSQL·DuckDB → 기본이 NULLS LAST (ASC 에서도 맨 뒤)ORDER BY (first_paid IS NULL), first_paid 처럼 IS NULL(0/1)을 첫 키로 써서 직접 정한다. MySQL 에는 NULLS LAST 문법이 없다.LEFT JOIN + GROUP BY 왼쪽테이블 이면 그룹마다 NULL 행이 한 개 남아 SUM(COALESCE(amount,0)) 도 0 이 나온다. 하지만 그룹 자체가 비는 경우(상관 서브쿼리, INNER JOIN 뒤)는 SUM 이 NULL 이고 안쪽 COALESCE 는 실행될 기회가 없다. 밖에 씌우는 쪽이 항상 안전하다.MAX(plan) 로 바로 뽑으면 틀린다. 그건 '알파벳 순으로 가장 큰 plan' 이다 ('pro' > 'basic'). '마지막 결제의 plan' 은 날짜로 찾아야 한다 — paid_date = MAX(paid_date) 로 재조인하거나 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY paid_date DESC) 로 1 번만 남긴다.LIMIT 은 정렬 뒤에 걸린다. ORDER BY 를 빼고 LIMIT 만 쓰면 아무 20행이 나온다. 20번째와 21번째가 완전 동점이면 어느 쪽이 들어올지 정해지지 않으므로, 문제가 준 정렬 키를 전부 써야 한다.“결제 없는 유저를 맨 뒤로”면 ORDER BY (first_paid IS NULL), first_paid. “마지막 결제 금액까지”면 재조인에서 MAX(lp.amount) 를 같이 가져온다. “N일 이상 결제 없는 유저”면 DATEDIFF(기준일, MAX(paid_date)) >= N 에 MAX(paid_date) IS NULL 케이스를 따로 처리한다.