SQLP 준비하면서 친절한 SQL 튜닝을 읽고 정리한 포스팅입니다.
2장 인덱스 기본을 학습하고 정리하였습니다.
[목차]
[이전 포스팅]
1. 인덱스 구조 및 탐색
1) 미리 보는 인덱스 튜닝
- 학생명부 예시
- 이름/학년-반-번호로 구성
- 이게 인덱스
- 학년-반-번호가 ROWID 역할
- DB 테이블에서 데이터 찾는 법
- 테이블 전체 스캔
- 인덱스 이용
- 인덱스는 큰 테이블에서 소량데이터 검색시 유용(OLTP 시스템)
- 인덱스 튜닝 핵심 방법
- 인덱스 스캔 효율화 튜닝
- 테이블 엑세스 횟수 줄이기(인덱스 랜덤I/O)
- 랜덤I/O를 줄이는 것이 핵심이며 파티션, IOT, 클러스터 등 I/O를 줄이기 위한 노력들
2) 인덱스 구조
- 인덱스는 책 뒤 색인과 같은 역할
- 범위스캔(Range Scan)이 가능한 이유는 인덱스(색인)이 정렬되어 있기 때문
- DBMS는 B*Tree 인덱스를 기본적으로 사용
- 루트 블록,브랜치 블록,리프 블록 구성
- 루트, 브랜치에 키값 안 갖는 특별한 레코드: LMC(Leftmost Child)
- 자식 노드 중 가장 왼쪽 끝에 위치한 블록 가리킴
- 리프 블록에 저장된 레코드는 키값 순으로 정렬 + 테이블레코드를 가리키는 rowid 주소값 가짐
- 인덱스는 키값 같으면 rowid 순 정렬
- ROWID = 테이블 블록 주소(데이터 파일 번호 + 블록 번호) + 로우 번호
- 인덱스 탐색 과정
- 수직적 탐색(인덱스 스캔 시작지점을 찾는 과정)
- 수평적 탐색(데이터를 찾는 과정)
3) 인덱스 수직적 탐색
- 정렬된 인덱스 레코드 중 조건 만족하는 '첫번째' 레코드를 찾는 과정
- 인덱스 스캔 시작지점 찾는 과정
- 루트(Root) 블록에서 시작
- '홍길동'을 검색할 때, 루트블록에 홍길동과 같거나 큰 값이 없으면 이전 레코드(박길동)이 가리키는 하위블록(>=박) 이동
- 박길동이 포함된 브랜치 블록에서 '홍길구'보다 큰 홍달동 찾으면 그 이전 레코드(홍길구)에 연결된 리프블록 찾아가서 첫번째 엑세스 조건 확인 가능
- 주의점
- 루트 블록, 브랜치 블록에는 실제 데이터저장 안 됨(키값과 포인터만 갖고 있음)
- LMC를 제외하고 큰 값만 찾아가는 방향
4) 인덱스 수평적 탐색
- 수직적 탐색으로 스캔 시작점 찾았으면 수평적 탐색으로 찾고자 하는 모든 데이터를 찾는 과정
- 양방향 링크드 리스트 구조라서 좌,우 수평 탐색 가능
- 일반적으로 인덱스 엑세스 후 테이블 엑세스도 하기 때문에 리프 블록에서 데이터와 함께 ROWID도 찾아야 함
5) 결합 인덱스 구조와 탐색
- 두개 이상 컬럼 결합하여 인덱스 만들 수 있음
- 인덱스 구성(성별,고객명) / 성별='남' 고객명='홍길동'을 찾는다면?
- 선별='남'을 먼저 찾은 후 고객명='홍길동'을 찾는 것이 아님
- 수직적 탐색으로 찾은 인덱스 스캔 시작점은 성별='남'인 첫번째 레코드가 아닌 성별='남' & 고객명='이재희' 레코드
- 인덱스를 (고객명,성별)로 하더라도 읽는 인덱스 블록 개수 동일(블록I/O개수가 동일하므로 성능도 동일)
- 엑셀 필터처럼 동작하는 것이 아님
2. 인덱스 기본 사용법
1) 인덱스를 사용한다는 것
- 기본은 Range Scan을 한다는 것
- 책의 색인
- 가공한 값이나 중간값으로는 스캔 시작점을 찾을 수 없음
- 색인을 쓸 수도 있으나 시작점을 모르기때문에 range scan은 안 됨 (색인 전체 스캔)
- 인덱스 컬럼 중 선두 컬럼을 가공하지 않아야 Range Scan이 가능
2) 인덱스를 Range Scan 할 수 없는 이유
- 초등학생 생년월일(yyyymmdd)순으로 정렬했다고 가정
- 년도로 찾으면 시작점을 찾을 수 있으나, 월로 찾으면 시작점을 찾을 수 없기 때문에 전체 스캔 필요
- OR 조건 역시 Range Scan 불가능하지만 옵티마이저에 의해 UNION ALL로 쪼개지고 각 조건에 선두컬럼이 인덱스 걸려있으면 Range Scan 가능(OR Expansion)
- use_concat 힌트를 이용해서 가능(실행계획에 CONCATENATION)
- IN 조건도 옵티마이저에 의해 쪼개져서 IN-List Iterator 방식으로 사용되어 Range Scan이 가능할 수 있음
- 실행계획에 INLIST_ITERATOR
3) 더 중요한 인덱스 사용 조건
- 인덱스 선두 컬럼이 조건절에 있어야 Range Scan
- 중간컬럼은 가공되어도 됨
- but, 인덱스 Range Scan이 항상 성능 좋은 것은 아님
- 주문일자, 상품번호 인덱스일 때, 매일 100만건 데이터 쌓인다고 가정
- 상품번호를 like로 찾거나 가공하면 스캔 범위 줄이는 역할 못함
- 주문일자로 100만건 조회되므로 효율성에서 문제 가능
4) 인덱스를 이용한 소트 연산 생략
- 인덱스가 정렬되어 있기 때문에 소트연산생략 효과 얻을 수 있음
- 고객테이블에 고객명, 성별, 고객전화번호로 구성된 인덱스가 있다고 가정
- 고객명, 성별이 조건이고 고객전화번호를 ORDER BY에 사용하면?
- 고객명, 성별조건으로 검색하면 고객전화번호는 자동정렬이므로 ORDER BY 생략 가능
- 내림차순도 자동으로 생략 가능(리프블록의 양방향 연결 리스트)
5) ORDER BY 절에서 컬럼 가공
- ORDER BY, SELECT-LIST에서 컬럼 가공으로 인덱스 정상 사용할 수 없는 경우 존재
- ORDER BY에 인덱스에 포함된 컬럼 2개가 포함되었다고 가정
- ORDER BY 컬럼1, 컬럼2 (인덱스 정상 사용 가능)
- ORDER BY 컬럼1 | 컬럼2 (가공되었으므로 정상 사용 불가능)
6) SELECT-LIST에서 컬럼 가공
- 고객테이블 인덱스 고객명,성별,고객전화번호 구성일 경우
- 아래 테이블 MIN, MAX 쿼리 정렬 수행 안 함(리프노드에서 가장 왼쪽, 오른쪽만 찾으면 되기 때문)
/* 실행 계획 FIRST ROW, INDEX RANGE SCAN(MIN/MAX) 확인 가능 */
SELECT MIN(고객전화번호)
FROM 고객
WHERE 고객명 = '홍길동';
SELECT MAX(고객전화번호)
FROM 고객
WHERE 고객명 = '홍길동';
7) 자동 형변환
- 생년월일이 선두 컬럼인 인덱스가 있을 때, 생년월일컬럼이 문자형인데 숫자값으로 검색을 한다면?
- 문자형 vs 숫자형은 숫자형이 이기기 때문에 문자형 컬럼이 TO_NUMBER로 가공되어 인덱스 사용 안 됨
- 동일하게 날짜형과 문자형이 만나면 날짜형이 이김
- 문자형 vs 숫자형은 기본적으로 숫자형이 이기지만 LIKE '문자열' 비교 연산 사용시에는 숫자형컬럼이 문자형으로 변환됨
- 숫자형 컬럼을 LIKE 조건으로 검색하면 자동형변환이 발생하므로 주의
- 또한 문자형,숫자형 컬럼 비교시 숫자형 컬럼에 문자값이 포함될 경우, 쿼리 수행 오류도 발생 가능
- where number_column = varchar_column; --수치 부적합 오류
- 결론?
- TO_CHAR, TO_DATE 등을 생략한다고 성능향상에 큰 도움 안 됨
- 성능에는 블록 I/O 줄이는 것이 중요하기 때문
- 인덱스를 안 타면 결국 블록 I/O 횟수가 늘기 때문에 정확한 형변환 필요
3. 인덱스 확장 기능 사용법
- index range scan
- 가장 기본적인 인덱스 수행 방식
- 루트 -> 리프까지 수직적 탐색 후 필요 범위(range) 스캔
- 선두컬럼 가공 안 해야 함
- index full scan
- 데이터 검색을 위한 최적 인덱스가 없을 때 차선 선택
- 선두컬럼이 조건절에 없을 경우 range scan 불가능하나, 뒷쪽 컬럼이 인덱스에 있을 경우 선택
- 옵티마이저는 이럴 경우 Table Full Scan부터 고려. 근데 대용량 테이블이라면 인덱스 전략 다시 고려
- 해당 방식도 소팅생략 가능
- 옵티마이저가 Table Full Scan이 더 효율적인데 Index Full Scan을 선택할 경우
- 힌트로 first_rows를 줬다면?
- 처음일부를 빠르게 찾을 수 있기 때문에 Index Full Scan 채택 가능(부붐범위 처리)
- first_rows: 처음 n개 로우를 빠르게 가져오는 쪽으로 실행계획 최적화
- index unique scan
- 수직적 탐색만으로 데이터 찾는 방식
- pk 찾기. 1건만 찾으면 되므로 수직적 탐색으로 1건 찾으면 끝
- index skip scan
- 인덱스 선두 컬럼 조건절에 사용 안 하면
- Table Full Scan 또는 Index Full Scan 고려
- 9i 버전부터 인덱스 선두컬럼이 조건절에 없어도 인덱스 활용하는 새로운 스캔 방식이 index skip scan
- 조건절에 빠진 인덱스 선두컬럼의 DV(Distinct Value) 개수가 적고 후행 컬럼의 DV 개수가 많을 때 유용
- 선두컬럼 성별, 뒷컬럼 고객번호
- 루트, 브랜치 블록에서 읽은 컬럼 값 정보를 이용해 조건절에 부함하는 레코드를 포함할 '가능성 있는' 리프 블록만 골라 엑세스하는 방식
- 중간컬럼에 대한 조건절이 없거나 선두컬럼이 like, between 등 범위검색 조건일 때도 사용 가능
- 인덱스 선두 컬럼 조건절에 사용 안 하면
- index fast full scan
- index full scan보다는 빠름
- 논리적인 인덱스 트리 구조 무시하고 인덱스 세그먼트 전체를 멀티블록 I/O방식으로 스캔
- 물리적으로 디스크에 저장된 순서대로 인덱스 리프블록들을 읽음
- 멀티블록 I/O라서 대량 인덱스 블록 읽을때 효과
- 리프 노드 연결리스트 구조 무시하고 읽어서 인덱스 키순서대로 정렬 안 됨
- 인덱스가 파티션 되어있지 않더라도 병렬 쿼리가 가능
- 병렬 쿼리시 Direct Path I/O 방식으로 더 빠름
- 관련힌트 index_ffs, no_index_ffs
- index range scan descending
- index range scan과 기본적으로 동일(뒤에서부터 스캔)
- index_desc로 유도 가능
📖 출처: 『친절한 SQLP튜닝』, 조시형 저, DBian, 2025
반응형
'개발자 일지 > DB' 카테고리의 다른 글
[친절한 SQL 튜닝 정리] 1장. SQL 처리 과정과 I/O (2) | 2025.05.28 |
---|---|
[SQLP 준비] 친절한 SQL 튜닝 후기 (1) | 2025.05.18 |
[SQLP 대비] Oracle 기반 DB 튜닝 용어 정리 Part 1 (1) | 2025.05.09 |
[책 후기] SQL레벨업 리뷰 및 정리 (1) | 2024.11.25 |
DB 튜닝 공부 (개념, 용어, 최적화 전략 등) (0) | 2024.09.27 |