스칼라 서브쿼리 반복 제거: 사전 집계·LATERAL·구조화 반환
같은 범위를 여러 Scalar Subquery가 반복 읽는 문제를 사전 집계 Join·LATERAL과 구조화된 반환 방식으로 개선합니다.
핵심 요약
같은 상세 Table을 여러 스칼라 서브쿼리가 반복해서 읽으면 SQL은 짧아 보여도 실제 작업량이 커질 수 있습니다. 특히 평균·최소·최대·건수처럼 같은 행 집합에서 여러 값을 구하는 경우에는 다음 대안을 비교합니다.
같은 Source를 여러 번 조회
→ 한 번의 집계로 여러 값을 계산
Outer가 많고 전체 처리가 중요
→ 사전 집계 후 Join
Outer가 작고 Key별 선택적 접근이 유리
→ LATERAL·OUTER APPLY
여러 Column을 한 번에 반환
→ LATERAL·APPLY를 우선 검토
→ Object Type은 특별한 요구가 있을 때 제한적으로 사용
성능만 바꾸고 결과 의미를 놓치지 않도록 한 행의 Grain, 0건일 때의 NULL·0, 중복, Outer 행 보존, Top-1의 동점 규칙을 함께 검증해야 합니다.
다중 Scalar Lookup
→ Outer Key별 선택적 반복 처리
사전 집계 Join
→ 상세 Source를 집합으로 처리
LATERAL·APPLY
→ Outer Key를 참조하면서 여러 Column·여러 Row 반환 가능
Object Type
→ 여러 Attribute를 가진 단일 Scalar 값
이 이론의 범위
SQLP
SQL 고급 활용 및 튜닝 → 스칼라 서브쿼리범위에서 다중 Scalar 반복 제거, 사전 집계, LATERAL·CROSS APPLY·OUTER APPLY, Top-1 다중 Column 반환, Object Type과 실행계획 비교를 다룹니다.
학습 목표
이 이론을 학습한 뒤에는 다음을 설명할 수 있어야 합니다.
- 같은 Source를 반복하는 다중 스칼라 서브쿼리를 식별한다.
- 한 번의 집계로 여러 집계값을 함께 계산한다.
- 사전 집계 Join과 상관 Lookup의 비용 구조를 비교한다.
LATERAL,CROSS APPLY,OUTER APPLY의 행 보존 차이를 구분한다.- Top-1 관련 행의 여러 Column을 한 번에 반환하는 방법을 작성한다.
- Object Type과 문자열 결합 방식의 장점·제약을 판단한다.
CROSS APPLY와OUTER APPLY에서 오른쪽 0행·1행·다중행의 결과를 판단한다.- GROUP BY 없는 Aggregate APPLY가 빈 입력에서도 한 행을 만드는 예외를 설명한다.
- Object Type Constructor의 Attribute 개수·순서·타입 제약을 설명한다.
- 변환 전후의 Grain·NULL·COUNT 0·중복·정렬 기준을 검증한다.
1. 반복되는 다중 스칼라 서브쿼리
다음 SQL은 고객별 이번 달 거래의 평균·최소·최대 금액을 조회합니다.
SELECT c.customer_id,
c.customer_name,
(SELECT AVG(t.amount)
FROM trade t
WHERE t.customer_id = c.customer_id
AND t.trade_date >= TRUNC(SYSDATE, 'MM')) AS avg_amount,
(SELECT MIN(t.amount)
FROM trade t
WHERE t.customer_id = c.customer_id
AND t.trade_date >= TRUNC(SYSDATE, 'MM')) AS min_amount,
(SELECT MAX(t.amount)
FROM trade t
WHERE t.customer_id = c.customer_id
AND t.trade_date >= TRUNC(SYSDATE, 'MM')) AS max_amount
FROM customer c
WHERE c.signup_date >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -1);
세 서브쿼리는 다음 범위를 동일하게 사용합니다.
TRADE
WHERE customer_id = 현재 고객
AND trade_date >= 이번 달 시작일
논리적으로는 같은 범위를 세 번 조회합니다. Optimizer가 일부 변환을 수행할 수 있으므로 실제 세 번 읽었다고 SQL Text만으로 확정하지 않고 실행계획을 확인해야 합니다. 그러나 실제 Cursor에서 각 서브쿼리의 Starts와 Buffers가 반복된다면 한 번의 집계로 합치는 것이 유력한 개선 방향입니다.
반복 작업량의 기본 구조
Outer 고객 수 = 20,000
각 Scalar Subquery = 3개
Subquery 1회 평균 Buffers = 4
예상 반복 Buffers
≈ 20,000 × 3 × 4
= 240,000
상관 Key의 반복과 실행 중 재사용이 있더라도, 서로 다른 고객 수가 많으면 재사용 효과가 제한될 수 있습니다.
다만 SQL Text에 Scalar Subquery가 세 개 보인다는 사실만으로 물리적으로 항상 세 번 Scan했다고 단정하지 않습니다.
확인 대상
→ 각 Row Source의 Starts
→ 동일 Object Access의 반복
→ Subquery Unnesting·View Merging
→ Buffers·A-Time
Optimizer가 동등한 구조로 변환했다면 실제 반복 횟수는 SQL 모양과 다를 수 있습니다.
2. 같은 집합에서 여러 집계값을 한 번에 계산하기
평균·최소·최대는 동일한 입력 Row Set에서 계산할 수 있습니다.
SELECT customer_id,
AVG(amount) AS avg_amount,
MIN(amount) AS min_amount,
MAX(amount) AS max_amount,
COUNT(*) AS trade_count
FROM trade
WHERE trade_date >= TRUNC(SYSDATE, 'MM')
GROUP BY customer_id;
하나의 GROUP BY Operation이 입력을 읽으며 여러 Aggregate를 함께 계산합니다.
TRADE 입력 한 번 처리
→ 고객번호별 Group 생성
→ AVG·MIN·MAX·COUNT 동시 계산
Aggregate 식이 네 개라고 해서 Table을 네 번 읽는 구조가 되지는 않습니다. 실행계획에서는 하나의 HASH GROUP BY 또는 SORT GROUP BY 아래에 입력 Access가 배치될 수 있습니다.
다만 Aggregate마다 입력 의미가 다르면 한 번의 단순 집계로 합칠 수 없습니다.
SUM(CASE WHEN trade_type = 'BUY' THEN amount END)
SUM(CASE WHEN trade_type = 'SELL' THEN amount END)
COUNT(DISTINCT product_id)
조건부 Aggregate는 같은 Row Set에서 함께 계산할 수 있지만, 서로 다른 날짜 범위·Join 관계·DISTINCT Key가 필요하면 Workarea와 결과 의미를 별도로 검토합니다.
3. 사전 집계 후 LEFT JOIN
Outer 고객이 많고 결과를 끝까지 처리한다면 상세 데이터를 한 번 집계한 뒤 Join하는 방식이 유리할 수 있습니다.
SELECT c.customer_id,
c.customer_name,
a.avg_amount,
a.min_amount,
a.max_amount,
a.trade_count
FROM customer c
LEFT JOIN (
SELECT t.customer_id,
AVG(t.amount) AS avg_amount,
MIN(t.amount) AS min_amount,
MAX(t.amount) AS max_amount,
COUNT(*) AS trade_count
FROM trade t
WHERE t.trade_date >= TRUNC(SYSDATE, 'MM')
GROUP BY t.customer_id
) a
ON a.customer_id = c.customer_id
WHERE c.signup_date >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -1);
결과 Grain
사전 집계 Row Source는 다음 Grain을 가져야 합니다.
한 행 = 고객 한 명의 이번 달 거래 집계
따라서 GROUP BY에는 고객 한 명을 식별하는 Key만 들어갑니다.
GROUP BY customer_id
다음처럼 상태까지 Group Key에 포함하면 고객당 여러 행이 생깁니다.
GROUP BY customer_id, trade_status
그 결과 고객 한 행이 거래 상태 수만큼 증가할 수 있습니다.
0건일 때의 결과
원래 Aggregate Scalar Subquery는 거래가 없는 고객에게 다음 값을 반환합니다.
AVG·MIN·MAX → NULL
COUNT(*) → 0
사전 집계 Row Source에는 거래 없는 고객의 행이 존재하지 않으므로 LEFT JOIN 후 모든 집계 Column이 NULL이 됩니다.
따라서 거래 건수 의미까지 같게 만들려면 다음처럼 처리할 수 있습니다.
NVL(a.trade_count, 0) AS trade_count
반면 평균·최소·최대는 업무상 NULL을 유지하는 편이 자연스러울 수 있습니다. 모든 Aggregate에 일괄적으로 NVL(..., 0)을 적용하면 결과 의미가 바뀔 수 있습니다.
4. 필요한 Outer Key만 먼저 줄인 뒤 집계하기
전체 TRADE를 모두 집계하면 대상 고객이 적을 때 불필요한 작업이 생길 수 있습니다.
예를 들어 최근 가입 고객이 전체의 0.1%라면 다음 흐름이 더 적합할 수 있습니다.
대상 고객을 먼저 선별
→ 대상 고객의 거래만 읽음
→ 고객별 집계
→ 결과 결합
개념적인 SQL은 다음과 같습니다.
CTE를 작성했다는 사실만으로 target_customer가 반드시 먼저 물리적으로 Materialize되는 것은 아닙니다. Optimizer는 View Merging·Predicate Pushing·Join 순서 변경을 수행할 수 있으므로 실제 Plan에서 대상 Key 제한이 집계 입력을 줄였는지 확인합니다.
WITH target_customer AS (
SELECT customer_id,
customer_name
FROM customer
WHERE signup_date >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -1)
),
trade_agg AS (
SELECT t.customer_id,
AVG(t.amount) AS avg_amount,
MIN(t.amount) AS min_amount,
MAX(t.amount) AS max_amount,
COUNT(*) AS trade_count
FROM trade t
JOIN target_customer c
ON c.customer_id = t.customer_id
WHERE t.trade_date >= TRUNC(SYSDATE, 'MM')
GROUP BY t.customer_id
)
SELECT c.customer_id,
c.customer_name,
a.avg_amount,
a.min_amount,
a.max_amount,
NVL(a.trade_count, 0) AS trade_count
FROM target_customer c
LEFT JOIN trade_agg a
ON a.customer_id = c.customer_id;
Optimizer는 CTE를 Merge하거나 다른 Join Order를 선택할 수 있습니다. 핵심은 문법 자체가 아니라 Predicate 적용 후 실제 입력 행 수를 줄였는지 확인하는 것입니다.
5. LATERAL·CROSS APPLY·OUTER APPLY
LATERAL 또는 APPLY는 오른쪽 Inline View가 왼쪽 Row Source의 Column을 참조할 수 있게 합니다.
왼쪽 행
→ 오른쪽 Inline View에 Key 전달
→ 오른쪽에서 한 행 또는 여러 행·여러 Column 반환
CROSS APPLY
오른쪽에서 결과가 있는 왼쪽 행만 반환합니다.
SELECT d.deptno,
x.empno,
x.ename,
x.sal
FROM dept d
CROSS APPLY (
SELECT e.empno,
e.ename,
e.sal
FROM emp e
WHERE e.deptno = d.deptno
) x;
부서에 사원이 없으면 해당 부서는 결과에서 제외됩니다.
OUTER APPLY
오른쪽 결과가 없어도 왼쪽 행을 보존하고 오른쪽 Column을 NULL로 반환합니다.
SELECT d.deptno,
x.empno,
x.ename,
x.sal
FROM dept d
OUTER APPLY (
SELECT e.empno,
e.ename,
e.sal
FROM emp e
WHERE e.deptno = d.deptno
) x;
행 보존 의미는 LEFT OUTER JOIN과 비슷하지만, 오른쪽 Inline View가 왼쪽 Column을 직접 참조할 수 있다는 차이가 있습니다.
LATERAL Inline View
SELECT d.deptno,
x.empno,
x.ename,
x.sal
FROM dept d,
LATERAL (
SELECT e.empno,
e.ename,
e.sal
FROM emp e
WHERE e.deptno = d.deptno
) x;
LATERAL은 오른쪽 Subquery 안에서 왼쪽의 d.deptno를 참조하도록 허용합니다.
LATERAL·APPLY의 정확한 행 보존 규칙
LATERAL Inline View는 FROM 절에서 자신보다 왼쪽에 있는 Table의 Column을 참조할 수 있습니다.
SELECT c.customer_id,
x.trade_date,
x.amount
FROM customer c
LEFT JOIN LATERAL (
SELECT t.trade_date,
t.amount
FROM trade t
WHERE t.customer_id = c.customer_id
ORDER BY t.trade_date DESC, t.trade_id DESC
FETCH FIRST 1 ROW ONLY
) x
ON 1 = 1;
Oracle의 CROSS APPLY와 OUTER APPLY도 왼쪽 상관 참조를 지원합니다.
| 오른쪽 결과 | CROSS APPLY | OUTER APPLY |
|---|---|---|
| 0행 | 왼쪽 Row 제거 | 왼쪽 Row 보존, 오른쪽 Column NULL |
| 1행 | 왼쪽 Row 1행 | 왼쪽 Row 1행 |
| N행 | 왼쪽 Row N배 확장 | 왼쪽 Row N배 확장 |
중요한 예외가 있습니다. GROUP BY 없는 Aggregate Subquery는 입력이 없어도 Aggregate Row 한 건을 만듭니다.
SELECT c.customer_id,
x.avg_amount,
x.trade_count
FROM customer c
CROSS APPLY (
SELECT AVG(t.amount) AS avg_amount,
COUNT(*) AS trade_count
FROM trade t
WHERE t.customer_id = c.customer_id
) x;
거래가 없는 고객도 오른쪽 Aggregate가 다음 한 행을 반환합니다.
AVG_AMOUNT = NULL
TRADE_COUNT = 0
따라서 이 예제에서는 CROSS APPLY라도 왼쪽 고객이 제거되지 않습니다.
반대로 GROUP BY가 있으면 생성할 Group이 없을 때 오른쪽 결과가 0행이 될 수 있습니다.
CROSS APPLY (
SELECT t.trade_status,
COUNT(*) AS cnt
FROM trade t
WHERE t.customer_id = c.customer_id
GROUP BY t.trade_status
) x
이때 거래 없는 고객은 CROSS APPLY에서 제거되고 OUTER APPLY에서는 보존됩니다.
6. Top-1 행의 여러 Column을 한 번에 가져오기
부서별 최고 급여 사원의 이름·급여·입사일을 조회한다고 가정합니다.
여러 스칼라 서브쿼리를 각각 작성하면 동일한 정렬 범위를 반복할 수 있습니다.
SELECT d.deptno,
(SELECT e.ename
FROM emp e
WHERE e.deptno = d.deptno
ORDER BY e.sal DESC, e.empno
FETCH FIRST 1 ROW ONLY) AS ename,
(SELECT e.sal
FROM emp e
WHERE e.deptno = d.deptno
ORDER BY e.sal DESC, e.empno
FETCH FIRST 1 ROW ONLY) AS sal
FROM dept d;
OUTER APPLY를 사용하면 한 번 선택한 관련 행에서 여러 Column을 가져올 수 있습니다.
SELECT d.deptno,
x.empno,
x.ename,
x.sal,
x.hiredate
FROM dept d
OUTER APPLY (
SELECT e.empno,
e.ename,
e.sal,
e.hiredate
FROM emp e
WHERE e.deptno = d.deptno
ORDER BY e.sal DESC,
e.empno ASC
FETCH FIRST 1 ROW ONLY
) x;
Tie-Breaker가 필요한 이유
ORDER BY e.sal DESC
급여가 같은 사원이 여러 명이면 어느 행이 선택되는지 안정적으로 정의하지 못합니다.
ORDER BY e.sal DESC,
e.empno ASC
EMPNO를 Tie-Breaker로 추가하면 최고 급여 동률일 때 가장 작은 사원번호를 선택한다는 업무 규칙이 생깁니다.
FETCH FIRST 1 ROW ONLY를 제거하거나 WITH TIES를 사용하면 오른쪽에서 여러 Row가 반환될 수 있습니다. APPLY는 Scalar Subquery처럼 다중행 오류를 발생시키는 것이 아니라 왼쪽 Row를 Match 수만큼 확장합니다.
Scalar Subquery 다중행
→ ORA-01427
LATERAL·APPLY 다중행
→ 왼쪽 Row 증식
따라서 “여러 Column을 반환한다”와 “Outer Key당 최대 한 Row를 반환한다”는 서로 다른 조건입니다.
Index와 결합
다음 Index가 있다면 부서별 Top-1을 빠르게 찾을 가능성이 있습니다.
CREATE INDEX emp_dept_sal_ix
ON emp(deptno, sal DESC, empno ASC);
다만 Outer 부서 수가 매우 많으면 부서별 반복 Probe가 누적될 수 있으므로 전체 정렬·분석 함수 대안과 실제 Buffers를 비교합니다.
7. Aggregate와 APPLY를 함께 사용하기
Outer 고객 수가 작고 TRADE(customer_id, trade_date) Index가 효율적이라면 고객별 집계를 OUTER APPLY로 표현할 수 있습니다.
SELECT c.customer_id,
c.customer_name,
x.avg_amount,
x.min_amount,
x.max_amount,
x.trade_count
FROM customer c
OUTER APPLY (
SELECT AVG(t.amount) AS avg_amount,
MIN(t.amount) AS min_amount,
MAX(t.amount) AS max_amount,
COUNT(*) AS trade_count
FROM trade t
WHERE t.customer_id = c.customer_id
AND t.trade_date >= TRUNC(SYSDATE, 'MM')
) x
WHERE c.signup_date >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -1);
GROUP BY 없는 Aggregate Subquery는 일치 행이 없어도 결과 행 하나를 만들기 때문에 이 예제에서는 오른쪽이 한 행을 반환합니다.
따라서 이 경우 OUTER APPLY는 행 보존을 위해 반드시 필요한 문법은 아닐 수 있습니다. CROSS APPLY도 오른쪽 Aggregate Row 한 건을 받습니다. 하지만 향후 GROUP BY·HAVING·Top-N Filter가 추가돼 오른쪽이 실제 0행이 될 수 있다면 두 문법의 결과가 달라질 수 있습니다. 다음 차이를 기억합니다.
AVG·MIN·MAX → NULL
COUNT(*) → 0
유리할 수 있는 조건
- Outer 고객 수가 작음
- 고객별 거래 범위를 Index로 좁게 탐색
- 첫 행 응답이 중요
- 전체
TRADE집계보다 대상 고객 Lookup이 훨씬 적음
불리할 수 있는 조건
- Outer 고객 수가 큼
- 서로 다른 고객 Key가 매우 많음
- 고객별 거래 범위가 넓음
- 반복 Index Probe와 Table Access가 누적됨
8. Object Type으로 여러 값을 한 값처럼 반환하기
스칼라 서브쿼리는 한 개의 값을 반환해야 하지만, Object Type 인스턴스 하나도 단일 값으로 취급할 수 있습니다.
CREATE TYPE trade_stat_t AS OBJECT (
avg_amount NUMBER,
min_amount NUMBER,
max_amount NUMBER
);
/
SELECT c.customer_id,
(SELECT trade_stat_t(
AVG(t.amount),
MIN(t.amount),
MAX(t.amount)
)
FROM trade t
WHERE t.customer_id = c.customer_id) AS trade_stat
FROM customer c;
이 방식은 여러 Attribute를 가진 구조화된 값을 하나로 반환할 수 있습니다.
Object Type 이름과 같은 System-Defined Constructor를 호출할 때는 Attribute 정의 순서에 맞춰 값의 개수와 타입을 전달합니다.
TYPE Attribute
1. avg_amount NUMBER
2. min_amount NUMBER
3. max_amount NUMBER
Constructor
trade_stat_t(avg_value, min_value, max_value)
Argument 개수가 Attribute 수와 다르거나 호환되지 않는 타입을 전달하면 올바른 Object 인스턴스를 만들 수 없습니다. Attribute 순서를 바꾸면 문법상 타입이 호환되더라도 의미가 뒤바뀔 수 있습니다.
Object 자체가 NULL인 경우와 Object는 존재하지만 모든 Attribute가 NULL인 경우도 구분합니다.
적용 제약
- Schema Object Type을 생성·관리해야 함
- Type 변경 시 의존 객체 영향이 생길 수 있음
- 일반 SQL 사용자가 Attribute 접근 문법을 추가로 익혀야 함
- 단순 조회에서
LATERAL·APPLY·사전 집계 Join보다 복잡할 수 있음 - 애플리케이션 Driver의 Object Type 처리 방식을 확인해야 함
Object Type은 재사용 가능한 도메인 구조가 실제로 필요할 때 검토하고, 단순히 여러 Column을 SELECT하기 위한 기본 해법으로 사용하지 않습니다.
9. 문자열 결합 후 분해하는 방식
여러 값을 고정 길이 문자열이나 구분자로 결합해 스칼라 값 하나로 만든 뒤 다시 SUBSTR·INSTR로 분해하는 방식도 가능합니다.
평균 || '|' || 최소 || '|' || 최대
이 방식은 다음 위험이 있습니다.
- NULL이 결합 결과에서 사라지거나 위치가 바뀜
- 숫자·날짜가 NLS 설정에 따라 다른 문자열로 변환됨
- 값 길이가 예상보다 커져 경계가 깨짐
- 구분자가 실제 값에 포함될 수 있음
- 다중 Byte 문자에서 길이 계산이 복잡해짐
- 바깥 Query에서 다시 형변환해야 함
- 데이터 타입 검증이 실행 시점까지 늦어짐
10|20|30같은 값이 실제 데이터와 포장 구분을 혼동할 수 있음- 숫자 Format·소수점 문자·날짜 Calendar가 Session NLS에 영향받을 수 있음
Legacy SQL을 해석하기 위해 알아둘 수 있지만, 신규 설계에서는 Typed Column을 유지하는 사전 집계·LATERAL·Object Type을 우선 검토합니다.
10. 방식별 비용 구조 비교
| 방식 | 대표 장점 | 대표 비용·주의 |
|---|---|---|
| 여러 Scalar Subquery | SQL 작성이 직관적 | 같은 Source 반복 Access 가능 |
| 한 Scalar Aggregate | 한 Lookup에서 여러 Aggregate 계산 가능 | 여러 Column을 직접 반환하기 어려움 |
| 사전 집계 + Join | 상세 Source를 한 번 처리해 전체 처리량 절감 가능 | 전체 Group By·Workarea·TEMP 비용 |
| 대상 Key 제한 후 집계 | 불필요한 전체 집계 감소 | Query Transformation 후 실제 Plan 확인 필요 |
| LATERAL·OUTER APPLY | Outer Key별로 여러 Column·Top-N 반환 | Outer가 크면 반복 Starts 증가 |
| Object Type | 구조화된 단일 값 | Schema Type 의존성과 사용 복잡성 |
| 문자열 포장 | 구버전·Legacy에서 구현 가능 | 타입·NULL·길이·NLS 위험 |
선택의 핵심
Outer가 작은가, 큰가?
상관 Key의 NDV는 얼마인가?
Inner Access가 선택적인가?
같은 Source를 몇 번 읽는가?
전체 집계 입력은 얼마인가?
첫 행 응답과 전체 처리 중 무엇이 중요한가?
11. 실행계획 비교 방법
다중 Scalar Subquery
다음 항목을 확인합니다.
각 Subquery Row Source의 Starts
각 Subquery의 Buffers·A-Time
동일 Table Access가 몇 번 나타나는지
상관 Key의 NDV
사전 집계 Join
다음 항목을 확인합니다.
집계 전 입력 A-Rows
GROUP BY의 A-Rows
OMem·1Mem·Used-Mem·Used-Tmp
Join Build·Probe 입력 크기
최종 결과 행 수
LATERAL·APPLY
다음 항목을 확인합니다.
오른쪽 Row Source의 Starts
Starts당 평균 A-Rows·Buffers
Top-N STOPKEY 적용 여부
오른쪽 0·1·다중행
CROSS·OUTER의 Outer 행 보존 여부
실측 SQL은 다음처럼 작성할 수 있습니다.
SELECT /*+ GATHER_PLAN_STATISTICS */
...
FROM ...;
SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
NULL,
NULL,
'ALLSTATS LAST +PREDICATE +ALIAS +MEMSTATS +NOTE'
)
);
12. 결과 의미 검증 체크리스트
성능 개선 전후에 다음 질문에 모두 답해야 합니다.
- 최종 결과의 한 행은 무엇을 의미하는가?
- Outer Key당 결과가 최대 한 행인가?
- 일치 행이 없을 때 Outer 행을 보존하는가?
SUM·AVG·MIN·MAX의 NULL을 유지하는가?COUNT(*)의 0을 유지하는가?- 오른쪽 중복 때문에 Outer 행이 늘어나지 않는가?
- Top-1의 정렬 기준과 Tie-Breaker가 같은가?
- Predicate가
ON에서WHERE로 이동해 Outer Join 의미가 바뀌지 않았는가? - 집계 Group Key가 원래 Grain보다 세분화되지 않았는가?
- Object Constructor의 Attribute 순서·타입이 같은 의미인가?
- 문자열 포장 시 NLS·NULL·Delimiter 의미가 안전한가?
- 전체 Fetch 범위에서 실제 작업량이 줄었는가?
혼동하기 쉬운 판단
| 혼동하기 쉬운 판단 | 정확한 기준 |
|---|---|
| Aggregate를 세 개 쓰면 Table을 세 번 읽는다 | 같은 Query Block의 Aggregate들은 한 입력 처리에서 함께 계산할 수 있다 |
| 사전 집계는 항상 빠르다 | Outer가 작고 선택적 Index Lookup이 가능하면 반복 방식이 더 적을 수 있다 |
OUTER APPLY는 결과를 항상 한 행으로 만든다 | 오른쪽 Subquery가 여러 행이면 왼쪽 행도 여러 행으로 확장된다 |
Top-1에서 ORDER BY sal DESC만 있으면 충분하다 | 동점 처리 규칙을 위한 고유 Tie-Breaker가 필요하다 |
| Object Type은 다중 Column 조회의 기본 해법이다 | Schema Type이 실제로 필요한 경우에 제한적으로 사용한다 |
| 문자열 결합은 Column 수만 줄이는 안전한 최적화다 | 타입·NULL·NLS·길이 오류와 변환 비용이 생긴다 |
| CTE를 작성하면 반드시 먼저 계산된다 | Optimizer가 Merge·재배치할 수 있으므로 실제 Plan을 확인한다 |
| CROSS APPLY는 항상 Match 없는 왼쪽 Row를 제거한다 | GROUP BY 없는 Aggregate는 빈 입력에서도 한 Row를 만들 수 있다 |
| OUTER APPLY는 오른쪽을 항상 한 Row로 제한한다 | 오른쪽이 N행이면 왼쪽도 N행으로 확장된다 |
| LATERAL은 사전 집계보다 항상 빠르다 | Outer 수·NDV·Index 범위·전체 Fetch를 비교한다 |
| Object Constructor는 Argument 순서가 중요하지 않다 | Attribute 순서·개수·타입에 맞춰야 한다 |
| Object의 모든 Attribute NULL과 Object NULL은 같다 | Object 존재 여부와 Attribute NULL을 구분한다 |
핵심 판단 순서
같은 Source를 반복하는가?
→ 같은 Row Set의 Aggregate를 한 번에 계산할 수 있는가?
→ 결과 한 행의 Grain은 무엇인가?
→ Outer Key 수와 NDV는 얼마인가?
→ 전체 사전 집계와 Key별 Lookup 중 작업량이 작은 것은 무엇인가?
→ 대상 Outer Key를 먼저 제한할 수 있는가?
→ 여러 Column이 필요하면 LATERAL·APPLY가 적합한가?
→ 오른쪽은 0·1·다중행 중 무엇인가?
→ CROSS·OUTER의 왼쪽 행 보존이 원래 의미와 같은가?
→ Top-1 정렬·Tie-Breaker가 결정적인가?
→ Object·문자 포장보다 Typed Relational Column이 단순한가?
→ Grain·NULL·COUNT 0·중복·행 보존이 동일한가?
→ Starts·Buffers·Workarea·TEMP·첫 행·전체 Fetch로 실측했는가?
개념 확인 문제
문제를 누르면 바로 아래에서 정답과 해설을 확인할 수 있습니다.
01동일한 조건의 AVG, MIN, MAX 스칼라 서브쿼리를 각각 작성할 때 생길 수 있는 핵심 성능 문제는 무엇인가?
같은 상세 범위를 Outer 행마다 여러 번 읽을 수 있다는 점입니다. 각 Scalar Row Source의 Starts·Buffers·A-Time이 누적될 수 있습니다.
02같은 행 집합의 여러 Aggregate를 하나의 Query Block에서 계산하면 어떤 이점이 있는가?
같은 Row Set의 AVG·MIN·MAX·COUNT는 하나의 Aggregate Query Block에서 함께 계산할 수 있습니다. Aggregate 식 수가 Table Scan 횟수와 같지는 않습니다.
03사전 집계 Row Source가 고객당 한 행을 보장하려면 GROUP BY Grain을 어떻게 정해야 하는가?
사전 집계 결과의 Grain은 Join Key당 최대 한 행이어야 합니다. 고객 결과라면 GROUP BY는 고객 식별 Key를 중심으로 구성합니다.
04거래가 없는 고객에서 원래 COUNT() Scalar Subquery와 사전 집계 LEFT JOIN의 결과는 어떻게 달라질 수 있는가?
원래 COUNT Scalar는 Match 없음에서 0이지만 사전 집계 LEFT JOIN은 집계 Row 자체가 없어 NULL이 됩니다. 같은 의미라면 NVL(count,0)을 적용합니다.
05CROSS APPLY와 OUTER APPLY의 왼쪽 행 보존 규칙은 어떻게 다른가?
CROSS APPLY는 오른쪽이 실제 0행이면 왼쪽 Row를 제외하고 OUTER APPLY는 보존합니다. 오른쪽이 N행이면 두 방식 모두 왼쪽을 N행으로 확장할 수 있습니다.
06부서별 최고 급여 사원의 여러 Column을 가져올 때 OUTER APPLY가 다중 Scalar Subquery보다 유리할 수 있는 이유는 무엇인가?
GROUP BY 없는 Aggregate는 빈 입력에서도 Aggregate Row 한 건을 만듭니다. 따라서 CROSS APPLY 오른쪽도 AVG NULL·COUNT 0 한 행을 반환해 왼쪽이 유지될 수 있습니다.
07Top-1 조회에 고유한 Tie-Breaker가 필요한 이유는 무엇인가?
LATERAL·APPLY는 관련 Top-1 Row를 한 번 선택한 뒤 여러 Column을 함께 반환할 수 있습니다. Scalar Column별 반복 조회를 줄일 수 있습니다.
08Outer가 작고 Inner Index가 선택적일 때 사전 집계보다 LATERAL·APPLY가 유리할 수 있는 이유는 무엇인가?
Top-1은 동률을 해소하는 고유 Tie-Breaker가 필요합니다. 예를 들어 ORDER BY sal DESC, empno ASC로 한 행을 결정합니다.
09Object Type 반환 방식이 단순 조회의 기본 대안이 되기 어려운 이유는 무엇인가?
Object Type Constructor는 Attribute 개수·정의 순서·호환 타입에 맞춘 Argument가 필요합니다. Schema 의존성·Attribute 접근·Driver 지원도 검토합니다.
10다중 Scalar Subquery와 사전 집계·APPLY를 비교할 때 확인할 핵심 실행 통계는 무엇인가?
다중 Scalar의 Starts·Buffers와 사전 집계의 입력 A-Rows·Workarea·TEMP, APPLY의 Starts·오른쪽 행 수를 비교합니다. 결과 Grain·NULL·COUNT 0·중복·Outer 보존도 함께 검증합니다.