Search

SECTION 05 원문 백업

재편집 전 원문 백업 | SECTION 05 | 2026-10-14
조인은 필요한 데이터를 정확히 결합하는 작업이다.
조인 컬럼 하나가 잘못되거나 1:N 관계를 잘못 이해하면 성능뿐 아니라 조회 결과 자체가 틀릴 수 있다.
이번 SECTION에서는 ms_mbr_big, tr_ord_big, ms_shop_big, tr_ord_det_big, tr_event_entry_big을 사용한다. 앞부분은 조인 정확성, 뒷부분은 NL JOIN과 Hash Join에서의 반복 접근·데이터 접근량을 점검하는 방식으로 진행한다.
⚠️
실행계획 및 예제 전제
•
본 페이지의 실행계획은 실제 SQL을 실행해 수집한 실측 결과가 아닌 예상·개념 계획이다. cost, 실행시간, actual rows, Buffers 등 실측값을 만들어 넣지 않았다.
•
제공된 PostgreSQL 교재에 실제 사례와 실습 SQL이 있는 부분은 원본 페이지·실습 ID를 표시했다. 별도로 만든 SQL은 원본 SQL 그대로라고 주장하지 않는다.
•
실제 인덱스 구성, 자료형, NOT NULL·FK 제약조건, 데이터 건수와 통계에 따라 계획이 달라질 수 있다.
•
BAD/GOOD의 목표는 원래 의도한 결과의 정확성을 확보한 뒤 불필요한 접근을 줄이는 것이다. SQL을 구조적으로 변경할 때는 결과 동등성을 반드시 확인한다.
•
교재에는 pg_hint_plan 힌트로 NL·Hash·조인 순서를 강제한 사례가 있지만, 이 페이지의 SQL에는 환경 의존적인 힌트를 기본 적용하지 않았다.

이번 섹션의 핵심 규칙

ID
구분
규칙
05-01
MUST
조인 조건의 테이블과 컬럼을 정확히 연결하자
05-02
MUST
1:N과 N:M 결합으로 결과가 증폭되는지 확인하자
05-03
MUST
복합키 조인은 업무상 필요한 키를 빠짐없이 사용하자
05-04
MUST
LEFT JOIN의 대상 테이블 필터는 ON과 WHERE를 구분하자
05-05
CHECK
불필요한 JOIN은 제거하되 데이터 무결성을 먼저 확인하자
05-06
TIP
존재 여부만 필요하면 JOIN + DISTINCT 대신 EXISTS를 고려하자
05-07
CHECK
NL JOIN에서 Driven 테이블의 반복 접근과 인덱스를 확인하자
05-08
TIP
Driven의 조인 컬럼뿐 아니라 필터 컬럼도 함께 점검하자
05-09
CHECK
JOIN의 Driving은 무조건 작은 테이블이라는 공식으로 정하지 말자
05-10
CHECK
Hash Join에서는 Build·Probe 입력의 필터 조건을 점검하자
구분: MUST — 기본 작성 원칙 / TIP — SQL 개선 노하우 / CHECK — 업무 결과·실행계획 점검 항목

먼저 기억할 공통 테이블 관계

-- 주문 1건 ↔ 회원 1명 (주문에서 회원으로) o.mbr_id = m.mbr_id -- 주문 1건 ↔ 매장 1곳 (주문에서 매장으로) o.shop_id = s.shop_id -- 주문 1건 ↔ 주문상세 여러 건 o.ord_no = d.ord_no -- 이벤트 응모 1건 식별용 복합키 e.event_id, e.entry_no
SQL
복사
위 관계는 SECTION 01의 BIG 계열 ERD에 기록된 논리적 관계이다. 실제 물리 FK 제약조건 생성 여부는 아직 검증되지 않았다.

REMINDER 05-01. 조인 조건의 테이블과 컬럼을 정확히 연결하자

