재편집 전 원문 백업 | SECTION 06 | 2026-10-14
서브쿼리는 반복 횟수와 접근 범위를 확인하자.
SECTION 06은 PostgreSQL BIG 계열 실습 테이블을 사용해 스칼라 서브쿼리와 인라인 뷰의 작성 규칙을 학습합니다.
실행계획 안내: 아래 실행계획은 실측 결과가 아닌 교육용 예상 형태입니다. PostgreSQL을 실행하지 않았고 비용·실행시간·실제 행 수·Buffers 수치를 임의로 만들지 않았습니다. 같은 SQL이라도 통계, 인덱스, 실행계획 변환 등에 따라 실제 계획은 달라집니다. BAD는 모든 상황에서 무조건 느리다는 뜻이 아닙니다.
이번 섹션의 규칙
ID | 구분 | 핵심 주제 |
06-01 | MUST | 같은 테이블의 반복 스칼라 서브쿼리 |
06-02 | MUST | 외부 행마다 반복하는 집계와 선집계 |
06-03 | CHECK | 스칼라 서브쿼리가 유리한 예외 |
06-04 | TIP | 기간별 반복 집계와 조건부 집계 |
06-05 | MUST | WHERE 상관 서브쿼리의 MAX 제거 |
06-06 | MUST | 인라인 뷰의 외부 조건 공급 |
06-07 | CHECK | 선별된 회원부터 조인·집계 |
06-08 | CHECK | 인라인 뷰 병합과 구체화 구분 |
06-09 | TIP | 필요한 행 선별 후 부가정보 조회 |
06-10 | MUST | 스칼라 서브쿼리의 단일 값 보장 |
독립 섹션 구분: LATERAL 응용은 SECTION 07, WITH/MATERIALIZED는 SECTION 08, 최신 데이터 한 건 검색은 SECTION 09에서 상세히 다룹니다.
REMINDER 06-01. 같은 테이블에 반복 접근하는 스칼라 서브쿼리를 줄이자
MUST · 상황: 주문에 회원 닉네임·휴대전화, 매장명·테이블 수를 표시한다.
BAD — 같은 회원·매장 테이블을 반복 조회
SELECT o.ord_no, o.ord_dtm,
(SELECT m.nick_nm FROM ms_mbr_big m
WHERE m.mbr_id = o.mbr_id) AS nick_nm,
(SELECT m.mobl_no FROM ms_mbr_big m
WHERE m.mbr_id = o.mbr_id) AS mobl_no,
(SELECT s.shop_nm FROM ms_shop_big s
WHERE s.shop_id = o.shop_id) AS shop_nm,
(SELECT s.table_qty FROM ms_shop_big s
WHERE s.shop_id = o.shop_id) AS table_qty
FROM tr_ord_big o
WHERE o.ord_dtm >= TIMESTAMP '2023-01-01'
AND o.ord_dtm < TIMESTAMP '2023-01-11';
SQL
복사
GOOD — JOIN으로 필요한 속성을 함께 가져오기
SELECT o.ord_no, o.ord_dtm,
m.nick_nm, m.mobl_no, s.shop_nm, s.table_qty
FROM tr_ord_big o
LEFT JOIN ms_mbr_big m ON m.mbr_id = o.mbr_id
LEFT JOIN ms_shop_big s ON s.shop_id = o.shop_id
WHERE o.ord_dtm >= TIMESTAMP '2023-01-01'
AND o.ord_dtm < TIMESTAMP '2023-01-11';
SQL
복사
예상 실행계획
BAD: 주문 스캔
SubPlan 1 -> 회원 PK 조회
SubPlan 2 -> 회원 PK 조회
SubPlan 3 -> 매장 PK 조회
SubPlan 4 -> 매장 PK 조회
외부 주문 행 수에 따라 SubPlan 반복 가능
GOOD: Hash Left Join 또는 Nested Loop Left Join
-> 주문 집합
-> 회원·매장 JOIN
중복된 개별 SubPlan을 줄일 가능성
Plain Text
복사
핵심: 같은 키로 동일 테이블의 여러 컬럼을 반복 검색할 필요가 없다. 주의: 스칼라의 불일치 행은 NULL이므로 LEFT JOIN으로 재작성했다. 실제 키가 유일해야 결과 행 수가 보존된다.
출처: StartUP Tuning PG PDF p.229, SQL-맛튜닝-스칼라1; MySQL/Oracle GUIDE의 반복 스칼라 규칙.
REMINDER 06-02. 반복 집계와 선집계
MUST · 상황: 전체 회원의 2023년 12월 주문금액 합계를 구한다.
BAD — 회원마다 SUM 반복 가능
SELECT m.mbr_id, m.nick_nm,
(SELECT SUM(o.ord_amt)
FROM tr_ord_big o
WHERE o.mbr_id = m.mbr_id
AND o.ord_dtm >= TIMESTAMP '2023-12-01'
AND o.ord_dtm < TIMESTAMP '2024-01-01') AS ord_amt
FROM ms_mbr_big m;
SQL
복사
GOOD — 주문을 회원별로 선집계한 후 LEFT JOIN
SELECT m.mbr_id, m.nick_nm, a.ord_amt
FROM ms_mbr_big m
LEFT JOIN (
SELECT o.mbr_id, SUM(o.ord_amt) AS ord_amt
FROM tr_ord_big o
WHERE o.ord_dtm >= TIMESTAMP '2023-12-01'
AND o.ord_dtm < TIMESTAMP '2024-01-01'
GROUP BY o.mbr_id
) a ON a.mbr_id = m.mbr_id;
SQL
복사
예상 실행계획
BAD: Seq Scan on ms_mbr_big
SubPlan -> Aggregate
-> tr_ord_big 회원별 검색
외부 회원 수만큼 반복 가능
GOOD: Hash Left Join
-> 회원 스캔
-> Hash
-> HashAggregate (mbr_id)
-> 월간 주문 집합 스캔
Plain Text
복사
핵심: 외부 행이 많으면 반복 집계보다 집계 한 번 + 조인 방식을 비교하자. 주의: 주문 없는 회원의 SUM은 NULL이다. GOOD 역시 이 값을 보존한다. 소수 회원만 조회하면 스칼라 방식이 더 나을 수 있다.
근거: StartUP Tuning PG PDF p.230~231, SQL-맛튜닝-스칼라2-1/2-2.
REMINDER 06-03. 한 명의 조회에는 스칼라 방식도 유리할 수 있다
CHECK · 상황: 회원 M00010 한 명의 주문금액을 구한다. 대량 선집계를 강제하기 전에 반복 횟수를 확인한다.
BAD 후보 — 전체 월간 주문을 선집계
SELECT m.mbr_id, a.ord_amt
FROM ms_mbr_big m
LEFT JOIN (
SELECT mbr_id, SUM(ord_amt) AS ord_amt
FROM tr_ord_big
WHERE ord_dtm >= TIMESTAMP '2023-12-01'
AND ord_dtm < TIMESTAMP '2024-01-01'
GROUP BY mbr_id
) a ON a.mbr_id = m.mbr_id
WHERE m.mbr_id = 'M00010';
SQL
복사
GOOD 후보 — 해당 회원만 조회
SELECT m.mbr_id,
(SELECT SUM(o.ord_amt) FROM tr_ord_big o
WHERE o.mbr_id = m.mbr_id
AND o.ord_dtm >= TIMESTAMP '2023-12-01'
AND o.ord_dtm < TIMESTAMP '2024-01-01') AS ord_amt
FROM ms_mbr_big m
WHERE m.mbr_id = 'M00010';
SQL
복사
예상 실행계획
BAD 위험: 월간 주문 전체 Aggregate -> 회원 조인
GOOD 가능: 회원 PK Index Scan -> SubPlan 1
-> Aggregate -> 회원ID·일시 조건의 주문 인덱스 검색
Plain Text
복사
핵심: 외부 회원이 1건이면 스칼라 SUM도 적절할 수 있다. 주의: PostgreSQL이 BAD의 회원 조건을 집계 뷰에 전달하면 같은 계획이 될 수도 있다.
근거: PDF p.232, SQL-맛튜닝-스칼라2-3.
REMINDER 06-04. 기간별 반복 집계는 조건부 집계로 통합하자
TIP · 상황: 회원별 2022년 12월과 2023년 12월 주문금액을 비교한다.
BAD — 기간마다 스칼라 SUM 실행
SELECT m.mbr_id,
(SELECT SUM(o.ord_amt) FROM tr_ord_big o
WHERE o.mbr_id = m.mbr_id AND o.ord_dtm >= '2022-12-01'
AND o.ord_dtm < '2023-01-01') AS amt_2212,
(SELECT SUM(o.ord_amt) FROM tr_ord_big o
WHERE o.mbr_id = m.mbr_id AND o.ord_dtm >= '2023-12-01'
AND o.ord_dtm < '2024-01-01') AS amt_2312
FROM ms_mbr_big m;
SQL
복사
GOOD — PostgreSQL FILTER 집계
SELECT m.mbr_id, a.amt_2212, a.amt_2312
FROM ms_mbr_big m
LEFT JOIN (
SELECT o.mbr_id,
SUM(o.ord_amt) FILTER
(WHERE o.ord_dtm >= '2022-12-01' AND o.ord_dtm < '2023-01-01')
AS amt_2212,
SUM(o.ord_amt) FILTER
(WHERE o.ord_dtm >= '2023-12-01' AND o.ord_dtm < '2024-01-01')
AS amt_2312
FROM tr_ord_big o
WHERE (o.ord_dtm >= '2022-12-01' AND o.ord_dtm < '2023-01-01')
OR (o.ord_dtm >= '2023-12-01' AND o.ord_dtm < '2024-01-01')
GROUP BY o.mbr_id
) a ON a.mbr_id = m.mbr_id;
SQL
복사
예상 실행계획
BAD: 회원 스캔 -> SubPlan 1 Aggregate / SubPlan 2 Aggregate
GOOD: Hash Left Join -> HashAggregate (mbr_id)
-> 조건에 맞는 주문 검색
Plain Text
복사
핵심: 한 테이블의 여러 집계를 한 그룹 집계로 계산할 수 있다. 주의: FILTER는 교재의 CASE 집계를 PostgreSQL 문법으로 재구성한 교육용 대안이다. 기간별 선택성이 높으면 개별 인덱스 검색이 더 나을 수도 있다. 빈 집계의 NULL 의미를 유지한다.
근거: PDF p.233~234, SQL-맛튜닝-스칼라3-1/3-2.
REMINDER 06-05. WHERE 서브쿼리의 불필요한 MAX는 EXISTS로 바꾸자
MUST · 상황: 2023년 3월, PLAT 회원의 주문 건수를 회원별로 구한다.
BAD — 회원 존재 확인에 MAX 집계
SELECT o.mbr_id, COUNT(*) AS cnt
FROM tr_ord_big o
WHERE o.ord_dtm >= TIMESTAMP '2023-03-01'
AND o.ord_dtm < TIMESTAMP '2023-04-01'
AND o.mbr_id = (
SELECT MAX(m.mbr_id)
FROM ms_mbr_big m
WHERE m.mbr_id = o.mbr_id AND m.mbr_gd = 'PLAT'
)
GROUP BY o.mbr_id;
SQL
복사
GOOD — 존재 여부만 확인
SELECT o.mbr_id, COUNT(*) AS cnt
FROM tr_ord_big o
WHERE o.ord_dtm >= TIMESTAMP '2023-03-01'
AND o.ord_dtm < TIMESTAMP '2023-04-01'
AND EXISTS (
SELECT 1 FROM ms_mbr_big m
WHERE m.mbr_id = o.mbr_id AND m.mbr_gd = 'PLAT'
)
GROUP BY o.mbr_id;
SQL
복사
예상 실행계획
BAD: 주문 검색 -> Filter(SubPlan)
SubPlan -> Aggregate(MAX) -> 회원 조회
GOOD: 주문 검색 -> Nested Loop Semi Join / Hash Semi Join
-> 회원등급 필터
Plain Text
복사
핵심: PK가 유일한 회원을 확인하는 데 MAX를 사용할 필요가 없다. 주의: ms_mbr_big.mbr_id가 회원 고유키라는 전제이며, PostgreSQL이 SubPlan을 최적화하는 경우도 있다.
근거: PDF p.267, SQL-맛튜닝-WH서브쿼리1.
REMINDER 06-06. 인라인 뷰 바깥 조건의 내부 전달을 확인하자
MUST · 상황: 회원 M00001의 2023년 상반기 주문금액 합계를 구한다. 외부 회원 조건이 집계 인라인 뷰 내부 검색까지 반영되는지 살펴본다.
BAD 후보 — 회원 조건을 집계 뷰 밖에만 작성
SELECT m.mbr_id, a.ord_amt
FROM ms_mbr_big AS m
JOIN (
SELECT o.mbr_id, SUM(o.ord_amt) AS ord_amt
FROM tr_ord_big AS o
WHERE o.ord_dtm >= TIMESTAMP '2023-01-01'
AND o.ord_dtm < TIMESTAMP '2023-07-01'
GROUP BY o.mbr_id
) AS a ON a.mbr_id = m.mbr_id
WHERE m.mbr_id = 'M00001';
SQL
복사
GOOD 후보 — 필터가 내부에서도 명확하게 적용되도록 작성
SELECT m.mbr_id, a.ord_amt
FROM ms_mbr_big AS m
JOIN (
SELECT o.mbr_id, SUM(o.ord_amt) AS ord_amt
FROM tr_ord_big AS o
WHERE o.mbr_id = 'M00001'
AND o.ord_dtm >= TIMESTAMP '2023-01-01'
AND o.ord_dtm < TIMESTAMP '2023-07-01'
GROUP BY o.mbr_id
) AS a ON a.mbr_id = m.mbr_id
WHERE m.mbr_id = 'M00001';
SQL
복사
예상 실행계획 — PostgreSQL이 조건 공급에 성공한 경우
BAD와 GOOD에 공통으로 가능한 형태
Nested Loop
-> Index Scan on ms_mbr_big
Index Cond: (mbr_id = 'M00001')
-> GroupAggregate / Aggregate
-> Index Scan using tr_ord_big_x04 on tr_ord_big
Index Cond: (mbr_id = 'M00001'
AND ord_dtm >= ...
AND ord_dtm < ...)
→ 외부 회원 조건이 내부 주문 검색에도 반영됨
조건 공급에 실패하는 복잡한 구조라면
HashAggregate (기간 내 다수 회원의 주문)
-> 주문 범위 Scan
Hash Join -> 특정 회원과 결합
→ 더 넓은 주문 집계를 수행할 위험
Plain Text
복사
기억할 이유: 인라인 뷰 바깥에만 회원 조건이 있어도 PostgreSQL은 이를 내부의 Index Cond로 전달할 수 있다. SQL 문장만 보고 뷰 전체를 집계한다고 단정하지 말고 실행계획에서 확인해야 한다.
주의·예외: 원본 교재 SQL-맛튜닝-조건공급1-1 자체가 바깥 조건을 내부로 전달하는 데 성공한 사례다. 따라서 이 항목의 BAD는 '반드시 느린 SQL'이 아니라 점검 대상이다. GOOD은 특정 회원을 상수로 조회할 때만 가능한 작성 방법이며, 다수 회원을 동적으로 조회할 때는 동일 상수를 복제하지 않는다. 인덱스 x04를 사용한다는 것도 조건부 예상이다.
교재 근거: StartUPTuning_PG_SQL_Script.sql [SQL-맛튜닝-조건공급1-1]; StartUPTuning_PG_20250616.pdf p.238.
REMINDER 06-07. 조건 공급이 어렵다면 선별된 회원부터 JOIN하자
CHECK · 상황: 특정 가입일에 SNS로 가입한 GOLD 회원들의 2023년 상반기 주문합계를 구한다.
BAD 후보 — 회원과 무관하게 기간 전체의 주문을 집계할 위험
SELECT m.mbr_id, m.nick_nm, a.ord_amt
FROM ms_mbr_big AS m
JOIN (
SELECT o.mbr_id, SUM(o.ord_amt) AS ord_amt
FROM tr_ord_big AS o
WHERE o.ord_dtm >= TIMESTAMP '2023-01-01'
AND o.ord_dtm < TIMESTAMP '2023-07-01'
GROUP BY o.mbr_id
) AS a ON a.mbr_id = m.mbr_id
WHERE m.mbr_gd = 'GOLD'
AND m.join_dtm >= TIMESTAMP '2019-03-20'
AND m.join_dtm < TIMESTAMP '2019-03-21'
AND m.join_tp = 'SNS';
SQL
복사
GOOD 후보 — 회원부터 필터링·JOIN한 후 집계
SELECT m.mbr_id, m.nick_nm,
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 = 'GOLD'
AND m.join_dtm >= TIMESTAMP '2019-03-20'
AND m.join_dtm < TIMESTAMP '2019-03-21'
AND m.join_tp = 'SNS'
AND o.ord_dtm >= TIMESTAMP '2023-01-01'
AND o.ord_dtm < TIMESTAMP '2023-07-01'
GROUP BY m.mbr_id, m.nick_nm;
SQL
복사
예상 실행계획
BAD (외부 회원 조건 공급이 어려운 경우)
Hash Join
-> HashAggregate (mbr_id)
-> tr_ord_big 반기 주문 범위 스캔
-> 회원 필터링 결과
→ 불필요한 회원의 주문까지 선집계 가능
GOOD (회원 필터의 선택성이 높고 주문 인덱스가 적합한 경우)
HashAggregate / GroupAggregate
-> Nested Loop
-> ms_mbr_big (GOLD, 가입일, SNS 필터)
-> Index Scan on tr_ord_big
Index Cond: (mbr_id = m.mbr_id AND ord_dtm 범위)
→ 선별된 회원의 주문만 반복 조회 가능
다른 데이터 분포에서는 두 SQL 모두 Hash Join 등을 선택 가능
Plain Text
복사
기억할 이유: 외부 조건을 복잡한 집계 뷰 내부로 전달하기 어려운 경우, 필터링된 회원을 주문과 먼저 조인하고 나중에 GROUP BY하는 방법도 있다.
주의·예외: 두 SQL은 매칭 주문이 있는 회원만 출력하는 INNER JOIN이며, 회원 ID가 고유하고 nick_nm이 그 회원의 속성이라는 전제를 둔다. PostgreSQL이 BAD에서도 적절한 조건을 공급하거나 JOIN을 재배치하면 차이가 작을 수 있다. 적합한 주문 인덱스가 없다면 GOOD의 Nested Loop도 비효율적이다.
교재 근거: 실습 SQL SQL-맛튜닝-조건공급2-1(집계 인라인 뷰)과 SQL-맛튜닝-조건공급2-4(JOIN 후 집계). 조건공급2-2의 LATERAL, 조건공급2-3의 WITH·EXISTS 대안은 SECTION 07~08에 연결한다.
REMINDER 06-08. 단순 인라인 뷰는 PostgreSQL에서 병합될 수 있다
CHECK · 상황: 회원 M00019의 2023년 3월 주문번호·금액을 조회한다.
BAD 후보 — 불필요한 단순 인라인 뷰를 먼저 만든다고 생각하는 작성 방식
SELECT v.ord_no, v.ord_amt
FROM (
SELECT o.ord_no, o.ord_amt, o.mbr_id
FROM tr_ord_big AS o
WHERE o.ord_dtm >= TIMESTAMP '2023-03-01'
AND o.ord_dtm < TIMESTAMP '2023-04-01'
) AS v
WHERE v.mbr_id = 'M00019';
SQL
복사
GOOD — 간단한 조건은 한 SELECT로 작성
SELECT o.ord_no, o.ord_amt
FROM tr_ord_big AS o
WHERE o.mbr_id = 'M00019'
AND o.ord_dtm >= TIMESTAMP '2023-03-01'
AND o.ord_dtm < TIMESTAMP '2023-04-01';
SQL
복사
예상 실행계획
BAD와 GOOD에 공통으로 가능한 형태
Index Scan using tr_ord_big_x04 on tr_ord_big
Index Cond: (mbr_id = 'M00019'
AND ord_dtm >= ...
AND ord_dtm < ...)
→ 단순 인라인 뷰가 상위 SELECT와 통합되면 같은 접근 경로
인라인 뷰 내부에 집계·DISTINCT·LIMIT 등이 있으면
Subquery Scan / Aggregate / Sort 등의 노드가 남을 수 있음
Plain Text
복사
기억할 이유: 괄호로 감싼 서브쿼리가 반드시 독립적으로 끝까지 실행되고 중간 결과가 저장되는 것은 아니다. 단순한 인라인 뷰는 옵티마이저가 상위 SQL과 병합할 수 있다.
주의·예외: 이 BAD는 성능 오류가 아니라 불필요한 구조 또는 잘못된 실행 순서 가정을 보여준다. LIMIT, DISTINCT, 집계가 있는 인라인 뷰의 경계를 무작정 없애면 결과의 의미가 달라질 수 있다. CTE의 MATERIALIZED 제어는 SECTION 08에서 따로 설명한다.
자료 근거: PG PDF p.233의 인라인 뷰 병합 설명 및 p.238의 외부 조건 공급 사례를 바탕으로 단순 SQL을 재구성한 교육용 예제다.
REMINDER 06-09. 최종 출력에 필요한 부가정보는 행을 먼저 선별한 뒤 조회하자
TIP · 상황: 2023년 1월 1~10일의 주문금액 상위 10건에 회원 닉네임을 표시한다.
BAD 후보 — 정렬과 LIMIT이 있는 SELECT에서 스칼라 서브쿼리를 함께 작성
SELECT o.ord_no, o.ord_amt,
(SELECT m.nick_nm
FROM ms_mbr_big AS m
WHERE m.mbr_id = o.mbr_id) AS nick_nm
FROM tr_ord_big AS o
WHERE o.ord_dtm >= TIMESTAMP '2023-01-01'
AND o.ord_dtm < TIMESTAMP '2023-01-11'
ORDER BY o.ord_amt DESC NULLS LAST, o.ord_no DESC
LIMIT 10;
SQL
복사
GOOD 후보 — 상위 주문 10건을 확정하고 회원과 JOIN
SELECT q.ord_no, q.ord_amt, m.nick_nm
FROM (
SELECT o.ord_no, o.mbr_id, o.ord_amt
FROM tr_ord_big AS o
WHERE o.ord_dtm >= TIMESTAMP '2023-01-01'
AND o.ord_dtm < TIMESTAMP '2023-01-11'
ORDER BY o.ord_amt DESC NULLS LAST, o.ord_no DESC
LIMIT 10
) AS q
LEFT JOIN ms_mbr_big AS m
ON m.mbr_id = q.mbr_id
ORDER BY q.ord_amt DESC NULLS LAST, q.ord_no DESC;
SQL
복사
예상 실행계획
BAD (PostgreSQL이 출력 표현식 계산을 늦출 수 있는 경우)
Limit
-> Sort / Top-N Sort / 정렬 인덱스 Scan
-> 기간 내 주문
SubPlan -> 회원 PK Index Scan
→ SubPlan이 실제 상위 10행에 대해서만 실행될 수도 있음
GOOD (회원 PK 인덱스 존재 시 가능한 형태)
Nested Loop Left Join
-> Limit
-> Sort / Index Scan
-> 기간 내 주문
-> Index Scan on ms_mbr_big
Index Cond: (mbr_id = q.mbr_id)
→ 최종 상위 10행에 회원 부가정보 연결
Plain Text
복사
기억할 이유: 주문 순위는 주문금액과 주문번호만으로 정해진다. 따라서 회원 닉네임은 순위를 확정한 후에 조회해도 결과가 달라지지 않는다는 점을 이용할 수 있다.
주의·예외: BAD SQL에서도 PostgreSQL이 스칼라 서브쿼리 평가를 LIMIT 뒤로 미루는 경우가 있으므로, 반드시 10행보다 많이 조회한다고 단정하지 않는다. GOOD은 회원 PK가 유일하고 LEFT JOIN으로 행 보존을 유지하는 전제다. 닉네임으로 정렬하거나 회원 컬럼을 필터에 사용한다면 LIMIT 이후 JOIN은 결과가 달라질 수 있다. 자세한 페이징 최적화는 SECTION 10에서 다룬다.
자료 근거: 기존 Oracle/MySQL SQL GUIDE의 스칼라 서브쿼리·출력 데이터 최소화 규칙을 응용한 교육용 SQL이다. 제시한 주문 TOP 10 SQL 자체의 원본 실측 사례는 확인되지 않았다.
REMINDER 06-10. 스칼라 서브쿼리는 한 행·한 컬럼의 값을 반환해야 한다
MUST · 상황: 회원 M00019의 모든 주문금액 합계를 회원 행에 표시한다.
BAD — 주문이 여러 건이면 스칼라 서브쿼리가 여러 행을 반환
SELECT m.mbr_id,
(SELECT o.ord_amt
FROM tr_ord_big AS o
WHERE o.mbr_id = m.mbr_id) AS ord_amt
FROM ms_mbr_big AS m
WHERE m.mbr_id = 'M00019';
SQL
복사
GOOD — 업무 목적에 맞게 SUM 집계하여 단일 값으로 만든다
SELECT m.mbr_id,
(SELECT SUM(o.ord_amt)
FROM tr_ord_big AS o
WHERE o.mbr_id = m.mbr_id) AS ord_amt
FROM ms_mbr_big AS m
WHERE m.mbr_id = 'M00019';
SQL
복사
예상 실행계획 및 오류
BAD (해당 회원 주문이 2건 이상이라면)
Index Scan on ms_mbr_big
SubPlan 1
-> Index Scan on tr_ord_big (mbr_id = m.mbr_id)
ERROR: more than one row returned by a subquery used as an expression
GOOD (가능한 형태)
Index Scan on ms_mbr_big
SubPlan 1
-> Aggregate (SUM)
-> Index Scan / Seq Scan on tr_ord_big
Index Cond 또는 Filter: (mbr_id = m.mbr_id)
→ SUM은 일치 주문이 없을 때 NULL인 단일 집계 값 반환
Plain Text
복사
기억할 이유: SELECT 절의 스칼라 서브쿼리는 표현식 하나로 사용되므로 최대 한 행, 한 컬럼이 필요하다. 여러 주문의 합계가 요구사항이면 반드시 SUM 등 집계로 단일 값을 산출한다.
주의·예외: 업무 목적이 최신 주문금액 한 건이라면 SUM이 아니라 ORDER BY ... LIMIT 1 등이 맞다(SECTION 09). 임의 LIMIT 1로 오류만 감추면 어떤 행이 선택되는지 정의되지 않을 수 있다. 0건 결과는 스칼라 표현식에서 NULL로 평가된다.
자료 근거: PG 실습 SQL-맛튜닝-스칼라2-1은 SUM 집계를 사용한다. 다중 행 스칼라 오류 예제 자체는 PostgreSQL의 일반적인 SQL 의미를 설명하기 위해 추가했으며 원본의 튜닝 사례는 아니다.
서브쿼리를 변경할 때 확인할 세 가지
1. 반복 실행량: SubPlan이 실제로 외부 몇 행에 대해 실행되는가? 외부 행이 한 건이면 스칼라가 효율적일 수 있고, 많다면 선집계를 비교한다.
2. 조건 공급: 바깥의 회원·기간 조건이 인라인 뷰 안까지 전달돼 Index Cond로 쓰이는가? SQL에 인라인 뷰가 있다는 이유만으로 모든 데이터를 먼저 집계한다고 판단하지 않는다.
3. 결과 동등성: 스칼라 서브쿼리를 JOIN으로 변경할 때 NULL·중복 행·집계 단위·INNER/LEFT 의미가 유지되는지 점검한다.
결과가 같은지 비교하는 예제
아래는 06-08의 단순 인라인 뷰와 직접 WHERE 조건 SQL을 비교하는 실행 가능한 검증 예제다. EXCEPT ALL은 중복 행까지 구분하므로 양방향으로 비교한다.
WITH old_result AS (
SELECT v.ord_no, v.ord_amt
FROM (
SELECT o.ord_no, o.ord_amt, o.mbr_id
FROM tr_ord_big AS o
WHERE o.ord_dtm >= TIMESTAMP '2023-03-01'
AND o.ord_dtm < TIMESTAMP '2023-04-01'
) AS v
WHERE v.mbr_id = 'M00019'
),
new_result AS (
SELECT o.ord_no, o.ord_amt
FROM tr_ord_big AS o
WHERE o.mbr_id = 'M00019'
AND o.ord_dtm >= TIMESTAMP '2023-03-01'
AND o.ord_dtm < TIMESTAMP '2023-04-01'
)
SELECT 'OLD_ONLY' AS diff, d.ord_no, d.ord_amt
FROM (
SELECT * FROM old_result
EXCEPT ALL
SELECT * FROM new_result
) AS d
UNION ALL
SELECT 'NEW_ONLY' AS diff, d.ord_no, d.ord_amt
FROM (
SELECT * FROM new_result
EXCEPT ALL
SELECT * FROM old_result
) AS d;
SQL
복사
판단: 차이 결과가 0행이면 두 SELECT의 행 값과 중복 개수가 일치한다. 단, 행의 정렬 순서는 집합 비교 대상이 아니므로 ORDER BY가 중요한 업무에서는 별도 검증이 필요하다. 데이터가 변경되는 중에는 하나의 SQL 문장 내 스냅샷 기준으로 비교한다.
실습 DB가 준비되면 계획 확인
-- 실제 쿼리를 실행하지 않고 예상 계획 확인
EXPLAIN (COSTS OFF)
SELECT m.mbr_id,
(SELECT SUM(o.ord_amt)
FROM tr_ord_big AS o
WHERE o.mbr_id = m.mbr_id
AND o.ord_dtm >= TIMESTAMP '2023-12-01'
AND o.ord_dtm < TIMESTAMP '2024-01-01') AS ord_amt
FROM ms_mbr_big AS m
WHERE m.mbr_id = 'M00010';
-- 조회 SQL을 실제로 실행해 성능 정보를 확인할 때만
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT m.mbr_id,
(SELECT SUM(o.ord_amt)
FROM tr_ord_big AS o
WHERE o.mbr_id = m.mbr_id
AND o.ord_dtm >= TIMESTAMP '2023-12-01'
AND o.ord_dtm < TIMESTAMP '2024-01-01') AS ord_amt
FROM ms_mbr_big AS m
WHERE m.mbr_id = 'M00010';
SQL
복사
확인할 노드: SubPlan의 loops, Aggregate의 반복, Nested Loop의 Driven 접근, Index Cond와 Filter, 뷰 내부의 actual rows, Buffers를 살펴본다. EXPLAIN ANALYZE는 SQL을 실제로 수행한다.
개발 전 체크리스트
동일 키의 같은 테이블을 여러 스칼라 서브쿼리에서 반복 조회하는가?
외부 행이 많은데 SUM·COUNT를 매번 반복 계산하는가?
조회 대상이 극히 적은 경우 스칼라 방식의 이익도 검토했는가?
여러 기간을 따로 집계할 때 하나의 조건부 집계로 합칠 수 있는가?
존재 여부만 확인하기 위해 MAX 같은 불필요한 집계를 사용하지 않는가?
인라인 뷰 밖의 회원 조건이 내부 검색까지 전달되는가?
조건 전달이 어려운 집계 뷰를 선별 후 JOIN 방식으로 바꿀 수 있는가?
인라인 뷰 자체를 중간 결과의 물리 저장으로 오해하지 않는가?
정렬·LIMIT 후에 필요한 컬럼만 별도 조회할 수 있는가?
스칼라 서브쿼리는 한 행·한 컬럼을 보장하는가?
재작성 전후 NULL·중복·집계 결과·행 보존 규칙이 동일한가?
예상 실행계획을 실측 결과라고 오인하지 않도록 구분했는가?
자료 근거 및 교육용 확장 범위
원본에서 확인한 PostgreSQL 실습
•
StartUPTuning_PG_SQL_Script.sql — SQL-맛튜닝-스칼라1: 회원·매장 반복 스칼라 서브쿼리.
•
SQL-맛튜닝-스칼라2-1/2-2/2-3: 월별 SUM, JOIN으로의 재작성, 결과 검증 및 단일 회원 예외.
•
SQL-맛튜닝-스칼라3-1/3-2: 두 기간의 SUM과 CASE를 이용한 조건부 집계.
•
SQL-맛튜닝-WH서브쿼리1: 상관 서브쿼리와 MAX 활용 문제를 다루는 실습.
•
SQL-맛튜닝-조건공급1-1: 외부 회원 조건이 인라인 뷰 내부까지 공급되는 사례.
•
SQL-맛튜닝-조건공급2-1/2-4: 집계 인라인 뷰와 JOIN 후 집계 비교. 2-2 LATERAL 및 2-3 WITH·EXISTS 대안은 SECTION 07~08에서 설명.
•
StartUPTuning_PG_20250616.pdf p.229~234(스칼라·조건부 집계), p.238~242(조건 공급), p.267(WHERE 서브쿼리).
실전 분류 및 일반화
•
— 0317_03 반복 스칼라 서브쿼리, 0915_37 중복 스칼라 제거, 0702_04 내부 조건 추가 관련 기록.
•
06-04의 PostgreSQL FILTER (WHERE ...), 06-08의 단순 인라인 뷰 병합, 06-09의 TOP 10 후 JOIN, 06-10의 다중 행 스칼라 오류, EXCEPT ALL 검증은 학습을 위해 추가하거나 재구성한 PostgreSQL 일반 패턴이다.
•
이 문서의 예제 SQL과 예상 계획은 실습 자료의 개념을 BIG 계열에 맞춰 재작성한 것이며, 실행·측정한 실제 계획이나 성능 수치를 포함하지 않는다.
그림 제작 메모 — 현재 실제 이미지는 삽입하지 않음
•
그림 A · 반복 스칼라 조회: 외부 주문 N건에서 회원·매장 SubPlan으로 여러 번 접근하는 BAD와 JOIN을 이용한 GOOD 비교
•
그림 B · 선집계와 반복 집계: 회원 수만큼 SUM이 수행되는 형태와 주문 한 번 집계 후 LEFT JOIN의 차이
•
그림 C · 인라인 뷰 조건 공급: 바깥 mbr_id = 'M00001' 조건이 내부 Index Cond에 전달되는 성공 사례와 전달되지 않는 복잡한 구조
•
그림 D · 집계 순서 전환: 기간 전체 GROUP BY 후 회원 JOIN과 필터된 회원을 주문에 JOIN 후 GROUP BY하는 접근 범위 차이
SECTION 06 한 줄 요약: 반복 서브쿼리는 수행 횟수를 줄이고, 인라인 뷰는 조건 공급을 확인하며, 재작성 전후 결과의 정확성을 먼저 검증하자.




