SQL 튜터 세션 정리
SQL
LeetCode
조건부 집계, Window Function, Correlated Subquery, LEFT JOIN NULL 함정, INNER JOIN 필터링, COUNT DISTINCT 활용
SQL 튜터 세션 정리
날짜: 2026-04-20
오늘 푼 문제
| # | 문제 | 난이도 | 핵심 개념 |
|---|---|---|---|
| 1 | LeetCode 1193. Monthly Transactions I | Medium | DATE_FORMAT, 조건부 집계 SUM(CASE WHEN), CTE vs CASE 직접 사용 |
| 2 | LeetCode 1174. Immediate Food Delivery II | Medium | RANK() Window Function, Correlated Subquery (MIN), SUM vs COUNT 함정 |
| 3 | LeetCode 550. Game Play Analysis IV | Medium | LEFT JOIN NULL 활용, INTERVAL 날짜 연산, 분모 컬럼 선택 |
| 4 | LeetCode 1070. Product Sales Analysis III | Medium | CTE + INNER JOIN 필터링, “all entries 반환” vs 집계 구분 |
| 5 | LeetCode 1045. Customers Who Bought All Products | Medium | COUNT DISTINCT, Scalar Subquery, HAVING 필터링 |
풀이 코드
1. LeetCode 1193. Monthly Transactions I
월별/국가별로 전체 거래 수·전체 금액·승인된 거래 수·승인된 금액을 집계하는 문제.
WITH t1 AS (
SELECT id,
DATE_FORMAT(trans_date, '%Y-%m') AS `month`,
country,
amount,
CASE WHEN state = 'approved' THEN 1 ELSE 0 END AS is_approved
FROM Transactions
)
SELECT `month`,
country,
COUNT(id) AS trans_count,
SUM(is_approved) AS approved_count,
SUM(amount) AS trans_total_amount,
SUM(is_approved * amount) AS approved_total_amount
FROM t1
GROUP BY `month`, country;대안 풀이 (CTE 없이 CASE를 집계 함수에 직접 삽입)
SELECT DATE_FORMAT(trans_date, '%Y-%m') AS `month`,
country,
COUNT(*) AS trans_count,
SUM(CASE WHEN state = 'approved' THEN 1 ELSE 0 END) AS approved_count,
SUM(amount) AS trans_total_amount,
SUM(CASE WHEN state = 'approved' THEN amount ELSE 0 END) AS approved_total_amount
FROM Transactions
GROUP BY `month`, country;2. LeetCode 1174. Immediate Food Delivery II
고객별 첫 번째 주문 중 즉시 배달(order_date = customer_pref_delivery_date) 비율을 구하는 문제.
방법 A — RANK() Window Function 활용
WITH t1 AS (
SELECT RANK() OVER(PARTITION BY customer_id
ORDER BY order_date ASC) rnk,
CASE WHEN order_date = customer_pref_delivery_date THEN 'immediate' ELSE 'scheduled' END AS order_type
FROM Delivery
)
SELECT ROUND(SUM(CASE WHEN order_type = 'immediate' THEN 1 ELSE 0 END) / COUNT(order_type) * 100, 2) AS immediate_percentage
FROM t1
WHERE rnk = 1;방법 B — Correlated Subquery (MIN) 활용
SELECT ROUND(SUM(CASE WHEN order_date = customer_pref_delivery_date THEN 1 ELSE 0 END) / COUNT(order_date) * 100, 2) AS immediate_percentage
FROM Delivery
WHERE order_date = (SELECT MIN(order_date)
FROM Delivery d2
WHERE d2.customer_id = Delivery.customer_id);3. LeetCode 550. Game Play Analysis IV
최초 로그인 다음날에도 로그인한 플레이어 비율을 구하는 문제.
WITH t1 AS (
SELECT player_id, MIN(event_date) AS first_day
FROM Activity
GROUP BY player_id
)
SELECT ROUND(
SUM(CASE WHEN t1.player_id IS NULL THEN 0 ELSE 1 END) / COUNT(DISTINCT Activity.player_id),
2) AS fraction
FROM Activity
LEFT JOIN t1
ON Activity.player_id = t1.player_id
AND Activity.event_date = t1.first_day + INTERVAL 1 DAY;4. LeetCode 1070. Product Sales Analysis III
각 상품이 처음 판매된 연도의 모든 판매 기록을 반환하는 문제.
WITH t1 AS (
SELECT product_id, MIN(year) AS first_year
FROM Sales
GROUP BY product_id
)
SELECT s.product_id,
t1.first_year,
quantity,
price
FROM Sales s
INNER JOIN t1
ON s.product_id = t1.product_id
AND s.year = t1.first_year;5. LeetCode 1045. Customers Who Bought All Products
모든 상품을 구매한 고객 ID를 찾는 문제.
방법 A — CTE + frac = 1
WITH t1 AS (
SELECT customer_id,
COUNT(DISTINCT product_key) / (SELECT COUNT(product_key) FROM Product) AS frac
FROM Customer
GROUP BY customer_id
)
SELECT customer_id
FROM t1
WHERE frac = 1;방법 B — HAVING으로 간결하게
SELECT customer_id
FROM Customer
GROUP BY customer_id
HAVING COUNT(DISTINCT product_key) = (SELECT COUNT(*) FROM Product);핵심 개념 정리
DATE_FORMAT() — 날짜를 원하는 문자열로 변환
| 함수 | 2018-12-18에 적용한 결과 |
타입 |
|---|---|---|
DATE_FORMAT(d, '%Y-%m') |
'2018-12' |
문자열 |
LEFT(d, 7) |
'2018-12' |
문자열 |
EXTRACT(YEAR_MONTH FROM d) |
201812 |
숫자 |
%Y→ 4자리 연도,%m→ 2자리 월- 참고:
date_parse는 MySQL 함수가 아니라 Presto/Trino 함수 — 헷갈리지 말 것
조건부 집계(Conditional Aggregation) 패턴
-- 1) 조건에 맞는 행의 개수
SUM(CASE WHEN 조건 THEN 1 ELSE 0 END)
-- 2) 조건에 맞는 행의 값 합계
SUM(CASE WHEN 조건 THEN value ELSE 0 END)
-- == SUM(flag * value)| 원리 | 설명 |
|---|---|
SUM(flag) |
1을 전부 더하면 = 해당 조건 행의 개수. 0은 더해도 영향 없음 |
SUM(flag * value) |
1 * value = value, 0 * value = 0 → 조건 맞는 행만 합산됨 |
⚠️ WHERE로 조건을 걸면 안 되는 이유: 조건 불일치 행이 사라져 전체 집계(분모)가 망가짐. “전체”와 “조건부” 집계를 동시에 하려면 조건부 집계 패턴 필수.
COUNT vs SUM — 조건부 집계 시 흔한 함정
-- ❌ 잘못된 방식: ELSE 0이 있으면 COUNT가 0도 세어버림
COUNT(CASE WHEN order_type = 'immediate' THEN 1 ELSE 0 END)
-- ✅ 올바른 방식 1: ELSE 없이 COUNT (NULL은 무시됨)
COUNT(CASE WHEN order_type = 'immediate' THEN 1 END)
-- ✅ 올바른 방식 2: SUM 사용 (0은 더해도 영향 없음)
SUM(CASE WHEN order_type = 'immediate' THEN 1 ELSE 0 END)LEFT JOIN에서 분모 컬럼 선택 원칙
LEFT JOIN 후 오른쪽 테이블 컬럼은 매칭 실패 시 NULL이 됨.
| 컬럼 | COUNT 결과 |
|---|---|
COUNT(DISTINCT 왼쪽테이블.player_id) |
전체 플레이어 수 ✅ |
COUNT(DISTINCT 오른쪽테이블.player_id) |
NULL 무시 → 매칭된 행만 카운트 ❌ |
분모처럼 “전체”를 세야 할 때는 반드시 왼쪽 테이블(FROM 절 테이블)의 컬럼을 사용할 것.
MySQL 날짜 연산
-- ❌ 정수 연산으로 변환됨
event_date + 1 → 20160302 (숫자)
-- ✅ 올바른 날짜 연산
event_date + INTERVAL 1 DAY“return all entries” vs 집계 구분
| 문장 패턴 | 의미 |
|---|---|
"return all entries/records" |
행 그대로 반환 → 집계 ❌ |
"find the total / sum / count" |
집계 필요 → SUM/COUNT ✅ |
"may have multiple entries" |
데이터 구조 설명 → 집계 여부와 무관 |
COUNT DISTINCT — 중복 제거 후 집계
-- ❌ 중복 포함: 같은 상품을 3번 사도 3으로 카운트
COUNT(product_key)
-- ✅ 중복 제거: 고유 상품 종류만 카운트
COUNT(DISTINCT product_key)“This table may contain duplicate rows” 조건이 있을 때는 항상 DISTINCT 필요 여부를 체크할 것.
오늘의 메타 정리
- 잘했던 것: CTE + JOIN 구조를 매 문제마다 자연스럽게 활용. 풀이 방향을 스스로 설계하고 피드백으로 다듬는 패턴 정착.
- 틀렸던 포인트 모음: COUNT(ELSE 0) 함정 / DATE + 1 정수 변환 / WHERE절 분모 오염 / SUM으로 집계 vs 행 그대로 반환 혼동 / COUNT DISTINCT 누락
- 배운 관점: 문제 조건 문장(“may have multiple entries”, “return all entries”)을 SQL 의도로 정확히 번역하는 독해력이 핵심.