Index Skip Scan: 선두 컬럼 없이 논리 Subindex 탐색
복합 인덱스의 선두 컬럼 조건이 없을 때 선두 NDV별 논리 Subindex를 반복 Probe하는 Skip Scan의 적용 조건과 한계를 이해합니다.
핵심 요약
INDEX SKIP SCAN은 복합 B-tree Index의 선두 컬럼 또는 선두 Prefix가 Query Predicate에 없을 때, Oracle이 Index를 여러 논리 Subindex로 나누어 후행 Key를 반복 탐색하는 Access Path입니다.
CREATE INDEX customer_gender_email_ix
ON customer(gender, email);
SELECT customer_id
FROM customer
WHERE email = :email;
gender 값이 F, M처럼 적다면 개념적으로 다음 두 Subindex를 탐색합니다.
Subindex F
(gender='F', email=:email)
Subindex M
(gender='M', email=:email)
TABLE ACCESS BY INDEX ROWID BATCHED CUSTOMER
INDEX SKIP SCAN CUSTOMER_GENDER_EMAIL_IX
Oracle 공식 기준에서 Skip Scan이 유리해질 가능성이 큰 조건은 다음과 같습니다.
선두 컬럼 조건 없음
+ 선두 Key의 Distinct Value가 상대적으로 적음
+ 비선두 Key의 Distinct Value가 많고 Predicate가 선택적
+ 각 Subindex의 Leaf Range와 Table Access가 작음
Skip Scan의 비용은 단순히 선두 NDV 하나로 결정되지 않습니다.
Skip Scan 총비용
≈ 논리 Subindex 수직 탐색
+ 각 Subindex Leaf Scan
+ Index Filter
+ 후보 ROWID
+ Table Block Access
+ 상위 Iterator·Nested Loops의 반복 Starts
이 이론의 범위
이 이론은 SQLP의
SQL 고급활용 및 튜닝 → 인덱스 튜닝 → 인덱스 스캔 방식범위에서 Index Skip Scan의 구조·통계·대안 비교를 다룹니다.
학습 목표
이 이론을 학습한 뒤에는 다음을 설명할 수 있어야 합니다.
- Skip Scan과 일반 Range Scan의 차이를 설명한다.
- 논리 Subindex와 선두 NDV의 관계를 설명한다.
- 후행 Equality·Range Predicate가 Skip Scan 비용에 미치는 영향을 설명한다.
- 여러 선두 컬럼이 생략됐을 때 실제 Prefix 조합 수를 판단한다.
- 선두값 편중·컬럼 상관관계·Column Group Statistics의 역할을 설명한다.
- Skip Scan의
access와filter가 동시에 나타날 수 있는 이유를 설명한다. Starts와 내부 논리 Subindex Probe 수를 구분한다.- Covering·Clustering Factor·Table Access 비용을 포함해 판단한다.
- Skip Scan·전용 Index·Full Scan 계열을 비교한다.
INDEX_SS·INDEX_SS_ASC·INDEX_SS_DESCHint를 실험에 사용한다.
1. 선두 컬럼이 없으면 왜 일반 Range Scan이 어려운가
Index를 다음처럼 정의합니다.
CREATE INDEX customer_gender_email_ix
ON customer(gender, email);
B-tree Key는 사전식으로 정렬됩니다.
F, a@example.com
F, b@example.com
F, z@example.com
M, a@example.com
M, b@example.com
M, z@example.com
선두 Key가 있는 조건입니다.
WHERE gender = 'F'
AND email = :email
('F',:email) Prefix를 직접 탐색
→ 일반 INDEX RANGE SCAN
선두 Key가 없는 조건입니다.
WHERE email = :email
같은 Email Key가 F Group과 M Group에 각각 존재할 수 있어 Index 전체에서 하나의 연속 구간으로 모이지 않습니다.
F Group의 email 위치
M Group의 email 위치
따라서 하나의 일반 Range Scan 경계만으로 모든 후보를 찾기 어렵습니다.
2. 논리 Subindex의 원리
Oracle은 Index를 물리적으로 분할하지 않고 선두 Key의 각 값별로 작은 Index가 있는 것처럼 해석합니다.
Physical Index
(gender,email)
Logical Subindex F
F,a...
F,b...
F,z...
Logical Subindex M
M,a...
M,b...
M,z...
개념적으로 다음 SQL의 합집합과 비슷합니다.
SELECT *
FROM customer
WHERE gender='F'
AND email=:email
UNION ALL
SELECT *
FROM customer
WHERE gender='M'
AND email=:email;
각 Subindex에서는 후행 email이 정렬돼 있으므로 Equality 또는 Range 탐색이 가능합니다.
F Prefix 수직 탐색 → email Range
M Prefix 수직 탐색 → email Range
3. Oracle이 Skip Scan을 고려하는 대표 조건
Oracle 공식 Access Path 설명은 다음 두 조건을 핵심으로 제시합니다.
- 복합 Index의 선두 컬럼이 Query Predicate에 없음
- 선두 Key의 Distinct Value는 비교적 적고 비선두 Key에는 많은 Distinct Value가 있음
예시입니다.
gender NDV = 2
email NDV = 10,000,000
WHERE email = :email
Email이 선택적이면 각 성별 Prefix에서 짧은 Range만 확인하면 됩니다.
반대 예입니다.
customer_id NDV = 5,000,000
signup_date Predicate = 최근 2년
많은 논리 Prefix
× 각 Prefix의 넓은 날짜 Range
→ Skip Scan 비용 증가
NDV는 후보 판단의 시작이지 최종 결론이 아닙니다.
4. 후행 Predicate의 형태
4.1 Equality Predicate
WHERE email = :email
Email이 거의 고유하다면 각 Subindex에서 0~소수 Entry만 반환할 수 있습니다.
낮은 선두 NDV
× 선택적인 후행 Equality
→ Skip Scan에 유리할 가능성
4.2 Range Predicate
WHERE signup_date >= :from_date
AND signup_date < :to_date
각 Subindex마다 날짜 Range를 반복 탐색합니다.
Prefix F 날짜 Range
Prefix M 날짜 Range
...
날짜 범위가 넓으면 선두 NDV가 낮아도 많은 Leaf Block을 읽을 수 있습니다.
4.3 Prefix LIKE
WHERE email LIKE 'kim%'
고정 Prefix가 있는 LIKE는 각 논리 Subindex에서 Range를 만들 수 있습니다.
F Group의 kim Prefix
M Group의 kim Prefix
Leading Wildcard LIKE '%kim'은 각 Subindex에서도 좁은 Start Key를 만들기 어려워 Skip Scan 이점이 제한됩니다.
5. 여러 선두 컬럼이 생략된 경우
다음 Index를 가정합니다.
CREATE INDEX sales_x1
ON sales(channel_code, region_code, customer_id, sale_date);
다음 SQL은 앞의 두 Key를 지정하지 않습니다.
WHERE customer_id = :customer_id
AND sale_date >= :from_date
개념적인 논리 Prefix입니다.
(ONLINE,SEOUL)
(ONLINE,BUSAN)
(STORE,SEOUL)
(STORE,BUSAN)
...
비용은 각 컬럼 NDV를 단순히 곱한 값과 항상 같지 않습니다.
단순 독립 가정
channel NDV × region NDV
실제 판단
존재하는 (channel,region) 조합 수
+ 각 조합의 Row 분포
+ customer_id·sale_date와의 상관관계
예를 들어 ONLINE은 서울만, STORE는 부산만 존재한다면 실제 조합 수는 단순 곱보다 작습니다.
6. Column Group Statistics와 상관관계
개별 Column Statistics만 있으면 Optimizer는 여러 선두 컬럼의 관계를 독립적으로 추정할 수 있습니다.
channel_code와 region_code가 강하게 상관됨
→ 단일 Column NDV만으로 조합 수를 잘못 추정 가능
Column Group Statistics는 여러 컬럼을 하나의 단위로 취급해 결합 Cardinality 추정을 개선할 수 있습니다.
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(
ownname => USER,
tabname => 'SALES',
method_opt => 'FOR ALL COLUMNS SIZE AUTO ' ||
'FOR COLUMNS SIZE AUTO (channel_code,region_code)',
cascade => TRUE
);
END;
/
Column Group Statistics는 실제 Skip Scan 내부 Probe 수를 직접 표시하지는 않지만, 관련 Predicate·Prefix의 Cardinality와 Cost 추정에 도움을 줄 수 있습니다.
7. 데이터 편중과 Bind별 차이
선두 NDV가 2여도 분포는 다를 수 있습니다.
| gender | 비율 |
|---|---|
| F | 99.9% |
| M | 0.1% |
후행 Email Equality가 선택적이면 두 Prefix 모두 짧은 Scan으로 끝날 수 있습니다.
후행 날짜 Range가 넓으면 다음처럼 비용이 편중됩니다.
F Prefix
→ Index 대부분을 Scan
M Prefix
→ 매우 작은 Range
다음을 함께 확인합니다.
- 선두 NDV
- Prefix별 Row 수
- 후행 값의 Histogram·편중
- 선두·후행 컬럼의 상관관계
- 대표 Bind·극단 Bind의 실제 A-Rows·Buffers
평균 Selectivity 하나로 모든 Bind 비용을 일반화하지 않습니다.
8. NULL 선두 Key 주의
일반 B-tree는 모든 Index Key Column이 NULL인 Row만 저장하지 않습니다.
(gender,email)
gender=NULL
email='a@example.com'
→ 전체 Key가 NULL이 아니므로 Entry 존재 가능
따라서 선두 컬럼이 Nullable이면 논리적으로 NULL Prefix Group도 존재할 수 있습니다.
gender=NULL Subindex
gender='F' Subindex
gender='M' Subindex
실제 Data·Statistics·Predicate를 기준으로 판단하고 F·M 두 번만 Probe한다처럼 고정하지 않습니다.
9. 실행계획의 Access·Filter
가능한 Plan입니다.
TABLE ACCESS BY INDEX ROWID BATCHED CUSTOMER
INDEX SKIP SCAN CUSTOMER_GENDER_EMAIL_IX
Predicate Information 예시입니다.
access(email=:email)
filter(email=:email)
같은 후행 Predicate가 access와 filter 양쪽에 나타날 수 있습니다.
access
→ 각 논리 Subindex의 탐색 경계에 사용
filter
→ 읽은 Entry가 실제 조건을 만족하는지 추가 확인
Operation 이름이나 Predicate Text만으로 Leaf Scan량을 확정하지 않습니다.
Index A-Rows 10
Index Buffers 5,000
→ 내부적으로 넓은 Scan·Filter 가능성
10. Starts와 내부 Probe 수를 구분한다
V$SQL_PLAN_STATISTICS_ALL.LAST_STARTS는 마지막 실행에서 해당 Row Source가 시작된 횟수입니다.
INDEX SKIP SCAN Starts = 1
이는 상위 Operation이 Skip Scan Row Source를 한 번 시작했다는 의미입니다.
Starts = 1
≠ 논리 Subindex가 1개
Skip Scan 내부에서 F·M·NULL 등 여러 Subindex를 탐색해도 하나의 Row Source 내부 작업으로 처리될 수 있습니다.
반대로 다음처럼 Starts가 클 수 있습니다.
NESTED LOOPS
INDEX SKIP SCAN
Starts = 50,000
상위 Driving Row마다 Skip Scan 전체가 반복된 것입니다.
총비용
= Starts
× Skip Scan 1회 내부 논리 Subindex 작업
11. Covering·Table Access·Clustering
11.1 Covering
CREATE INDEX customer_gender_email_ix
ON customer(gender, email, customer_id);
SELECT customer_id
FROM customer
WHERE email=:email;
필요한 Column이 Index에 모두 있으면 Table Access를 생략할 수 있습니다.
INDEX SKIP SCAN
→ Index-only 결과 가능
11.2 Table Access
SELECT customer_name
FROM customer
WHERE email=:email;
customer_name이 Index에 없으면 ROWID Table Access가 필요합니다.
11.3 Clustering Factor
Skip Scan이 많은 ROWID를 반환할 때 Table Row가 넓은 Block에 분산돼 있으면 Table Buffers가 커질 수 있습니다.
같은 후보 10,000 ROWID
Clustering 양호
→ 500 Table Blocks
Clustering 불량
→ 9,000 Table Blocks
후행 Predicate 선택도만이 아니라 Table Access 비용을 포함합니다.
12. 대안 Access Path 비교
대안 A: INDEX SKIP SCAN
장점:
- 기존 복합 Index 재사용
- 신규 Index DML·공간 비용 없음
- 낮은 선두 NDV와 선택적인 후행 조건에서 유리 가능
단점:
- 논리 Subindex 반복
- Cardinality·Statistics 변화에 따라 Plan 변동 가능
- 핵심 고빈도 SQL에서는 누적 비용이 큼
대안 B: 후행 컬럼 전용 Index
CREATE INDEX customer_email_ix
ON customer(email);
장점:
- Email Key로 직접 한 번 Range Scan
- Plan 단순·안정 가능
단점:
- Segment·Cache·DML·Redo·Undo 증가
- 기존 Index와 중복 가능
대안 C: 다른 복합 Index
CREATE INDEX customer_email_gender_ix
ON customer(email, gender);
Email 중심 SQL을 직접 지원하지만 Gender 중심 SQL 공용성과 DML을 함께 평가합니다.
대안 D: INDEX FULL·FAST FULL SCAN
Skip Scan의 논리 Prefix 수와 각 Range가 커지면 Index 전체 Scan이 더 저렴할 수 있습니다.
대안 E: TABLE ACCESS FULL
Table이 작거나 결과 비율이 크고 ROWID Table Access가 많으면 Full Scan이 더 유리할 수 있습니다.
13. Skip Scan과 Index Join·Bitmap Conversion 차이
| 구분 | Skip Scan | Index Join | Bitmap Conversion |
|---|---|---|---|
| Index 수 | 하나의 복합 Index | 같은 Table의 여러 Index | 여러 B-tree·Bitmap Source |
| 원리 | 생략된 선두값별 논리 Subindex | ROWID 기준 Hash Join | ROWID 집합을 Bitmap으로 결합 |
| Table Access | Covering 여부에 따라 | 모든 Column이 Index에 있으면 생략 | 최종 ROWID로 Table Access 가능 |
| 핵심 비용 | 반복 수직 탐색·Leaf Scan | 여러 Index Scan·Hash Join | 여러 Scan·변환·Bitmap 연산 |
INDEX SKIP SCAN은 하나의 Index 내부 Access Path입니다.
14. Hint로 후보 비교
Oracle은 다음 Hint를 제공합니다.
/*+ INDEX_SS(c customer_gender_email_ix) */
/*+ INDEX_SS_ASC(c customer_gender_email_ix) */
/*+ INDEX_SS_DESC(c customer_gender_email_ix) */
INDEX_SS: Skip Scan 유도INDEX_SS_ASC: Ascending 방향 Skip Scan 유도INDEX_SS_DESC: Descending 방향 Skip Scan 유도
Hint는 테스트 환경에서 후보 Access Path의 실제 비용을 비교하는 수단으로 사용합니다.
Hint 사용
→ 반드시 적용됐다고 가정 금지
→ 실제 Plan·Hint Report·Outline 확인
운영 SQL에 고정하기 전에 Statistics·Index 설계·Bind 편중을 먼저 해결합니다.
15. 실제 검증 절차
SELECT /*+ GATHER_PLAN_STATISTICS */
customer_id
FROM customer
WHERE email=:email;
SELECT *
FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
:sql_id,
:child_no,
'ALLSTATS LAST +PREDICATE +ALIAS +NOTE'
)
);
확인 순서입니다.
1. Index Key 순서와 생략된 선두 Prefix를 표시한다.
2. 선두 NDV·NULL·실제 조합 수를 확인한다.
3. 후행 Predicate의 형태와 선택도를 확인한다.
4. INDEX SKIP SCAN·access·filter를 확인한다.
5. LAST_STARTS·LAST_OUTPUT_ROWS를 확인한다.
6. LAST_CR_BUFFER_GETS·LAST_DISK_READS를 확인한다.
7. Table A-Rows·Buffers·Clustering 비용을 확인한다.
8. 전용 Index·Full·Fast Full·Table Full 후보를 비교한다.
9. 동일 Bind Type·Fetch·동시성에서 반복 측정한다.
10. 신규 Index DML·Redo·공간과 다른 SQL 회귀를 포함한다.
16. 자주 혼동하는 판단
| 혼동 | 정확한 기준 |
|---|---|
| 선두 조건이 없으면 복합 Index를 사용할 수 없다 | Cost가 유리하면 Skip Scan을 사용할 수 있다 |
| 선두 NDV가 낮으면 Skip Scan은 항상 빠르다 | 후행 Range·Table Access·빈도까지 본다 |
| Skip Scan은 Index를 물리적으로 분할한다 | 실행 중 논리 Subindex로 해석한다 |
| Starts는 논리 Subindex 수다 | Row Source 시작 횟수이며 내부 Probe와 다르다 |
| 선두 NDV 2면 정확히 두 번만 탐색한다 | NULL Prefix·분포·내부 구현과 전체 Plan을 확인한다 |
| A-Rows가 적으면 Leaf Scan도 작다 | Buffers·Reads와 Filter를 확인한다 |
| Skip Scan이 선택되면 전용 Index가 불필요하다 | 고빈도 핵심 SQL은 전용 Index가 유리할 수 있다 |
| 후행 Equality면 Table Access도 항상 적다 | 후보 ROWID·Clustering·Covering을 확인한다 |
| INDEX_SS Hint면 운영 정답이다 | 후보 비교용이며 통계·회귀를 검증한다 |
| 여러 선두 컬럼 NDV는 단순 곱이다 | 실제 조합·상관관계·Column Group을 본다 |
개념 확인 문제
문제를 누르면 바로 아래에서 정답과 해설을 확인할 수 있습니다.
01Skip Scan이 필요한 대표 Index·Predicate 구조를 설명하시오.
대표 구조
- 복합 B-tree의 선두 컬럼 또는 선두 Prefix가 Predicate에 없습니다.
- 후행 컬럼에는 Equality·Range처럼 탐색 가능한 조건이 있습니다.
- 예:
(gender,email)Index와WHERE email=:email.
02(gender,email) Index에서 email=:email을 찾는 논리 Subindex 구조를 설명하시오.
논리 Subindex
- Gender별 Key Group을
F와M의 작은 Index처럼 해석합니다. - 각 Prefix 안에서 email Key를 별도로 탐색합니다.
- 물리 Index는 하나이며 실행 중 논리적으로 분할합니다.
03Oracle이 Skip Scan을 고려하는 두 가지 핵심 통계 조건을 설명하시오.
공식 핵심 조건
- 복합 Index의 Leading Column이 Predicate에 없습니다.
- Leading Key의 Distinct Value는 상대적으로 적고 Nonleading Key에는 많은 Distinct Value가 있어 후행 조건이 선택적입니다.
04Equality·Range·Prefix LIKE 후행 Predicate의 Skip Scan 비용 차이를 설명하시오.
후행 Predicate
- Equality는 각 Subindex에서 짧은 범위를 만들 가능성이 큽니다.
- Range는 각 Prefix마다 넓은 Leaf 구간을 읽을 수 있습니다.
- Prefix LIKE는 고정 문자열로 Range를 만들 수 있지만 Leading Wildcard는 좁은 Start Key를 만들기 어렵습니다.
05여러 선두 컬럼 생략 시 실제 Prefix 조합 수를 단순 NDV 곱으로 판단하면 안 되는 이유를 설명하시오.
여러 선두 컬럼
- 컬럼이 독립적이지 않으면 실제 조합 수가 NDV 곱과 다릅니다.
- 존재하지 않는 조합이 많거나 특정 조합에 Row가 편중될 수 있습니다.
- 실제 Column Group NDV와 분포를 확인합니다.
06Column Group Statistics가 Skip Scan 후보 Cost 추정에 도움을 주는 이유를 설명하시오.
Column Group Statistics
- 여러 선두 컬럼의 상관관계를 단일 Column Statistics가 표현하지 못할 수 있습니다.
- Column Group Statistics는 결합 Cardinality와 Prefix 조합 추정을 개선해 Access Path Cost 판단에 도움을 줍니다.
- 내부 Probe 수를 직접 보여 주는 통계는 아닙니다.
07Nullable 선두 Key가 논리 Subindex 판단에 미치는 영향을 설명하시오.
Nullable 선두 Key
- 복합 Index는 모든 Key가 NULL인 Row만 저장하지 않습니다.
- 선두 Key가 NULL이고 후행 Key가 Non-NULL이면 Entry가 존재해 NULL Prefix Group이 생길 수 있습니다.
- F·M 두 값만 있다고 고정하지 않고 Data와 Statistics를 확인합니다.
08Starts·A-Rows·Buffers를 이용해 Skip Scan 내부 비용을 해석하는 방법을 설명하시오.
Runtime 해석
- Starts는 Row Source 시작 횟수이며 내부 논리 Subindex 수가 아닙니다.
- A-Rows는 상위로 반환한 Entry·ROWID 수입니다.
- Buffers·Reads는 내부 수직 탐색과 Leaf Scan 작업량을 반영합니다.
- Table A-Rows·Buffers를 함께 봐 후보 ROWID와 Clustering 비용을 확인합니다.
09Skip Scan과 전용 Index·Index Join·Full Scan의 차이를 설명하시오.
대안 비교
- Skip Scan은 하나의 복합 Index 내부를 Prefix별로 탐색합니다.
- 전용 Index는 후행 Key를 선두로 직접 한 번 탐색하지만 DML·공간이 증가합니다.
- Index Join은 여러 Index의 ROWID를 결합합니다.
- Full·Fast Full·Table Full Scan은 반복 Prefix 비용이 큰 경우 대안입니다.
10Skip Scan 후보를 안전하게 검증하고 적용하는 절차를 설명하시오.
검증 절차 - Index Key와 생략된 Prefix, NDV·편중·Column Group을 확인합니다. - 후행 Predicate 선택도와 Covering·Table Access를 분석합니다. - ALLSTATS LAST로 Starts·A-Rows·Buffers·Reads를 수집합니다. - 동일 Bind·Fetch에서 전용 Index와 Full Scan 계열을 비교합니다. - 신규 Index의 DML·Redo·공간과 다른 SQL 회귀를 포함해 적용합니다.