MUST · 상황: 주문번호에 해당하는 회원 닉네임과 매장명을 조회한다.
BAD — 잘못된 작성 방식 또는 점검이 필요한 SQL
SELECT o.ord_no, m.nick_nm, s.shop_nm FROM tr_ord_big AS o JOIN ms_mbr_big AS m ON m.mbr_id = o.mbr_id JOIN ms_shop_big AS s ON s.shop_id = o.mbr_id -- 잘못된 컬럼 참조 WHERE o.ord_no = 1001;
SQL
복사
GOOD — 정확한 SQL 또는 비교할 개선 후보
SELECT o.ord_no, m.nick_nm, s.shop_nm FROM tr_ord_big AS o JOIN ms_mbr_big AS m ON m.mbr_id = o.mbr_id JOIN ms_shop_big AS s ON s.shop_id = o.shop_id WHERE o.ord_no = 1001;
SQL
복사
예상 실행계획 — 실측이 아닌 개념적 형태
BAD (가능한 실행계획, 컬럼 타입이 호환된다는 전제) Nested Loop -> Nested Loop -> Index Scan on tr_ord_big (ord_no = 1001) -> Index Scan on ms_mbr_big (mbr_id = o.mbr_id) -> Index Scan on ms_shop_big Index Cond: (shop_id = o.mbr_id) -- 잘못된 조건도 실행 가능 GOOD (가능한 실행계획) Nested Loop -> Nested Loop -> Index Scan on tr_ord_big (ord_no = 1001) -> Index Scan on ms_mbr_big (mbr_id = o.mbr_id) -> Index Scan on ms_shop_big Index Cond: (shop_id = o.shop_id)
Plain Text
복사
기억할 이유: 동일한 조인 알고리즘과 비슷한 실행계획이더라도 ON 절에서 다른 컬럼을 참조하면 결과가 틀릴 수 있다. SQL이 정상 실행된다는 사실은 조인이 정확하다는 근거가 아니다.
주의·예외: 원본 사례 0810_08은 세 테이블 조인에서 잘못된 별칭의 컬럼을 사용한 기록이며, 위 SQL은 이를 BIG 모델로 재구성한 교육용 예제다. ord_no 타입은 실제 DB에 맞게 바꿔야 한다. 회원 ID와 매장 ID가 서로 다른 타입이라면 BAD는 오류가 날 수도 있다.
자료 근거: 204건 사례 0810_08, StartUP Tuning PG PDF p.5 (BIG 계열 논리 관계).

REMINDER 05-02. 1:N과 N:M 결합으로 결과가 증폭되는지 확인하자

MUST · 상황: 회원 한 명의 주문 총금액과 이벤트 응모 건수를 동시에 조회한다.
BAD — 잘못된 작성 방식 또는 점검이 필요한 SQL
SELECT m.mbr_id ,SUM(o.ord_amt) AS ord_amt ,COUNT(e.entry_no) AS entry_cnt FROM ms_mbr_big AS m LEFT JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id LEFT JOIN tr_event_entry_big AS e ON e.mbr_id = m.mbr_id WHERE m.mbr_id = 'M00019' GROUP BY m.mbr_id;
SQL
복사
GOOD — 정확한 SQL 또는 비교할 개선 후보
SELECT m.mbr_id ,COALESCE(o.ord_amt, 0) AS ord_amt ,COALESCE(e.entry_cnt, 0) AS entry_cnt FROM ms_mbr_big AS m LEFT JOIN ( SELECT mbr_id, SUM(ord_amt) AS ord_amt FROM tr_ord_big WHERE mbr_id = 'M00019' GROUP BY mbr_id ) AS o ON o.mbr_id = m.mbr_id LEFT JOIN ( SELECT mbr_id, COUNT(*) AS entry_cnt FROM tr_event_entry_big WHERE mbr_id = 'M00019' GROUP BY mbr_id ) AS e ON e.mbr_id = m.mbr_id WHERE m.mbr_id = 'M00019';
SQL
복사
예상 실행계획 — 실측이 아닌 개념적 형태
BAD (가능한 형태) GroupAggregate / HashAggregate -> Hash Left Join -> Hash Left Join -> 회원 1행 -> 해당 회원 주문 N행 -> 해당 회원 응모 M행 → 주문 × 응모 조합으로 N×M 중간 결과 가능 GOOD (가능한 형태) Hash Left Join / Nested Loop Left Join -> 회원 1행 -> 주문을 먼저 회원별 Aggregate (최대 1행) -> 응모를 먼저 회원별 Aggregate (최대 1행) → 집계 결과가 서로 증폭되지 않음
Plain Text
복사
기억할 이유: 1:N 관계 두 개를 동시에 펼쳐서 조인하면 각각의 행이 서로 곱해질 수 있다. 서로 독립된 측정값을 합산해야 한다면 각 테이블에서 먼저 집계한 뒤 1:1 결과로 조인하는 편이 안전하다.
주의·예외: 이 사례는 결과 정확성 문제다. 실제 성능과 조인 방식은 행 수·인덱스에 따라 달라진다. 회원별 합계가 아니라 주문별 응모 정보가 필요한 업무라면 전혀 다른 관계가 필요하다. 선집계 최적화는 SECTION 06·10에서도 다룬다.
자료 근거: 204건 사례 0608_06, 0825_24, 0910_12의 M:M 조인 점검 기록을 바탕으로 재구성; BIG 모델 tr_ord_big / tr_event_entry_big.

REMINDER 05-03. 복합키 조인은 업무상 필요한 키를 빠짐없이 사용하자

