“인덱스를 탄다”는 게 내부적으로 무슨 일인지 정리했다. MySQL은 SQL 엔진(옵티마이저·실행기) 과 스토리지 엔진(InnoDB) 이 핸들러 API로 통신하는 구조라, 인덱스 스캔도 이 둘의 협업으로 일어난다.
큰 그림: 두 계층
- MySQL 엔진 — 쿼리를 파싱하고, 옵티마이저가 어떤 인덱스를 어떻게 탈지 결정하고, 스토리지 엔진에 “다음 레코드 줘”를 요청한다
- InnoDB — 실제 B-Tree를 탐색하고 페이지(기본 16KB)를 읽어 레코드를 돌려준다
스캔 과정
1. 접근 방식 결정 (옵티마이저)
옵티마이저가 WHERE·JOIN·ORDER BY를 보고 어떤 인덱스로 어떤 범위를 읽을지 정한다. 결과는 EXPLAIN의 type으로 드러난다(아래).
2. B-Tree 탐색 — 시작점 찾기
InnoDB 인덱스는 B-Tree다. 루트 → 브랜치 → 리프로 내려가며 조건에 맞는 시작 위치를 찾는다. 리프 노드는 서로 연결(doubly-linked)돼 있어, 범위 스캔은 시작 리프부터 옆으로 순회한다.
3. 세컨더리 인덱스 → 클러스터드 인덱스 룩업
InnoDB에서 클러스터드 인덱스(=PK)의 리프에는 행 전체가 들어 있고, 세컨더리 인덱스의 리프에는 PK 값만 들어 있다. 그래서 세컨더리 인덱스로 찾은 뒤 SELECT에 다른 컬럼이 필요하면, 그 PK로 클러스터드 인덱스를 한 번 더 조회한다.
이 2차 조회가 랜덤 I/O를 유발한다. 조회 건수가 많으면 “인덱스를 탔는데도 느린” 원인이 되곤 한다.
4. 조건 필터링 & 결과 반환
읽어온 레코드에 남은 조건을 적용하고(인덱스로 못 거른 조건은 여기서), SELECT 컬럼을 모아 결과를 반환한다. 정렬·집계가 필요하면 임시 테이블/정렬 버퍼를 거친다.
EXPLAIN 접근 타입 (좋음 → 나쁨)
옵티마이저가 고른 방식은 EXPLAIN의 type으로 확인한다.
| type | 의미 |
|---|---|
const / system | PK·유니크로 단일 행. 가장 빠름 |
eq_ref | 조인에서 상대 테이블을 PK·유니크로 1건씩 |
ref | 비유니크 인덱스로 같은 값 여러 건 |
range | 인덱스 범위 스캔(BETWEEN, >, IN 등) |
index | 인덱스 풀 스캔 — 인덱스 전체를 훑음(테이블 풀스캔보단 나음) |
ALL | 테이블 풀 스캔 — 인덱스 못 탐. 튜닝 대상 |
커버링 인덱스
SELECT·WHERE에 필요한 모든 컬럼이 인덱스에 있으면, 3단계의 클러스터드 인덱스 재조회가 사라진다. 인덱스만 읽고 끝나는 것이다. EXPLAIN의 Extra에 Using index 로 표시된다.
-- (user_id, status) 복합 인덱스가 있을 때
SELECT status FROM orders WHERE user_id = 100; -- 인덱스만으로 해결 → Using index
SELECT * FROM orders WHERE user_id = 100; -- 다른 컬럼 필요 → PK로 클러스터드 재조회
정리
- 인덱스 스캔 = 옵티마이저가 방식 결정 → InnoDB가 B-Tree 탐색 → (필요시) PK로 클러스터드 재조회 → 필터·반환
- 세컨더리 인덱스 리프엔 PK만 있다 → 다른 컬럼 필요하면 2차 조회(랜덤 I/O)
EXPLAIN type은const/eq_ref/ref/range를 노리고index/ALL을 피한다- 커버링 인덱스(
Using index) 로 2차 조회 자체를 없애는 게 강력한 튜닝