설치·접속: PostgreSQL 설치와 접속
부제: "상품마다 최근 주문 2건", "카테고리별 최신 N개"처럼 바깥 행 값을 참조하는 Top-N을 조인 한 번으로 뽑을 때
-- 상품마다, 그 상품의 최근 주문 2건씩 (per-row Top-N)
SELECT p.id, p.name, o.ord_id, o.qty
FROM products p
CROSS JOIN LATERAL (
SELECT id AS ord_id, qty
FROM orders
WHERE product_id = p.id -- ← 바깥 행 p를 참조
ORDER BY id DESC
LIMIT 2
) o
WHERE p.id <= 3
ORDER BY p.id, o.ord_id DESC;
id | name | ord_id | qty
----+-------+--------+-----
1 | item1 | 72 | 2
1 | item1 | 65 | 5
2 | item2 | 173 | 5
2 | item2 | 150 | 3
3 | item3 | 181 | 3
3 | item3 | 160 | 2
(6 rows)보통 서브쿼리는 바깥 쿼리의 컬럼을 FROM 절 안에서 참조할 수 없다. LATERAL을 붙이면 그 제약이 풀려, 서브쿼리가 바깥에서 이미 만들어진 행(p)을 볼 수 있고 planner는 바깥 각 행마다 서브쿼리를 한 번씩 실행한다 — 상관 서브쿼리를 조인처럼 쓰는 것이다. 결정적 이점은 서브쿼리 안에서 ORDER BY ... LIMIT n을 쓸 수 있다는 점. 스칼라 상관 서브쿼리는 값 하나만 돌려주지만, LATERAL은 행 여러 개(Top-N)를 돌려줘 조인 결과로 펼쳐진다. "그룹마다 상위 N개"를 윈도우 함수(row_number() <= n) 서브쿼리로 푸는 것보다 의도가 직접 드러난다.
CROSS JOIN LATERAL은 서브쿼리가 빈 결과면 그 바깥 행도 사라진다. 주문 없는 상품까지 남기려면 LEFT JOIN LATERAL ... ON true를 쓴다. 같은 패턴을 "카테고리 목록 × 카테고리별 최신 N개"로 확장하면 흔한 per-category Top-N이 된다.
-- 카테고리별 최신 2개: 카테고리 목록을 바깥에 두고, 각 카테고리마다 LATERAL Top-N
SELECT c.category, t.name, t.launched
FROM (SELECT DISTINCT category FROM prod) c
CROSS JOIN LATERAL (
SELECT name, launched
FROM prod p
WHERE p.category = c.category
ORDER BY launched DESC
LIMIT 2
) t
ORDER BY c.category, t.launched DESC;
category | name | launched
----------+------+------------
kbd | K4 | 2026-06-01
kbd | K3 | 2026-05-20
monitor | D2 | 2026-06-15
monitor | D1 | 2026-05-05
mouse | M3 | 2026-06-30
mouse | M2 | 2026-04-09
(6 rows)카테고리 3개 각각에 대해 서브쿼리가 최신 2개를 뽑아 6행으로 펼쳐졌다. 바깥의 "그룹 목록"과 안쪽의 "그룹별 Top-N"을 분리해 쓰는 이 구조가 LATERAL의 전형이다. 그룹 수가 적고 각 그룹의 상위 몇 개만 필요할 때, 인덱스(category, launched DESC)와 맞물리면 전체를 정렬하는 윈도우 방식보다 훨씬 적게 읽는다.
이렇게도 쓴다
주문 없는 상품도 남긴다. 매칭이 없으면 우측 컬럼은 NULL. (조합: LEFT JOIN)
SELECT p.id, p.name, o.ord_id
FROM products p
LEFT JOIN LATERAL (
SELECT id AS ord_id FROM orders WHERE product_id = p.id ORDER BY id DESC LIMIT 1
) o ON true;
앞에서 계산한 파생값을 뒤 컬럼에서 재사용한다(중복 식 제거). (조합: 파생 컬럼)
SELECT p.name, calc.subtotal, calc.subtotal*0.1 AS vat
FROM products p
CROSS JOIN LATERAL (SELECT p.price*10 AS subtotal) calc;
집합 반환 함수(generate_series, unnest, jsonb_to_recordset)를 행 값으로 펼친다. FROM의 함수 호출은 암묵적으로 LATERAL. (조합: SRF)
SELECT p.id, g AS month
FROM products p, generate_series(1,3) g
WHERE p.id <= 2;
LIMIT 1이면 "그룹별 대표 1행". DISTINCT ON의 대안으로 비교된다. (조합: Top-1)
SELECT p.id, o.ord_id
FROM products p
CROSS JOIN LATERAL (
SELECT id AS ord_id FROM orders WHERE product_id=p.id ORDER BY ordered_at DESC, id DESC LIMIT 1
) o;
바깥 행별 집계(카운트/합)를 상관 서브쿼리 대신 LATERAL로. (조합: 집계)
SELECT p.id, p.name, s.cnt, s.total_qty
FROM products p
CROSS JOIN LATERAL (
SELECT count(*) cnt, sum(qty) total_qty FROM orders WHERE product_id=p.id
) s
WHERE p.id <= 5;
LATERAL이 정말 행마다 실행되는지 EXPLAIN으로 확인한다(Nested Loop 아래 서브플랜). (조합: EXPLAIN)
EXPLAIN (COSTS OFF)
SELECT p.id, o.ord_id FROM products p
CROSS JOIN LATERAL (SELECT id ord_id FROM orders WHERE product_id=p.id LIMIT 2) o;'명령어 > DB' 카테고리의 다른 글
| [PostgreSQL] array_position 으로 상태값을 커스텀 순서로 정렬한다 (0) | 2026.07.22 |
|---|---|
| [PostgreSQL] 배열 검색 = ANY / && / @> 와 GIN 인덱스 (0) | 2026.07.22 |
| [PostgreSQL] 집계 안의 ORDER BY — string_agg/array_agg 결과 순서를 고정한다 (0) | 2026.07.22 |
| [PostgreSQL] 윈도우 last_value 프레임 함정 — "파티션 최종값"이 자기 값으로 나올 때 (0) | 2026.07.22 |
| [PostgreSQL] PREPARE 제네릭 플랜 함정 — 배포 직후엔 빠르다가 갑자기 느려질 때 (0) | 2026.07.22 |