MUST · 상황: 선택된 이벤트 응모 건의 실제 상태 코드를 동일 응모 건과 정확히 연결한다. event_id와 entry_no의 조합이 한 응모를 식별한다.
BAD — 잘못된 작성 방식 또는 점검이 필요한 SQL
WITH targets AS ( SELECT event_id, entry_no FROM tr_event_entry_big WHERE entry_dtm >= TIMESTAMP '2024-01-01' AND entry_dtm < TIMESTAMP '2024-02-01' ) SELECT e.event_id, e.entry_no, e.entry_rslt_cd FROM targets AS t JOIN tr_event_entry_big AS e ON e.event_id = t.event_id; -- entry_no 누락
SQL
복사
GOOD — 정확한 SQL 또는 비교할 개선 후보
WITH targets AS ( SELECT event_id, entry_no FROM tr_event_entry_big WHERE entry_dtm >= TIMESTAMP '2024-01-01' AND entry_dtm < TIMESTAMP '2024-02-01' ) SELECT e.event_id, e.entry_no, e.entry_rslt_cd FROM targets AS t JOIN tr_event_entry_big AS e ON e.event_id = t.event_id AND e.entry_no = t.entry_no;
SQL
복사
예상 실행계획 — 실측이 아닌 개념적 형태
BAD (가능한 계획) Hash Join / Nested Loop Join Cond: (e.event_id = t.event_id) → 하나의 event_id에 속한 여러 entry_no가 중복 결합될 수 있음 GOOD (가능한 계획) Nested Loop -> targets 추출 -> Index Scan on tr_event_entry_big Index Cond: (event_id = t.event_id AND entry_no = t.entry_no) 또는 Hash Join (복합키 모두 Hash Cond에 포함)
Plain Text
복사
기억할 이유: 복합키의 일부만 결합하면 같은 이벤트의 다른 응모가 섞인다. 조인 조건은 고유 행을 식별해야 하는지, 1:N 관계를 의도하는지에 따라 설계한다.
주의·예외: 모든 조인에 PK 전체를 넣으라는 뜻은 아니다. 예를 들어 주문(tr_ord_big)과 주문상세(tr_ord_det_big)는 주문번호 ord_no만으로 조인하는 1:N 관계가 정상이다. 위 CTE 자기 조인 구조는 복합키 누락의 위험을 설명하기 위한 교육용 예시다. 원본 ERD의 논리 PK 표기를 기준으로 하며 실제 제약조건은 확인 필요.
자료 근거: 204건 사례 0326_17~18, 0910_12; PG 원본 ERD p.5의 tr_event_entry_big(event_id,entry_no) PK 표기.

REMINDER 05-04. LEFT JOIN의 대상 테이블 필터는 ON과 WHERE를 구분하자

MUST · 상황: 모든 회원을 보여주되, 픽업 완료된 주문이 있으면 그 주문번호도 함께 표시한다.
BAD — 잘못된 작성 방식 또는 점검이 필요한 SQL
SELECT m.mbr_id, o.ord_no FROM ms_mbr_big AS m LEFT JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id WHERE o.ord_st = 'PKUP' AND m.mbr_gd = 'GOLD';
SQL
복사
GOOD — 정확한 SQL 또는 비교할 개선 후보
SELECT m.mbr_id, o.ord_no FROM ms_mbr_big AS m LEFT JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id AND o.ord_st = 'PKUP' WHERE m.mbr_gd = 'GOLD';
SQL
복사
예상 실행계획 — 실측이 아닌 개념적 형태
BAD (가능한 계획) Hash Join 또는 Nested Loop (Inner Join으로 단순화 가능) Join Cond: (o.mbr_id = m.mbr_id) Filter/Scan Filter: (o.ord_st = 'PKUP') → 매칭 주문이 없는 회원은 제거됨 GOOD (가능한 계획) Hash Left Join / Nested Loop Left Join Join Cond: (o.mbr_id = m.mbr_id) → 주문이 없는 GOLD 회원도 유지 (o 컬럼은 NULL)
Plain Text
복사
기억할 이유: LEFT JOIN은 왼쪽 회원을 보존하는 것이 목적이다. 오른쪽 주문의 컬럼을 WHERE에서 NULL-거부 조건으로 필터링하면 매칭이 없는 회원이 사라지고, 옵티마이저가 INNER JOIN처럼 처리할 수 있다.
주의·예외: '픽업 완료 주문이 존재하는 회원만 조회'가 실제 요구사항이라면 BAD 쿼리는 INNER JOIN으로 재작성하는 것이 더 분명하다. 모든 오른쪽 조건을 무조건 ON에 넣으라는 기계적 규칙이 아니라, 보존해야 할 행의 의미를 먼저 구분한다.
자료 근거: MySQL GUIDE 슬라이드 24; Oracle GUIDE 슬라이드 10 (Oracle (+) 구문은 PostgreSQL에 사용하지 않음).

