백엔드 데이터 인프라 28편. SELECT 의 전체 구조 — 9개 절·실행 순서·서브쿼리·UNION·INTERSECT 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 28편이에요. 11편 SELECT 에서 "기본 흐름" 을 다뤘으니, 이번 28편은 그 위에 — SELECT 의 9개 절 + 실행 순서 + 집합 연산.
SELECT 9개 절 — 풀 구조
WITH (CTE) -- 1
SELECT 컬럼들 -- 2
FROM 테이블·서브쿼리·JOIN -- 3
WHERE 조건 -- 4
GROUP BY 컬럼 -- 5
HAVING 그룹 조건 -- 6
ORDER BY 컬럼 -- 7
LIMIT 개수 -- 8
OFFSET 시작 -- 9
작성 순서는 위와 같지만 — 실행 순서는 다름:
1. FROM·JOIN → 데이터 소스 결정
2. WHERE → 행 필터
3. GROUP BY → 그룹화
4. HAVING → 그룹 필터
5. SELECT → 컬럼 선택·표현식
6. DISTINCT
7. ORDER BY
8. LIMIT·OFFSET
WHERE 가 SELECT 보다 먼저 실행되니, SELECT 별칭은 WHERE 에서 못 쓴다.
SELECT amount * 1.1 AS new_price FROM products
WHERE new_price > 100; -- ❌ ERROR — new_price 없음
WHERE amount * 1.1 > 100; -- ✅
ORDER BY 는 SELECT 후라 별칭 OK:
SELECT amount * 1.1 AS new_price FROM products
ORDER BY new_price; -- ✅
WITH (CTE) — 가독성·재사용
CTE(공통 테이블 식, 임시 결과 집합) 패턴은 18편 에서 다뤘다.
WITH active_users AS (
SELECT id, name FROM users WHERE deleted_at IS NULL
),
recent_orders AS (
SELECT user_id, MAX(created_at) AS last_at
FROM orders WHERE status = 'PAID'
GROUP BY user_id
)
SELECT u.id, u.name, o.last_at
FROM active_users u
LEFT JOIN recent_orders o ON u.id = o.user_id;
복잡한 쿼리를 작은 조각으로 나누면 디버깅도 이해도 쉬워진다.
FROM 절 — 데이터 소스
| 형태 | 의미 |
|---|---|
FROM table |
한 테이블 |
FROM table AS alias |
별칭 |
FROM t1 JOIN t2 ON ... |
JOIN |
FROM (SELECT ...) AS sub |
서브쿼리 |
FROM func() |
함수 호출 결과 |
FROM ROWS FROM(f1(), f2()) |
여러 함수 |
FROM ONLY parent |
상속 자식 제외 |
FROM table TABLESAMPLE BERNOULLI(10) |
샘플링 |
-- 함수 결과 from
SELECT * FROM generate_series(1, 10) AS num;
-- 샘플링
SELECT * FROM users TABLESAMPLE BERNOULLI(1); -- 1% 무작위
WHERE 절의 깊이
조건 결합 우선순위
WHERE a = 1 AND b = 2 OR c = 3
-- = (a = 1 AND b = 2) OR c = 3
WHERE a = 1 AND (b = 2 OR c = 3)
-- 명시
AND 가 OR 보다 우선이라, 헷갈리면 괄호로 묶는 게 안전하다.
NULL safe 비교
WHERE col IS DISTINCT FROM 1
-- col != 1 OR col IS NULL
WHERE col IS NOT DISTINCT FROM 1
-- col = 1 (NULL safe)
PG 만의 표현으로, NULL 함정을 피할 때 쓴다.
GROUP BY 깊이
표준 GROUP BY
SELECT category, COUNT(*), SUM(amount)
FROM orders
GROUP BY category;
SELECT 의 모든 비집계 컬럼은 GROUP BY 에 들어가야 한다.
GROUP BY ROLLUP·CUBE·GROUPING SETS
-- 카테고리별 + 전체 합계
SELECT category, SUM(amount)
FROM orders
GROUP BY ROLLUP (category);
-- category 별 + brand 별 + 둘 다 + 전체
SELECT category, brand, SUM(amount)
FROM orders
GROUP BY CUBE (category, brand);
-- 특정 조합만
GROUP BY GROUPING SETS ((category), (brand), ());
여러 차원 집계를 한 SQL 로 끝낸다. 대시보드나 OLAP(온라인 분석 처리) 시나리오에서 자주 쓴다.
FILTER 집계
SELECT
user_id,
COUNT(*) AS total_orders,
COUNT(*) FILTER (WHERE status = 'PAID') AS paid_orders,
COUNT(*) FILTER (WHERE status = 'CANCELED') AS canceled_orders
FROM orders
GROUP BY user_id;
CASE WHEN 대신 쓰는 PG·SQL 표준이고, 더 깔끔하게 읽힌다.
HAVING — 그룹 필터
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 10;
WHERE 와 차이: - WHERE = 그룹 전 행 필터 - HAVING = 그룹 후 그룹 필터
ORDER BY 깊이
ORDER BY col ASC -- 오름차순 (기본)
ORDER BY col DESC -- 내림차순
ORDER BY col ASC NULLS LAST -- NULL 끝
ORDER BY col DESC NULLS FIRST -- NULL 앞
ORDER BY 1, 2 DESC -- 위치 (SELECT 컬럼 순서)
ORDER BY (CASE WHEN ... END) -- 표현식
컬렉션·언어별 정렬
ORDER BY name COLLATE "ko_KR.utf8" -- 한국어 룰
ORDER BY name COLLATE "en_US.utf8" -- 영어 룰
LIMIT·OFFSET 깊이
LIMIT 10 OFFSET 100 -- 11~110번째? 아니, 101~110번째
LIMIT 10 -- 처음 10
LIMIT ALL -- 제한 없음 (= 생략)
FETCH FIRST 10 ROWS ONLY -- SQL 표준
OFFSET 100 ROWS -- SQL 표준
OFFSET 깊음 = 성능 위험
LIMIT 10 OFFSET 1000000 -- 1백만 행 스킵 — 느림
11편 에서 다룬 "커서 페이지네이션" 권장.
-- 커서 패턴
WHERE id > 1000 ORDER BY id LIMIT 10
DISTINCT
SELECT DISTINCT col FROM ... -- 중복 제거
SELECT DISTINCT a, b FROM ... -- (a, b) 조합 unique
SELECT DISTINCT ON (col) * FROM ... ORDER BY col, created_at DESC -- 그룹별 첫
11편 의 DISTINCT ON = PG 강점.
집합 연산 — UNION·INTERSECT·EXCEPT
여러 SELECT 결과를 "집합으로" 결합.
UNION — 합집합
SELECT id FROM users WHERE city = 'Seoul'
UNION
SELECT id FROM users WHERE age >= 65;
-- 둘 중 어느 조건이라도 만족 (중복 제거)
UNION ALL
-- 중복 안 제거 (더 빠름)
INTERSECT — 교집합
SELECT user_id FROM premium_users
INTERSECT
SELECT user_id FROM active_users;
-- 둘 다 만족
EXCEPT — 차집합
SELECT user_id FROM all_users
EXCEPT
SELECT user_id FROM banned_users;
-- A 에 있고 B 에 없음
규칙
컬럼 수와 타입이 같아야 하고, ORDER BY 는 마지막에 한 번만 쓴다.
서브쿼리 형태 4가지
(1) 스칼라 서브쿼리
SELECT name,
(SELECT AVG(amount) FROM orders) AS avg_amount
FROM users;
(2) 행 서브쿼리
SELECT * FROM orders
WHERE (user_id, amount) = (SELECT user_id, MAX(amount) FROM orders WHERE ...);
(3) 테이블 서브쿼리 (FROM 안)
SELECT u.name, agg.cnt
FROM users u
JOIN (SELECT user_id, COUNT(*) AS cnt FROM orders GROUP BY user_id) agg
ON u.id = agg.user_id;
(4) EXISTS·IN 서브쿼리
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders WHERE user_id = u.id);
성능 면에서 EXISTS 가 IN 보다 나은 경우가 많다 (NULL 처리와 짧은 회로 덕분).
LATERAL — 외부 컬럼 참조
SELECT u.name, recent.*
FROM users u
LEFT JOIN LATERAL (
SELECT id, amount FROM orders
WHERE user_id = u.id
ORDER BY created_at DESC LIMIT 3
) recent ON true;
12편 JOIN 의 LATERAL — "각 그룹 TOP-N" 표준.
함정 5가지
(1) WHERE 에서 SELECT 별칭 참조
위에서 본 실행 순서 함정 그대로다.
(2) GROUP BY 누락
SELECT 의 비집계 컬럼이 GROUP BY 에 없으면 ERROR 가 난다 (MySQL 은 비표준으로 통과시킨다).
(3) UNION vs UNION ALL
UNION 은 중복을 제거하느라 느리니, 중복이 없다고 확신할 땐 UNION ALL 을 쓴다.
(4) OFFSET 깊음
페이지네이션이 깊어질 땐 커서 방식으로 전환한다.
(5) 서브쿼리 vs JOIN
결과는 비슷해도 데이터가 크면 JOIN 이 더 효율적이다. EXPLAIN 으로 확인하는 게 정석.
작성 = WITH·SELECT·FROM·WHERE·GROUP BY·HAVING·ORDER BY·LIMIT. 실행 = FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. WHERE 에 SELECT 별칭 X, ORDER BY 는 OK. 집합 연산 + LATERAL + CTE 가 PG 강점.
한 줄 정리 — SELECT 9절 작성 순서 ≠ 실행 순서. WITH·GROUP BY ROLLUP·FILTER·DISTINCT ON·LATERAL·UNION 가 PG 강력 도구. WHERE 별칭 X·OFFSET 깊음 주의. EXISTS 가 IN 보다 보통 효율.
시험 직전 한 번 더 — 쿼리 개요 입문자가 매번 헷갈리는 것
- SELECT 9절 = WITH·SELECT·FROM·WHERE·GROUP BY·HAVING·ORDER BY·LIMIT·OFFSET
- 작성 순서 ≠ 실행 순서
- 실행 순서 = FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
- WHERE 에 SELECT 별칭 X
- ORDER BY 에 별칭 OK (SELECT 후)
- WITH (CTE) = 가독성·재사용
- FROM = 테이블·서브쿼리·함수·샘플링
- TABLESAMPLE BERNOULLI = 무작위 샘플
- generate_series = 함수 from
- IS DISTINCT FROM = NULL safe 비교
- GROUP BY 비집계 컬럼 모두
- ROLLUP·CUBE·GROUPING SETS = 다차원 집계
- FILTER 집계 =
COUNT(*) FILTER (WHERE ...) - HAVING = 그룹 후 필터
- WHERE = 행 필터, HAVING = 그룹 필터
- ORDER BY NULLS FIRST·LAST
- COLLATE = 언어별 정렬
- LIMIT OFFSET = 페이지네이션 (깊으면 느림)
- 커서 페이지네이션 =
WHERE id > N - DISTINCT ON = 그룹별 첫 (PG 특별)
- UNION·INTERSECT·EXCEPT = 집합 연산
- UNION ALL = 중복 제거 X (빠름)
- EXISTS vs IN — EXISTS 보통 우수
- LATERAL = 외부 컬럼 참조 서브쿼리
- 서브쿼리 4종 = 스칼라·행·테이블·EXISTS
- 컬럼 수·타입 같아야 집합 연산
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 23편 — 제약 깊이 CHECK DEFERRABLE EXCLUDE
- 24편 — DML 개요 4가지 동사 + MVCC 통합
- 25편 — INSERT 깊이 Bulk·COPY·UPSERT 전략
- 26편 — UPDATE 깊이 HOT·fillfactor·FROM JOIN
- 27편 — DELETE 깊이 bloat·VACUUM·파티션 DROP
다음 글: