데이터베이스 연구실.log

스캔 범위와 필터 조건

결합 키의 스캔 범위, 필터, INLIST와 Text 검색 조건, NUM_INDEX_KEYS 지원 구문과 일부 측정 단위는 확인 불가

NUM_INDEX_KEYSINLIST ITERATORINDEX SKIP SCAN인덱스 스캔 효율액세스 조건필터 조건BETWEENIN-List선두 컬럼Oracle Text

#핵심 요약

결합 인덱스는 선두 컬럼부터 연속된 등치(=) 조건이 있을 때 스캔 범위를 가장 잘 줄임
실습 요약: "선두 컬럼이 =이면 모이고, BETWEEN이면 흩어진다"

범위 조건이 맨 뒤에 있으면 조건을 만족하는 레코드가 한 구간에 모여 불필요한 스캔이 거의 없음
선두 컬럼이 범위 조건이면 뒤 컬럼이 =라도 사이사이의 불필요한 레코드를 함께 읽음

중간 컬럼 조건이 빠지면 그 뒤 컬럼 조건은 스캔 범위를 줄이지 못하고 필터로만 쓰임

선두 컬럼의 BETWEEN을 IN-List로 바꾸면 "넓게 한 번 읽기"가 "짧게 여러 번 찾기"가 됨
값이 너무 많으면 수직 탐색이 반복되어 오히려 불리함

결과 건수(Rows)에 비해 읽은 블록(cr)이 지나치게 많으면 인덱스를 탔어도 넓게 읽고 대부분 버리는 비효율임

#개념과 동작 원리

실습 예(인덱스 c1 + c2, 값 A1A3, B1B4, C1~C3, D1):

조건 읽는 구간
c1 = 'B' AND c2 = 3 B3로 바로 감
c1 BETWEEN 'A' AND 'C' AND c2 BETWEEN 2 AND 3 A2부터 C3까지 읽고, 그 사이 B1, B4, C1은 읽었지만 버림

실습의 BETWEEN 비효율 설명: BETWEEN '1' AND '3'에서 시작값 1과 끝값 3 구간은 조건을 만족하는 지점부터 읽거나 벗어나기 직전에 멈출 수 있지만, 중간값 2 구간은 거의 전체를 읽어야 함

실행계획의 Predicate Information에서 스캔 범위를 정하는 조건은 access, 읽은 뒤 거르는 조건은 filter로 표시됨(실행계획 읽기)
퀴즈 해설은 범위 조건 뒤의 컬럼 조건이 "대개 filter가 된다"고 적었음

#주요 용어와 구성 요소

구문 의미
NDV 컬럼의 서로 다른 값 개수로 선택도 추정의 입력임
INLIST ITERATOR IN 목록에 대한 반복 탐색을 나타내는 연산임
유리한지 여부는 반복 횟수와 각 탐색 범위에 달림
INDEX SKIP SCAN 결합 인덱스의 선두 키 값별 하위 인덱스를 가정해 탐색하는 경로임
중간 또는 선두 조건이 없어도 Skip Scan을 선택할 수 있음
CONTEXT 문서에서 분석한 토큰을 검색하는 도메인 인덱스임

#필수 명령어 또는 SQL

항목 실행 조건
작업 대상 결합 키 스캔 범위, 테이블 필터, Text 검색의 결과와 반영 상태
필요 권한 테이블 소유자의 생성, 통계 수집 권한과 공간 할당 필요
Text 생성 권한과 선택한 동기화 방식의 권한을 별도 확인함
바꿀 입력값 실습 테이블, 컬럼, 인덱스 이름, IN 목록의 실제 업무 값, Text lexer, stoplist, SYNC 설정
결과 해석 Predicate Information을 연산별로 읽고 Starts, A-Rows, Buffers를 비교함
힌트 적용 여부는 실행계획으로 확인함
추가 실행 출력은 기재 없음

#실습 1: 등치 조건과 범위 조건

SQL*Plus (실습 계정)객체 또는 설정을 변경함
CREATE TABLE idx_demo (c1 VARCHAR2(1), c2 NUMBER, txt VARCHAR2(10));
-- A1~A3, B1~B4, C1~C3, D1 11행 입력 (INSERT 11개 생략)
CREATE INDEX idx_demo_c1_c2 ON idx_demo (c1, c2);
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'IDX_DEMO', CASCADE => TRUE);
SQL*Plus (실습 계정)조회만 함
SELECT * FROM idx_demo WHERE c1 = 'B' AND c2 = 3;
SELECT * FROM idx_demo WHERE c1 BETWEEN 'A' AND 'C' AND c2 BETWEEN 2 AND 3;

결과 해석:

실습 결과: 두 SQL 모두 인덱스 Buffers 2였고 테이블 포함 합계는 3과 4였음
데이터가 11건뿐이라 블록 수 차이로는 드러나지 않음

#실습 2: BETWEEN을 IN-List로