REMINDER 05-05. 불필요한 JOIN은 제거하되 데이터 무결성을 먼저 확인하자

CHECK · 상황: 기간별 주문 건수만 필요한 COUNT SQL인데 회원 테이블까지 결합했다.
BAD — 잘못된 작성 방식 또는 점검이 필요한 SQL
SELECT COUNT(*) AS ord_cnt FROM tr_ord_big AS o JOIN ms_mbr_big AS m ON m.mbr_id = o.mbr_id WHERE o.ord_dtm >= TIMESTAMP '2023-03-01' AND o.ord_dtm < TIMESTAMP '2023-04-01';
SQL
복사
GOOD — 정확한 SQL 또는 비교할 개선 후보
-- o.mbr_id가 NOT NULL이고, 존재하는 회원만을 -- 참조하도록 FK가 강제된 경우에 한해 동등한 단순화 SELECT COUNT(*) AS ord_cnt FROM tr_ord_big AS o WHERE o.ord_dtm >= TIMESTAMP '2023-03-01' AND o.ord_dtm < TIMESTAMP '2023-04-01';
SQL
복사
예상 실행계획 — 실측이 아닌 개념적 형태
BAD (가능한 계획) Aggregate -> Hash Join Hash Cond: (o.mbr_id = m.mbr_id) -> tr_ord_big의 기간 조건으로 검색 -> ms_mbr_big 읽기/해시 생성 GOOD (가능한 계획) Aggregate -> Index Only Scan / Index Scan / Seq Scan on tr_ord_big Index Cond 또는 Filter: (ord_dtm >= ... AND ord_dtm < ...) → 명시적인 회원 접근과 조인 자체가 없어짐
Plain Text
복사
기억할 이유: 출력과 필터에 사용하지 않는 테이블에 접근하면 불필요한 읽기와 조인 처리가 생길 수 있다.
주의·예외: BIG 모델의 ERD에는 FK 관계가 표시되지만 실제 DB에서 FK·NOT NULL 제약이 강제되는지 확인되지 않았다. 일치 회원이 없는 주문이나 NULL 회원ID가 있다면 두 SQL의 COUNT 결과가 달라진다. 최적화보다 결과 동등성을 먼저 검증한다.
자료 근거: MySQL GUIDE 슬라이드 19; Oracle GUIDE 슬라이드 7. BIG 모델의 실제 무결성 제약조건 여부는 미확인.

REMINDER 05-06. 존재 여부만 필요하면 JOIN + DISTINCT 대신 EXISTS를 고려하자

TIP · 상황: 주문 이력이 한 건이라도 있는 GOLD 등급 회원의 ID를 중복 없이 조회한다.
BAD — 잘못된 작성 방식 또는 점검이 필요한 SQL
SELECT DISTINCT m.mbr_id FROM ms_mbr_big AS m JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id WHERE m.mbr_gd = 'GOLD';
SQL
복사
GOOD — 정확한 SQL 또는 비교할 개선 후보
SELECT m.mbr_id FROM ms_mbr_big AS m WHERE m.mbr_gd = 'GOLD' AND EXISTS ( SELECT 1 FROM tr_ord_big AS o WHERE o.mbr_id = m.mbr_id );
SQL
복사
예상 실행계획 — 실측이 아닌 개념적 형태
BAD (가능한 계획) HashAggregate / Unique -> Hash Join Hash Cond: (o.mbr_id = m.mbr_id) → 회원당 여러 주문이 나오고 중복 제거가 뒤따를 수 있음 GOOD (가능한 계획) Hash Semi Join / Nested Loop Semi Join Join Cond: (m.mbr_id = o.mbr_id) → 회원당 존재 여부만 판단, 중복 결과 없음
Plain Text
복사
기억할 이유: 필요한 정보가 '주문이 있는 회원인가'라면 모든 주문을 결합해 중복 제거할 필요가 없다. EXISTS는 Semi Join 등 존재 확인 중심의 방법으로 처리될 수 있다.
주의·예외: 결과 동등성은 ms_mbr_big.mbr_id가 회원 PK라는 전제에서 성립한다. 실제 옵티마이저가 BAD를 변환해 비슷한 계획을 만들 수도 있고, EXISTS가 항상 더 빠른 것은 아니다.
자료 근거: 기존 SQL GUIDE의 불필요한 JOIN/존재 확인 규칙에서 확장한 교육용 예제; PG 교재 p.253 존재 확인 관련.

REMINDER 05-07. NL JOIN에서 Driven 테이블의 반복 접근과 인덱스를 확인하자

