백엔드 데이터 인프라 28편 — 쿼리 개요 SELECT 전체 구조

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 28편. SELECT 의 전체 구조 — 9개 절·실행 순서·서브쿼리·UNION·INTERSECT 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 28편 — 쿼리 개요 SELECT 전체 구조

이 글은 백엔드 데이터 인프라 시리즈 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 으로 확인하는 게 정석.

🎯 SELECT 9절 + 실행 순서

작성 = 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
  • 컬럼 수·타입 같아야 집합 연산

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!