SQL*Plus (실습 계정)객체 또는 설정을 변경함
CREATE TABLE mcustsum AS
SELECT ROWNUM custno,
       '2025' || LPAD(CEIL(ROWNUM / 100000), 2, '0') salemm,
       DECODE(MOD(ROWNUM, 12), 1, 'A', 'B') salegb,
       ROUND(DBMS_RANDOM.VALUE(1000, 100000), -2) saleamt
  FROM dual
CONNECT BY LEVEL <= 1200000;

CREATE INDEX mcustsum_idx01 ON mcustsum (salemm, salegb);
SQL*Plus (실습 계정)조회만 함
SELECT COUNT(*) FROM mcustsum t
 WHERE salegb = 'A' AND salemm BETWEEN '202501' AND '202512';
시도 실습 실행계획 Buffers
인덱스 없음 TABLE ACCESS FULL 3835
mcustsum_idx01 (salemm, salegb) INDEX FAST FULL SCAN 3367
mcustsum_idx02 (salegb, salemm) + index 힌트 INDEX RANGE SCAN 281
idx01 + salemm IN ('202501',..., '202512') INLIST ITERATOR 아래 INDEX RANGE SCAN(Starts 12) 304
idx01 + index_ss 힌트(BETWEEN 그대로) INDEX SKIP SCAN 300

결과 해석: 인덱스를 바꿀 수 있으면 등치 조건 컬럼 salegb를 앞에 두는 것이 가장 좋고, 인덱스를 바꿀 수 없으면 선두 컬럼의 BETWEEN을 IN-List로 바꿔 12번의 짧은 범위 스캔으로 만듦
실습에서는 IN-List 값을 LISTAGG(DISTINCT...)로 만들어 붙였음

#실습 3: 중간 컬럼이 빠진 결합 인덱스

SQL*Plus (실습 계정)객체 또는 설정을 변경함
CREATE INDEX idx_term_multi ON term_multi (c1, c2, c3, c4);
SQL*Plus (실습 계정)조회만 함
SELECT * FROM term_multi WHERE c1 = '성' AND c2 = '능' AND c4 = '선';

결과 해석:

실습 답은 빠진 c3를 건너뛴 인덱스 (c1, c2, c4)를 새로 만드는 것이었음
16건 실습이라 실습 기록상 두 인덱스 모두 Buffers 4로 차이가 보이지 않음

#실습 4: 중간 글자 검색과 Oracle Text

term LIKE '성능_선'은 앞 두 글자로 범위를 잡은 뒤 나머지를 거름
실습에서는 이를 Oracle Text 인덱스로 바꾸는 답을 적었음

SQL*Plus (실습 계정, Oracle Text 사용 가능)객체 또는 설정을 변경함
CREATE INDEX idx_term_text ON term_dict (term) INDEXTYPE IS ctxsys.context;
SQL*Plus (실습 계정)조회만 함
SELECT term FROM term_dict WHERE CONTAINS(term, '성능_선') > 0;

결과 해석:

실습 결과: DOMAIN INDEX로 바뀌었지만 Buffers는 2에서 10으로 오히려 늘었음(16건 실습)
실습 환경의 Oracle Text 설치 여부와 동기화 설정은 기재 없음
인덱스 생성 권한과 동기화 방식은 아래 조건으로 구분함

#IN 조건은 언제 액세스 조건이 되는가

실행계획의 카디널리티는 각 연산이 내보내는 행 수임
컬럼의 서로 다른 값 개수인 NDV와 구분함
값이 다양한 컬럼의 = 조건은 한 번에 좁은 범위로 줄여 주므로 보통 인덱스 앞에 둠
DEPTNO, JOB처럼 값 종류가 적은 컬럼도 IN 값 개수가 적으면 INLIST ITERATOR로 인덱스를 여러 번 짧게 찾아 읽는 블록을 줄일 수 있음

실습 기록(emp_big 14만 행, deptno = 20 AND job IN ('CLERK','SALESMAN','ANALYST')): 인덱스 (deptno, job)과 (job, deptno) 모두 INLIST ITERATOR 아래 INDEX RANGE SCAN Starts 3, Buffers 84였고, 두 인덱스를 함께 두면 옵티마이저는 (deptno, job)을 골랐음

인덱스 선두 컬럼에 IN이 오면 그 값마다 수직 탐색을 반복하는 INLIST ITERATOR만이 액세스 조건으로 쓰는 방법이고, 보통 그것이 효율적임
후행 컬럼의 IN은 다름: 선행 컬럼 조건만으로 이미 리프 블록이 충분히 좁혀지면, 후행 IN을 액세스 조건으로 만들어 여러 번 수직 탐색하는 것보다 필터로 한 번 훑는 편이 블록을 덜 읽음

SQL*Plus (실습 계정)조회만 함
-- 인덱스: 고객별가입상품_상품X (상품ID, 고객번호), 고객별가입상품_고객X (고객번호, 상품ID)
SELECT /*+ index(a 고객별가입상품_상품X) */ *
  FROM 고객별가입상품 a
 WHERE 고객번호 = 1234
   AND 상품ID IN ('NH00037', 'NH00041', 'NH00050');      -- 두 컬럼 모두 액세스 조건