CHECK · 상황: PLAT·INV 회원과 2023-12-21~25 주문을 결합하여 회원별 주문금액을 합산한다.
BAD — 잘못된 작성 방식 또는 점검이 필요한 SQL
-- 동일 SQL, 회원ID 조인 인덱스가 없거나 부적합한 상태 SELECT m.mbr_id, SUM(o.ord_amt) AS ord_amt FROM ms_mbr_big AS m JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id WHERE m.mbr_gd = 'PLAT' AND m.join_tp = 'INV' AND o.ord_dtm >= TIMESTAMP '2023-12-21' AND o.ord_dtm < TIMESTAMP '2023-12-26' GROUP BY m.mbr_id;
SQL
복사
GOOD — 정확한 SQL 또는 비교할 개선 후보
-- 인덱스 후보: 교재 [SQL-CRIX-TR_ORD_BIG_X11] -- CREATE INDEX tr_ord_big_x11 ON tr_ord_big (mbr_id); -- 동일 SELECT: 인덱스가 있을 때 반복 접근 비용 비교 SELECT m.mbr_id, SUM(o.ord_amt) AS ord_amt FROM ms_mbr_big AS m JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id WHERE m.mbr_gd = 'PLAT' AND m.join_tp = 'INV' AND o.ord_dtm >= TIMESTAMP '2023-12-21' AND o.ord_dtm < TIMESTAMP '2023-12-26' GROUP BY m.mbr_id;
SQL
복사
예상 실행계획 — 실측이 아닌 개념적 형태
BAD (NL JOIN을 선택한 상황의 가능한 계획) GroupAggregate -> Nested Loop Join Filter: (o.mbr_id = m.mbr_id) -> 회원 집합 스캔 -> Materialize -> Seq Scan on tr_ord_big (날짜 Filter) → Materialize를 회원 행 수만큼 반복 조회할 수 있음 GOOD (NL JOIN + x11 선택 시) GroupAggregate -> Nested Loop -> 회원 집합 스캔 -> Index Scan using tr_ord_big_x11 on tr_ord_big Index Cond: (mbr_id = m.mbr_id) Filter: (ord_dtm >= ... AND ord_dtm < ...) → Driven 인덱스 검색이 외부 회원 수만큼 반복될 수 있음
Plain Text
복사
기억할 이유: Nested Loop는 외부 집합(Driving)의 각 행을 기준으로 안쪽 집합(Driven)을 검색한다. Driven에 조인 컬럼 인덱스가 없으면 동일 데이터를 반복 확인할 위험이 있다.
주의·예외: 위 BAD 계획은 NL JOIN을 전제로 한 특정 시나리오다. 일반 SQL에는 pg_hint_plan 힌트가 없다면 PostgreSQL이 Hash Join 등을 선택할 수 있다. 원본 교재는 실제로 pg_hint_plan의 Leading/NestLoop 힌트를 써서 비교했으나, 여기서는 확장 설치 여부가 확인되지 않아 힌트를 SQL에 넣지 않았다.
자료 근거: StartUP Tuning PG PDF p.194~197, [SQL-JOIN성능-NL1], [SQL-CRIX-TR_ORD_BIG_X11].

REMINDER 05-08. Driven의 조인 컬럼뿐 아니라 필터 컬럼도 함께 점검하자

