728x90
※ SQL: Order of Execution.
안녕하세요. 듀스트림입니다.
오늘은 초급 개발자분들이 보시면 많은 도움이 될 내용인 SQL 실행 순서에 대한 내용입니다.
1. 우리가 코드로 작성할 때 사용하는 문법적 순서

SELECT → FROM →JOIN→ON→WHERE → GROUP BY → HAVING → ORDER BY → LIMIT
2. 실행 순서 (논리적 평가 순서)

FROM → ON → JOIN → WHERE → GROUP BY → HAVING → SELECT → (WINDOW) → DISTINCT → ORDER BY → LIMIT
※ LATERAL은 FROM 평가 중에 실행됩니다.
3. 물리적 실행 순서(실행계획)
옵티마이저가 실제로 선택한 방식(NL/Hash/Sort 등).
→ 물리적 실행은 최적화를 위해 순서를 바꾸거나 병합할 수 있지만, 논리적 의미는 동일하게 보장됩니다.
4. SQL의 실행 순서 (논리적 평가 순서)
| 단계 | 절/개념 | 동작 | 주의 |
| 1 | FROM | 테이블(서브쿼리 포함)들을 가져오고 결합할 준비 | 이 시점에 LATERAL이 작동 (왼쪽→오른쪽) |
| 2 | ON | 조인 조건 평가 → 매칭될 행 결정 | LEFT JOIN에서 오른쪽 조건은 여기 넣어야 외부행 유지 |
| 3 | JOIN 결과 생성 | INNER/LEFT/RIGHT 등 조인 유형 반영 | 이 순간 하나의 "행 스트림"이 됨 |
| 4 | WHERE | 행 필터링(참/거짓) | LEFT JOIN 후 WHERE right_col IS NOT NULL = 사실상 INNER화 |
| 5 | GROUP BY | 그룹 키로 묶음 | 묶인 뒤엔 각 그룹당 1행 |
| 6 | HAVING | 그룹 단위 필터링 | WHERE는 행, HAVING은 그룹 대상 |
| 7 | SELECT | 표현식 계산, 별칭 생성 | 이때 윈도우 함수 계산 지점은 아래 참고 |
| 8 | WINDOW 함수 | … OVER (…) 계산 | FROM/WHERE/GROUP BY/HAVING 뒤, DISTINCT/ORDER BY 전 |
| 9 | DISTINCT | 중복 제거 | SELECT 결과에 적용 (윈도우 결과도 포함된 후 적용) |
| 10 | ORDER BY | 결과 정렬 | 윈도우의 ORDER BY와는 전혀 다른 개념 |
| 11 | LIMIT/OFFSET | 결과 행 수 제한 | 최종 잘라내기 단계 |
5. FROM 단계에서의 LATERAL
LATERAL은 FROM 절 내부에서만 의미가 있고, 왼쪽 테이블의 각 행을 오른쪽 서브쿼리가 참조할 수 있게 합니다.
평가 흐름은 다음과 같습니다.
- 왼쪽(outer) 테이블에서 한 행을 꺼냄
- 그 행의 값을 들고 오른쪽(LATERAL) 서브쿼리를 실행
- 결과를 조인 규칙대로 합침
- 다음 왼쪽 행으로 반복
즉, FROM 안에서의 평가 순서는 "왼쪽 → 오른쪽"이며, LATERAL이 있으면 오른쪽이 왼쪽에 의존합니다.
LATERAL 조합별 의미
- CROSS JOIN LATERAL (sub) = JOIN LATERAL (sub) ON TRUE
→ INNER 계열: 오른쪽 결과가 없으면 그 왼쪽 행은 빠짐 - LEFT JOIN LATERAL (sub) ON …
→ LEFT OUTER 계열: 오른쪽이 없어도 왼쪽 행은 유지(오른쪽은 NULL)
6. ON vs WHERE (특히 LEFT JOIN에서의 함정)
같은 조건이라도 어디에 쓰느냐에 따라 결과가 달라집니다.
-- A. LEFT JOIN의 "오른쪽 필터"를 ON에 넣은 경우 (왼쪽 유지)
FROM a
LEFT JOIN b ON b.id = a.id AND b.score > 90
-- B. WHERE에 넣은 경우 (사실상 INNER로 바뀌기 쉬움)
FROM a
LEFT JOIN b ON b.id = a.id
WHERE b.score > 90
- A: b.score > 90 매칭에 실패해도 a행은 살아있고, b 컬럼은 NULL처리
- B: WHERE에서 b.score > 90을 검사하는 순간, NULL은 탈락 → 사실상 INNER JOIN처럼 작동
규칙: "조인 매칭 논리"는 ON에서, "최종 결과 필터링"은 WHERE에서.
특히, LEFT JOIN에서는 오른쪽 제약 조건을 ON에 두는 습관이 안전합니다. (그래야 왼쪽 행이 유지됩니다.)
7. 윈도우 함수의 평가 시점 (OVER 절)
- 순서: FROM/ON/WHERE → GROUP BY → HAVING → (여기서) WINDOW(SELECT 계산 중) → DISTINCT → ORDER BY → LIMIT
- 윈도우의 ORDER BY는 윈도우 프레임 내에서의 순서이지, 결과 전체의 정렬이 아님 → 결과를 진짜로 정렬하려면 "바깥 ORDER BY"가 필요합니다.
SELECT emp_id,
SUM(sales) OVER (PARTITION BY dept ORDER BY day) AS running_sum
FROM t
ORDER BY dept, day; -- ← 이게 있어야 화면상 정렬
8. DISTINCT, ORDER BY, LIMIT의 상호작용
- DISTINCT는 SELECT 결과에 적용됩니다(윈도우 계산까지 끝난 뒤).
- ORDER BY는 DISTINCT 이후에 적용됩니다.
- LIMIT/OFFSET은 가장 마지막에 적용(정렬/중복제거 다 끝난 후 자름).
패턴상 "최신 1건" 같은 문제는 서브쿼리 내부에서 ORDER BY … LIMIT 1로 먼저 줄이고, 바깥에서 조인/집계하여 의도한 결과를 만듭니다. (바깥에서 ORDER BY … LIMIT만 쓰면 전역 상위 N건이 됩니다.)
9. 서브쿼리 vs 상관(연관) 서브쿼리 vs LATERAL
- 일반 서브쿼리: 바깥과 독립적으로 평가(바깥 컬럼 참조 불가)
- 상관 서브쿼리: WHERE/SELECT 등에서 바깥 컬럼을 참조, 보통 바깥 행마다 실행 (SubPlan)
- LATERAL: FROM 절에서 바깥 컬럼을 참조하며 테이블(다중 컬럼·다중 행)을 반환 가능
LATERAL = FROM에서 쓰는 "행별 연관 서브쿼리" (바깥 행을 하나씩 받아 내부를 실행 → 조인으로 붙임)
10. CTE(WITH)의 평가와 최적화 포인트
- 논리적으로는 WITH이 먼저 평가된 뒤 메인 쿼리에서 참조
- PostgreSQL 12+: 기본적으로 인라인 최적화 가능(실행 전 병합) → 반드시 "먼저 만들어 둔다"는 보장은 없음
- MATERIALIZED 키워드로 고정(materialize) 시킬 수 있고, 반대로 NOT MATERIALIZED로 인라인 힌트 가능
→ 성능/의도에 맞게 선택
+ TIP
- LATERAL
- FROM 내부에서 왼쪽 먼저, 오른쪽(LATERAL)은 왼쪽 행을 참조해 실행
- ON vs WHERE
- 조인 매칭/오른쪽 조건은 ON
- 최종 결과 필터는 WHERE
- LEFT JOIN 뒤 WHERE right_col IS NOT NULL = INNER화 주의
- 윈도우 함수
- FROM/WHERE/GROUP BY/HAVING 후, DISTINCT/ORDER BY/LIMIT 전
- DISTINCT/ORDER BY/LIMIT
- DISTINCT → ORDER BY → LIMIT 순으로 적용
- 서브쿼리
- 독립 서브쿼리: 바깥 참조 X
- 상관 서브쿼리: 바깥 참조 O (행별 실행)
- LATERAL: FROM에서 바깥 참조 O + 표 형태 반환
- CTE
- 논리상 WITH이 먼저지만, 최신 PG는 인라인 최적화로 실제 물리 실행은 달라질 수 있음
++ AND는 OR보다 높은 연산자 우선순위(precedence)를 가집니다.
-- 괄호가 없을 경우 이 구문은
WHERE a = 1 OR b = 2 AND c = 3
↓
-- 자동으로 이렇게 해석됨
WHERE a = 1 OR (b = 2 AND c = 3)
-- 논리적으로 b = 2 AND c = 3을 먼저 평가하고 그 결과를 a = 1 OR (...결과)와 비교
-- OR을 먼저 적용하려면 괄호를 반드시 사용
WHERE (a = 1 OR b = 2) AND c = 3
-- 이 경우에는 a = 2 OR b = 2를 먼저 평가하고 그 결과를 AND c = 3과 결합
-- 논리적으로 완전히 다릅니다.
+++ 논리 연산 우선순위
| 우선순위 | 연산자 | 의미 |
| 1 | NOT | 부정 |
| 2 | AND | 논리곱 |
| 3 | OR | 논리합 |
오늘은 여기까지~
728x90
'SQL' 카테고리의 다른 글
| SQL: 행 스트림 (0) | 2025.11.17 |
|---|---|
| SQL: 상관관계(상관 서브쿼리) (0) | 2025.10.27 |
| SQL: EXISTS / NOT EXISTS (0) | 2025.10.15 |
| SQL: ASC, DESC (0) | 2025.10.13 |
| SQL: ROLLUP (0) | 2025.09.29 |