튜닝

오라클 튜닝 기초 03강 — 인덱스 기초: B-Tree는 어떻게 빨리 찾나

🎯 학습 목표

  • B-Tree 인덱스의 구조(루트·브랜치·리프)를 이해한다.
  • 인덱스로 1건을 찾는 과정을 단계로 설명한다.
  • 인덱스가 항상 좋은 것은 아닌 이유(클러스터링 팩터)를 안다.

📖 인덱스는 “책 뒤의 색인”이다

두꺼운 책에서 “트랜잭션”이라는 단어를 찾는다고 합시다. 1페이지부터 넘기면(전체 스캔) 오래 걸리지만, 책 뒤 색인(index)에서 “트랜잭션 … 245p”를 보고 바로 245페이지로 가면 빠릅니다. 데이터베이스 인덱스도 똑같습니다. 값(컬럼)을 정렬해 두고, 그 값이 실제 데이터의 어디(ROWID)에 있는지 알려줍니다.

📖 B-Tree 구조

                 [ 루트 블록 ]          ← 1개, 큰 범위를 나눔
                /     |      
        [브랜치]   [브랜치]   [브랜치]    ← 범위를 더 잘게 나눔
        /         /         /   
     [리프][리프] [리프][리프] [리프][리프] ← 정렬된 (값 → ROWID) 목록
       └──── 리프끼리 양방향 연결(범위 스캔용) ────┘

* 루트 → 브랜치 → 리프로 내려가며 범위를 좁힘 (보통 3~4단계면 충분)
* 1000만 건이어도 3~4번의 블록 읽기로 원하는 리프에 도달
* 리프는 정렬되어 있고 옆 리프와 연결 → 범위 검색(BETWEEN, LIKE 'A%')에 강함

핵심은 “몇 단계만 내려가면 원하는 값에 도달한다”는 점입니다. 데이터가 1000만 건이든 1억 건이든 트리 높이는 거의 늘지 않아(3~4단계), 적은 블록 읽기로 검색이 끝납니다.

💻 인덱스로 1건 찾는 과정

CREATE INDEX emp_email_ix ON emp(email);   -- email 정렬 인덱스 생성

SELECT * FROM emp WHERE email = 'kim@example.com';
-- ① 루트 블록 읽기   → 'k'는 어느 브랜치?
-- ② 브랜치 블록 읽기 → 'kim@'은 어느 리프?
-- ③ 리프 블록 읽기   → 'kim@example.com'의 ROWID 획득
-- ④ 그 ROWID로 테이블 블록 1개 읽어 행 반환   (TABLE ACCESS BY INDEX ROWID)
-- 총 4블록 안팎 vs 전체 스캔 수만 블록

📖 인덱스가 만능이 아닌 이유 — 클러스터링 팩터

인덱스로 ROWID를 찾은 뒤에는 테이블에서 실제 행을 읽어야 합니다(컬럼 전체가 필요할 때). 그런데 찾은 행 100건이 테이블의 100개 서로 다른 블록에 흩어져 있다면, 테이블 블록을 100번 읽어야 합니다. 이렇게 인덱스 정렬 순서와 테이블 저장 순서가 얼마나 일치하는가클러스터링 팩터(Clustering Factor)라 합니다.

정렬 잘 맞음(좋은 CF):  찾은 행들이 같은/이웃 블록에 모여 있음 → 테이블 읽기 적음
정렬 안 맞음(나쁜 CF):  찾은 행들이 전 블록에 흩어짐 → 인덱스 타도 테이블 읽기 폭증

→ 그래서 "조회 범위가 전체의 일정 비율(흔히 통설로 ~10~20%)을 넘으면"
  인덱스로 한 건씩 테이블을 들락거리는 것보다 그냥 전체 스캔이 더 빠를 수 있다.

⚠️ 주의사항

  • 인덱스는 공짜가 아닙니다. INSERT/UPDATE/DELETE 시 인덱스도 함께 갱신해야 하므로 DML이 느려지고, 저장공간을 차지합니다. “안 쓰는 인덱스”는 오히려 부담입니다.
  • 넓은 범위(테이블의 큰 비율)를 읽는 SQL에 인덱스를 강제하면 전체 스캔보다 더 느려질 수 있습니다. 인덱스는 “소량을 콕 집어낼 때” 강합니다.

💡 팁

  • 인덱스 후보 1순위: WHERE 절·조인 조건·ORDER BY에 자주 쓰이면서, 적은 행을 골라내는(선택도 높은) 컬럼.
  • 다음 강(4강)에서 인덱스를 “만들어 놨는데도 안 타는” 가장 흔한 실수들을 다룹니다.