TIP · 상황: 05-07과 동일한 SQL을 회원ID 단일 인덱스와 회원ID·주문일시 복합 인덱스로 비교한다.
BAD — 잘못된 작성 방식 또는 점검이 필요한 SQL
-- x11(mbr_id)만 사용하는 상황 SELECT m.mbr_id, SUM(o.ord_amt) AS ord_amt FROM ms_mbr_big AS m JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id WHERE m.mbr_gd = 'PLAT' AND m.join_tp = 'INV' AND o.ord_dtm >= TIMESTAMP '2023-12-21' AND o.ord_dtm < TIMESTAMP '2023-12-26' GROUP BY m.mbr_id;
SQL
복사
GOOD — 정확한 SQL 또는 비교할 개선 후보
-- 교재의 복합 인덱스 후보 x12 -- CREATE INDEX tr_ord_big_x12 ON tr_ord_big (mbr_id, ord_dtm); SELECT m.mbr_id, SUM(o.ord_amt) AS ord_amt FROM ms_mbr_big AS m JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id WHERE m.mbr_gd = 'PLAT' AND m.join_tp = 'INV' AND o.ord_dtm >= TIMESTAMP '2023-12-21' AND o.ord_dtm < TIMESTAMP '2023-12-26' GROUP BY m.mbr_id;
SQL
복사
예상 실행계획 — 실측이 아닌 개념적 형태
[x11 (mbr_id)로 NL JOIN하는 경우] Nested Loop -> 회원 집합 -> Index Scan using tr_ord_big_x11 Index Cond: (mbr_id = m.mbr_id) Filter: (ord_dtm >= ... AND ord_dtm < ...) → 회원의 다른 날짜 주문도 후보가 되어 힙 접근 가능 [x12 (mbr_id,ord_dtm)로 NL JOIN하는 경우] Nested Loop -> 회원 집합 -> Index Scan using tr_ord_big_x12 Index Cond: (mbr_id = m.mbr_id AND ord_dtm >= ... AND ord_dtm < ...) → 회원 + 주문일시를 함께 인덱스에서 제한
Plain Text
복사
기억할 이유: NL Join의 반복 검색에서 조인 조건만 사용하는 인덱스보다, 조인 조건과 필터 범위가 맞는 복합 인덱스가 테이블 접근을 크게 줄일 수 있다.
주의·예외: PG 교재에서 x11/x12를 비교한 논리를 따른 예상 계획이다. 실제 데이터 분포와 이미 존재하는 다른 인덱스를 고려해야 하며, 복합 인덱스 추가가 무조건 답은 아니다. 인덱스 설계 원리는 SECTION 04와 연결된다.
자료 근거: StartUP Tuning PG PDF p.198~201, [SQL-JOIN성능-NL1], [SQL-CRIX-TR_ORD_BIG_X12].

REMINDER 05-09. JOIN의 Driving은 무조건 작은 테이블이라는 공식으로 정하지 말자

CHECK · 상황: 대량 기간의 회원 주문금액을 합산한다. 회원과 주문의 데이터 분포에 따라 유리한 조인 방향·방법이 달라질 수 있다.
BAD — 잘못된 작성 방식 또는 점검이 필요한 SQL
-- 잘못된 판단: FROM 절에 회원을 먼저 썼으니 -- 회원이 반드시 Driving이고, 항상 유리할 것이라고 가정 SELECT m.mbr_id, SUM(o.ord_amt) AS ord_amt FROM ms_mbr_big AS m JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id WHERE m.mbr_gd = 'PLAT' AND o.ord_dtm >= TIMESTAMP '2023-01-01' AND o.ord_dtm < TIMESTAMP '2025-01-01' GROUP BY m.mbr_id;
SQL
복사
GOOD — 정확한 SQL 또는 비교할 개선 후보
-- 동일 SQL의 실행계획을 확인한 후 조인 방법과 -- 각 입력 집합의 실제 건수를 비교한다. EXPLAIN (COSTS OFF) SELECT m.mbr_id, SUM(o.ord_amt) AS ord_amt FROM ms_mbr_big AS m JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id WHERE m.mbr_gd = 'PLAT' AND o.ord_dtm >= TIMESTAMP '2023-01-01' AND o.ord_dtm < TIMESTAMP '2025-01-01' GROUP BY m.mbr_id;
SQL
복사
예상 실행계획 — 실측이 아닌 개념적 형태
[가능한 계획 A] GroupAggregate -> Nested Loop -> 회원 집합 (Driving) -> Index Scan on 주문 (Driven, 반복 검색) [가능한 계획 B] HashAggregate -> Hash Join Hash Cond: (o.mbr_id = m.mbr_id) -> 주문 집합 -> Hash -> 필터링된 회원 집합 [다른 조건에서] Nested Loop의 Driving/Driven이 반대로 선택될 수도 있음
Plain Text
복사
기억할 이유: NL Join에서는 바깥 집합 건수가 안쪽 반복 횟수에 영향을 준다. 그러나 바깥 집합이 작더라도 안쪽 검색마다 읽는 데이터가 너무 많으면 비효율적일 수 있다. SQL의 FROM 나열 순서는 실제 실행 순서를 고정하지 않는다.
주의·예외: 원본 교재 p.212는 pg_hint_plan으로 NL의 방향을 강제해 실험했으며, 실행 환경의 디스크 I/O 영향을 강조한다. 이 페이지에서는 강제 힌트를 배제하고 계획 확인 규칙만 정리한다. EXPLAIN은 추정 계획이므로 실제 검증에서는 ANALYZE/BUFFERS와 데이터 정확성을 함께 확인한다.
자료 근거: StartUP Tuning PG PDF p.205~212, 특히 p.212의 방향·I/O 비교; 204건 사례 0325_12.

REMINDER 05-10. Hash Join에서는 Build·Probe 입력의 필터 조건을 점검하자