SELECT /*+ num_index_keys(a 고객별가입상품_상품X 1) */ *
  FROM 고객별가입상품 a
 WHERE 고객번호 = 1234
   AND 상품ID IN ('NH00037', 'NH00041', 'NH00050');      -- 첫 컬럼만 액세스, 둘째는 필터

SELECT *
  FROM 고객별가입상품 a
 WHERE 고객번호 = 1234
   AND 상품ID || '' IN ('NH00037', 'NH00041', 'NH00050'); -- 컬럼 가공으로 같은 효과

결과 해석:

실습(12만 건) 실행계획 Buffers
full(a) 힌트 TABLE ACCESS FULL 501
상품X 인덱스, 두 컬럼 액세스 INLIST ITERATOR 아래 INDEX RANGE SCAN 8
상품X 인덱스, num_index_keys(... 1) INDEX RANGE SCAN 한 번 5
고객X 인덱스, num_index_keys(... 2)(고객 5000, 상품 3개) 상품 수만큼 수직 탐색 8
고객X 인덱스, num_index_keys(... 1) 고객번호로 한 번 찾고 상품ID는 필터 5

NUM_INDEX_KEYS(테이블 인덱스 n)은 인덱스 앞 n개 컬럼까지만 액세스 조건으로 쓰게 하는 힌트임

선행 컬럼이 유일하거나 매우 좁게 걸러 주면 후행 IN은 필터로 둬도 됨
선행 컬럼이 많은 행을 남기면 후행 IN을 INLIST ITERATOR로 액세스 조건화하는 것이 도움이 됨

"IN 조건은 항상 액세스 조건이 유리하다"는 일반화는 인덱스 컬럼 순서와 데이터 분포에 따라 틀릴 수 있음

#BETWEEN과 LIKE의 스캔 범위

SQL*Plus (실습 계정)객체 또는 설정을 변경함
CREATE INDEX emp_big_idx1 ON emp_big (hire_ym, grade);   -- hire_ym '201812'~'201912', grade A 10% / B 90%

SELECT COUNT(*) FROM emp_big
 WHERE hire_ym LIKE '2019%' AND grade = 'B';

SELECT /*+ index_ss(emp_big emp_big_idx1) */ COUNT(*) FROM emp_big
 WHERE hire_ym BETWEEN '201901' AND '201912' AND grade = 'B';

결과 해석:

실습 기록: 튜닝 전 1826, 튜닝 뒤 1522(주석에 적힌 값으로, 무엇의 값인지는 기재 없음)
LIKE '2019%'는 '2019'로 시작하는 모든 값을 포함하는 범위라 BETWEEN '201901' AND '201912'보다 넓게 읽을 수 있음

#범위 조건을 남용할 때의 비효율

인덱스가 (회사코드, 지역코드, 상품코드) 순일 때, 항상 들어오는 지역코드를 선택 조건처럼 LIKE :reg || '%'로 쓰면, 값이 정확히 '05'로 바인딩되어도 LIKE는 범위 조건이라 그 뒤의 상품코드 = 조건이 액세스 조건이 되지 못하고 필터로 처리됨
실습은 (seller_code, region_code, product_name) 인덱스를 둔 50만 건 coupang_product 테이블로 같은 상황을 만들었음
선택 조건이 아닌 값은 =로 쓰고, 정말 선택적인 조건이면 경우마다 SQL을 나누는 방법을 검토함

연습 문제: 인덱스 emp_big(deptno, job)에서 deptno BETWEEN 10 AND 30 AND job = 'CLERK'가 INDEX FAST FULL SCAN(실습 Buffers 437)으로 처리될 때, 인덱스를 바꾸지 않고 SQL만 고쳐 읽는 블록을 줄여 봄(예: deptno IN (10, 20, 30))

#구문과 옵션 설명

CONTEXT 인덱스 생성에는 해당 텍스트 컬럼에 인덱스를 생성할 수 있는 권한이 필요하며 CTXAPP 역할 자체는 생성의 필수 조건이 아님
기본 SYNC (MANUAL)은 DML 뒤 수동 동기화가 필요한 방식임
SYNC (ON COMMIT)과 SYNC (EVERY ...)는 반영 시점이 다르며 주기 방식은 CREATE JOB 권한도 확인함

CONTAINS의 _는 검색어의 한 문자 자리를 확장하지만 문서의 토큰화, 불용어 설정이 개입함
전체 문자열을 비교하는 LIKE와 결과가 다를 수 있음

#사용 조건과 주의사항

NUM_INDEX_KEYS의 지원 구문은 확인 불가이므로 기존 실습의 힌트와 관찰을 지원되는 일반 설정으로 확대하지 않음
SQL 변경 전후에는 결과 집합의 동등성과 실제 계획, 행 수, Buffers를 함께 비교함
기존 1826, 1522는 측정 항목이 확인 불가이므로 실행 시간이나 I/O 단위를 새로 붙이지 않음

SEO_HKabsumveritas@gmail.comBuilt with Claude Code & Codex

마지막 갱신 개인 학습 기록이며 운영 환경 적용 전 공식 문서 확인 필요

두 글자 이상 입력, 위아래 화살표로 이동하고 Enter로 열기