WITH: 이름이 있는 파생 테이블
지난 레슨에서 본 중첩된 파생 테이블을 공통 테이블 표현식(CTE)으로 바꾸면 다음과 같습니다.
1
WITH category_stats AS (
SELECT cat.name AS category,
COUNT(*) AS products,
ROUND(AVG(p.price), 2) AS average
FROM products p
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
)
SELECT *
FROM category_stats
WHERE products >= 10
ORDER BY average DESC;
결과 — 26행 중 처음 5행
| category | products | average |
|---|---|---|
| Wearables | 11 | 1672.29 |
| Programming | 10 | 1585.67 |
| Tablets | 17 | 1529.45 |
| Audio | 11 | 1482.89 |
| Kitchen | 12 | 1443.21 |
브라우저의 새 탭에서 Planche가 열리고, 샘플 데이터베이스가 로드된 채로 이 쿼리가 들어갑니다. 이 페이지의 다른 예제도 같은 탭을 다시 씁니다. F9를 누르기 전까지는 아무것도 실행되지 않습니다.
결과는 같지만 읽는 순서가 자연스러워집니다. 먼저 category_stats를 정의하고 다음에 사용합니다. 이 이름은 현재 쿼리 안에서만 유효하며, 별도의 테이블이 만들어지지는 않습니다.
단계를 여러 개 이어 붙이기
CTE는 여러 개를 이어 쓸 수 있고, 뒤의 CTE는 앞선 결과를 참조할 수 있습니다. 깊은 중첩 대신 단계가 드러나는 쿼리가 됩니다.
2
WITH paid_orders AS (
SELECT id, customer_id, total
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
),
per_customer AS (
SELECT customer_id,
COUNT(*) AS orders,
ROUND(SUM(total), 2) AS revenue
FROM paid_orders
GROUP BY customer_id
)
SELECT c.first_name || ' ' || c.last_name AS customer,
c.country,
pc.orders,
pc.revenue
FROM per_customer pc
JOIN customers c ON c.id = pc.customer_id
ORDER BY pc.revenue DESC
LIMIT 20;
결과 — 20행 중 처음 5행
| customer | country | orders | revenue |
|---|---|---|---|
| Sofia Patel | BR | 10 | 70324.42 |
| Zara Novak | JP | 5 | 70149.53 |
| Elena Rossi | KR | 10 | 58867.23 |
| Ines Choi | CA | 6 | 58455.76 |
| Noah Costa | ES | 4 | 57721.52 |
이 쿼리는 주문을 고르고, 고객별로 요약하고, 고객 이름을 붙이는 세 단계로 나뉩니다. 중첩 서브쿼리와 달리 CTE는 처리 순서대로 읽을 수 있습니다.
윈도우 함수: 접지 않는 집계
GROUP BY는 여러 행을 한 행으로 요약합니다. 윈도우 함수는 여러 행을 기준으로 계산하지만 원래 행은 유지합니다.
3
SELECT p.name,
p.price,
RANK() OVER (ORDER BY p.price DESC) AS price_rank,
DENSE_RANK() OVER (ORDER BY p.price DESC) AS dense_price_rank,
ROW_NUMBER() OVER (ORDER BY p.price DESC) AS row_no
FROM products p
ORDER BY p.price DESC
LIMIT 20;
결과 — 20행 중 처음 5행
| name | price | price_rank | dense_price_rank | row_no |
|---|---|---|---|---|
| Glide GPS Watch Pro | 2484.4 | 1 | 1 | 1 |
| Terra 3D Puzzle | 2481.77 | 2 | 2 | 2 |
| Aria Castle Kit | 2479.41 | 3 | 3 | 3 |
| Aria LED Strip Pro | 2463.41 | 4 | 4 | 4 |
| Nova Brief Answers | 2445.49 | 5 | 5 | 5 |
OVER (ORDER BY p.price DESC)가 계산 대상과 순서를 지정합니다. 세 함수의 차이는 가격이 같은 행을 처리하는 방식에 있습니다.
ROW_NUMBER()— 1, 2, 3, 4. 항상 서로 다르고, 동점은 임의로 갈립니다.RANK()— 1, 2, 2, 4. 동점은 순위를 나눠 갖고 다음 값이 건너뜁니다.DENSE_RANK()— 1, 2, 2, 3. 동점은 순위를 나눠 갖되 건너뛰지 않습니다.
필요한 결과에 따라 함수를 고릅니다. 정확히 10개가 필요하면 ROW_NUMBER를, 동점자를 포함한 순위가 필요하면 순위 함수를 사용합니다.
그룹별 상위 N개
윈도우 함수가 없다면 같은 카테고리에서 자신보다 비싼 상품 수를 세는 상관 서브쿼리가 필요합니다. PARTITION BY는 카테고리마다 순번을 새로 매깁니다.
4
WITH ranked AS (
SELECT cat.name AS category,
p.name AS product,
p.price,
ROW_NUMBER() OVER (PARTITION BY cat.id ORDER BY p.price DESC) AS rank_in_category
FROM products p
JOIN categories cat ON cat.id = p.category_id
)
SELECT category, product, price
FROM ranked
WHERE rank_in_category <= 3
ORDER BY category, rank_in_category;
결과 — 81행 중 처음 5행
| category | product | price |
|---|---|---|
| Audio | Atlas Earbuds | 2334.28 |
| Audio | Kite Speaker | 2331.15 |
| Audio | Quill Speaker | 2177.84 |
| Bags | Pulse Crossbody Mini | 2377 |
| Bags | Zen Crossbody | 2295.35 |
누적합
윈도우에 ORDER BY를 지정하면 각 행은 현재 행까지의 앞선 행을 함께 계산합니다.
5
WITH monthly AS (
SELECT substr(ordered_at, 1, 7) AS month,
ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
AND ordered_at >= '2025-01-01'
GROUP BY month
)
SELECT month,
revenue,
ROUND(SUM(revenue) OVER (ORDER BY month), 2) AS running_total
FROM monthly
ORDER BY month;
결과 — 12행 중 처음 5행
| month | revenue | running_total |
|---|---|---|
| 2025-01 | 311707.73 | 311707.73 |
| 2025-02 | 350775.62 | 662483.35 |
| 2025-03 | 351184.96 | 1013668.31 |
| 2025-04 | 374465.94 | 1388134.25 |
| 2025-05 | 381541.09 | 1769675.34 |
CTE가 월마다 한 행을 만들고, SUM(revenue) OVER (ORDER BY month)가 그 목록을 따라 누적합니다. 누적 건수나 누적 비율도 같은 방식으로 구합니다. 어떤 행의 값이 그 앞 행들에 따라 정해지는 계산은 모두 이 형태입니다.
앞 행과 비교하기
6
WITH monthly AS (
SELECT substr(ordered_at, 1, 7) AS month,
ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
AND ordered_at >= '2025-01-01'
GROUP BY month
)
SELECT month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS previous_month,
ROUND(revenue - LAG(revenue) OVER (ORDER BY month), 2) AS change
FROM monthly
ORDER BY month;
결과 — 12행 중 처음 5행
| month | revenue | previous_month | change |
|---|---|---|---|
| 2025-01 | 311707.73 | NULL | NULL |
| 2025-02 | 350775.62 | 311707.73 | 39067.89 |
| 2025-03 | 351184.96 | 350775.62 | 409.34 |
| 2025-04 | 374465.94 | 351184.96 | 23280.98 |
| 2025-05 | 381541.09 | 374465.94 | 7075.15 |
LAG(revenue)는 이전 행의 값을, LEAD는 다음 행의 값을 가져옵니다. 이전에는 테이블을 자기 자신과 조인해야 했던 전월 대비 계산을 간단히 작성할 수 있습니다. 첫 행의 previous_month는 이전 행이 없으므로 NULL이며, 증감도 NULL이 됩니다.
지원 현황
윈도우 함수는 표준이고 만날 만한 곳에는 다 있습니다. PostgreSQL, SQL Server, Oracle은 오래전부터 있었고, SQLite는 3.25(2018년), MySQL은 8.0(2018년)부터입니다. MariaDB는 10.2부터입니다. 만날 가능성이 있는 버전 중 없는 건 MySQL 5.7 정도입니다.
여기까지로 할 수 있는 것
필요한 거의 모든 형태로 데이터를 읽을 수 있습니다. 남은 두 레슨에서는 데이터를 안전하게 바꾸는 법과, 쿼리가 느린 원인을 찾는 법을 다룹니다.