CHECK · 상황: DRCT 매장의 2023년 1~3월 준비 완료된 픽업 주문을 월별 매장 개설월로 집계한다. (원본 교재의 조인 사례를 간소화)
BAD — 잘못된 작성 방식 또는 점검이 필요한 SQL
SELECT SUBSTRING(s.shop_start_ymd FROM 1 FOR 6) AS open_ym ,COUNT(*) AS ord_cnt FROM ms_shop_big AS s JOIN tr_ord_big AS o ON o.shop_id = s.shop_id WHERE s.shop_oper_tp = 'DRCT' AND o.ord_st = 'PKUP' AND o.prep_cmp_dtm >= TIMESTAMP '2023-01-01' AND o.prep_cmp_dtm < TIMESTAMP '2023-04-01' GROUP BY SUBSTRING(s.shop_start_ymd FROM 1 FOR 6);
SQL
복사
GOOD — 정확한 SQL 또는 비교할 개선 후보
-- 동일 SQL, 주문의 기간 조건을 지원하는 인덱스가 -- 적절한지 검토 (교재 x16: prep_cmp_dtm) -- CREATE INDEX tr_ord_big_x16 ON tr_ord_big (prep_cmp_dtm); EXPLAIN (COSTS OFF) SELECT SUBSTRING(s.shop_start_ymd FROM 1 FOR 6) AS open_ym ,COUNT(*) AS ord_cnt FROM ms_shop_big AS s JOIN tr_ord_big AS o ON o.shop_id = s.shop_id WHERE s.shop_oper_tp = 'DRCT' AND o.ord_st = 'PKUP' AND o.prep_cmp_dtm >= TIMESTAMP '2023-01-01' AND o.prep_cmp_dtm < TIMESTAMP '2023-04-01' GROUP BY SUBSTRING(s.shop_start_ymd FROM 1 FOR 6);
SQL
복사
예상 실행계획 — 실측이 아닌 개념적 형태
[가능한 Hash Join 계획] HashAggregate -> Hash Join Hash Cond: (o.shop_id = s.shop_id) -> Seq Scan / Index Scan on tr_ord_big Filter 또는 Index Cond: (ord_st / prep_cmp_dtm) -> Hash ← Build 입력 -> Seq Scan on ms_shop_big Filter: (shop_oper_tp = 'DRCT') [주의] Hash 노드 아래가 Build, 다른 입력은 Probe. Hash Join이라고 해서 조인 키 인덱스만으로 속도가 결정되지 않음.
Plain Text
복사
기억할 이유: Hash Join은 한 입력 집합을 해시 테이블로 만들고(Build), 반대쪽으로 조회(Probe)한다. 조인 전에 각 입력 집합을 적절히 필터링하는 것이 중요하며, 조인 키 인덱스를 사용하는 NL Join과 판단 기준이 다를 수 있다.
주의·예외: Hash의 Build 입력은 실제 계획에서 Hash 노드가 위치한 쪽으로 판단한다. 일반적으로 작을수록 유리하지만 통계와 메모리, I/O에 따라 선택이 달라진다. 인덱스 x16은 교재의 실험용이며 필요 데이터가 많으면 Seq Scan이 더 합리적일 수 있다. 원본 교재는 Hash Join을 강제·비교한 부분이 있어 기본 옵티마이저 선택과 구분한다.
자료 근거: StartUP Tuning PG PDF p.178~180, p.220~223; [SQL-JOIN성능-HASH2], [SQL-CRIX-TR_ORD_BIG_X16].

JOIN 유형과 내부 처리 방식은 다르다

JOIN 유형은 결과에 어떤 행을 남길지 정하는 SQL의 의미이다. INNER JOIN, LEFT JOIN, CROSS JOIN 등이 여기에 해당한다.
내부 JOIN 알고리즘은 PostgreSQL이 해당 결과를 만드는 방법이다.
실행 노드
핵심 동작
개발자 확인 사항
Nested Loop
Driving 행마다 Driven을 반복 검색
loops, 조인 조건 인덱스, 후보/반환 행
Hash Join
Build 해시 테이블 작성 후 Probe
양쪽 입력 크기, Hash 입력, 메모리·temp
Merge Join
정렬된 입력을 키 순서대로 병합
정렬 비용, 인덱스 순서, 입력 건수
Hash Join은 작은 데이터에만 적용되는 것이 아니고, Nested Loop는 작은 테이블에만 적합한 것도 아니다. PostgreSQL은 통계·인덱스·반환량을 근거로 계획을 선택한다. JOIN 구문과 알고리즘을 혼동하지 않는 것이 우선이다. (PG PDF p.173~179)

실행계획을 점검하는 기본 SQL

-- 실제 실행 없이 옵티마이저의 추정 계획 확인 EXPLAIN (COSTS OFF) SELECT m.mbr_id, SUM(o.ord_amt) AS ord_amt FROM ms_mbr_big AS m JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id WHERE m.mbr_gd = 'PLAT' AND o.ord_dtm >= TIMESTAMP '2023-12-21' AND o.ord_dtm < TIMESTAMP '2023-12-26' GROUP BY m.mbr_id; -- 실습 환경이 확보된 뒤 SELECT를 실제 실행·측정 EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT m.mbr_id, SUM(o.ord_amt) AS ord_amt FROM ms_mbr_big AS m JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id WHERE m.mbr_gd = 'PLAT' AND o.ord_dtm >= TIMESTAMP '2023-12-21' AND o.ord_dtm < TIMESTAMP '2023-12-26' GROUP BY m.mbr_id;
SQL
복사
실측할 때: 조인 전후의 행 수, Join Filter와 Rows Removed by Join Filter, Driven 노드의 loops, Index Cond/ Filter, Hash와 Batches, temp read/written을 확인한다. EXPLAIN ANALYZE는 쿼리를 실제 실행하므로 DML 등에서는 주의한다.

개발 전 체크리스트

모든 JOIN의 양쪽 컬럼과 테이블 별칭이 정확한가?
업무에서 기대하는 1:1·1:N·N:M 관계를 알고 있는가?
복합키의 일부만 연결해 다른 행이 섞이지 않는가?
LEFT JOIN의 대상 테이블 조건을 ON과 WHERE에 의도대로 나누었는가?
불필요한 JOIN 제거 전에 PK/FK·NULL·누락 참조를 확인했는가?
존재 확인만 필요한데 JOIN 후 DISTINCT를 사용하지는 않았는가?
Nested Loop에서 Driven 접근이 지나치게 반복되지 않는가?
Driven의 조인 컬럼뿐 아니라 범위/필터 조건에 필요한 인덱스가 있는가?
SQL FROM 순서만 보고 실행 시 조인 순서를 가정하지는 않았는가?
Hash Join이면 Build·Probe의 검색량과 데이터 분포를 확인했는가?
최적화한 JOIN의 결과 행 수와 집계 값이 원본과 같은가?

참고한 자료와 재구성 범위

•
StartUPTuning_PG_20250616.pdf: p.5(BIG ERD), p.173~180(JOIN 알고리즘), p.194~202(NL Driven 인덱스), p.205~212(Driving 선택), p.214~219(다중 JOIN), p.220~226(Hash Join과 주의)
•
StartUPTuning_PG_SQL_Script.sql: SQL-JOIN성능-NL1, SQL-JOIN성능-NL2, SQL-JOIN성능-NL5, SQL-JOIN성능-HASH2, SQL-CRIX-TR_ORD_BIG_X11, X12, X16
•
SQL GUIDE for MySQL.pptx: 슬라이드 19(불필요한 JOIN), 슬라이드 24(OUTER JOIN의 ON/WHERE)
•
002_SQL GUIDE for ORACLE.pptx: 슬라이드 7(불필요한 JOIN), 슬라이드 10(OUTER JOIN)
•
: 0326_17, 0326_18, 0608_06, 0810_08, 0825_24, 0910_12, 0325_12
•
SECTION 01. SQL REMINDER 소개: 회원·주문·매장·주문상세·이벤트응모 관계
구분해야 할 내용: 05-07~10의 분석 방향은 PG 교재의 조인 실습에 직접 기반하지만, 이 문서의 모든 예상 계획은 새로 기술한 교육용 예시다. 05-01~06의 BAD/GOOD SQL은 원본의 문제 유형과 가이드 규칙을 설명하기 위해 BIG 모델로 재구성했으며, 해당 SQL이 204건의 실제 SQL 원문이라고 단정하지 않는다. 05-03의 복합키 매핑도 교육용 자기 조인 예시다.
🖼️
그림 제작 메모 — 현재 실제 그림은 삽입하지 않음
•
그림 A: 1:N × 1:N 중복 증폭 — 회원 1명에 주문 3건·응모 2건을 조인하면 중간 결과가 6건으로 증가하는 예시. BAD와 선집계 GOOD을 나란히 배치
•
그림 B: LEFT JOIN ON과 WHERE — 주문 없는 회원이 WHERE 조건에서 제거되는 모습을 비교
•
그림 C: NL JOIN 반복 접근 — Driving 회원 목록 → Driven 주문 테이블 접근, x11(mbr_id)와 x12(mbr_id,ord_dtm)의 검색 범위 차이
•
그림 D: Hash Join 구조 — Build(Hash 노드)·Probe, 각 집합의 WHERE 필터와 해시 생성 흐름
SECTION 05 한 줄 요약: JOIN은 먼저 정확한 행을 결합하고, 그다음 반복 접근과 불필요한 데이터 검색을 줄이는 것이 핵심이다.