#8. 커버링 인덱스
두 번 탐색을 한 번으로
# 커버링 인덱스 — 두 번 탐색을 한 번으로
> 📘 **학습자료 9편** | 연결 퀴즈: [**인덱스 활용2 풀러가기**](quiz.html?set=D)
> **커버링 인덱스(Covering Index)** 를 이해하면 인덱스 사용을 보는 눈이 달라집니다.
> 그리고 이걸 의식하면 인덱스 설계 자체가 바뀝니다.
> 📚 **이전 편**: [8편 - 여러 조건이 결합된 쿼리에서 인덱스]
> 8편까지 "어떤 쿼리가 인덱스를 타는가"를 다뤘다면, 이번 편은 **"무엇을 가져오는가"의 차이**가 만드는 강력한 최적화입니다.
## 이 자료를 다 읽으면 알게 되는 것
- 보조 인덱스로 `SELECT *` 할 때 일어나는 **두 번 탐색** (3편 복습)
- **커버링 인덱스**가 무엇이고, 왜 빠른지
- EXPLAIN과 EXPLAIN ANALYZE에서 커버링 인덱스를 **식별하는 법**
- **인덱스 설계 관점**에서 컴포지트 인덱스를 어떻게 짤지
---
## 📑 목차
- [1. 복습: 보조 인덱스의 두 번 탐색](#1-복습-보조-인덱스의-두-번-탐색)
- [⭐ 2. 커버링 인덱스: 두 번째 탐색을 건너뛰기](#2-커버링-인덱스-두-번째-탐색을-건너뛰기)
- [3. EXPLAIN으로 커버링 인덱스 식별하기](#3-explain으로-커버링-인덱스-식별하기)
- [4. 인덱스 설계 관점: 컴포지트 인덱스를 짜는 법](#4-인덱스-설계-관점-컴포지트-인덱스를-짜는-법)
- [5. 커버링 인덱스의 비용과 한계](#5-커버링-인덱스의-비용과-한계)
- [핵심 요약](#핵심-요약)
---
## 1. 복습: 보조 인덱스의 두 번 탐색
본격적으로 들어가기 전에, 3편에서 다룬 핵심 개념을 짧게 복습합니다.
InnoDB의 **보조 인덱스**(Secondary Index)의 리프 노드에는 **무엇**이 들어있나요?
> **보조 인덱스 키 + 클러스터링 인덱스 키(PK)**
실제 데이터 레코드는 **클러스터링 인덱스의 리프 노드**에만 있습니다. 그래서 보조 인덱스로 모든 컬럼을 가져오려 하면:
1. **보조 인덱스 탐색**: 조건에 맞는 행의 PK를 찾음
2. **클러스터링 인덱스 재탐색**: 그 PK로 실제 데이터 페이지에 접근
이 **두 단계**가 일어납니다. 그리고 두 번째 단계는 종종 **Random I/O**를 동반해 비용이 큽니다.
> 💡 **MySQL 공식 문서의 설명**
> "InnoDB에서 각 보조 인덱스 레코드는 해당 행의 **PK 컬럼**과 보조 인덱스에 지정된 컬럼들을 함께 보유한다. InnoDB는 이 PK 값을 사용해 클러스터링 인덱스에서 행을 찾는다."
이 한 문장이 이번 편 전체의 출발점입니다.
---
## 2. 커버링 인덱스: 두 번째 탐색을 건너뛰기
> ⭐ **이 자료에서 가장 중요한 섹션입니다.** 이 한 가지 개념이 인덱스 활용의 가장 강력한 카드입니다.
### 핵심 아이디어
> **만약 쿼리가 필요로 하는 모든 컬럼이 이미 보조 인덱스에 들어있다면?**
> **그렇다면 클러스터링 인덱스를 다시 탐색할 필요가 없다.**
이게 **커버링 인덱스(Covering Index)** 의 정의입니다. 보조 인덱스가 쿼리의 모든 요구를 "커버한다"는 의미입니다.
### 예시로 이해하기
다음 테이블을 봅시다.
```sql
CREATE TABLE test_case (
col1 INT PRIMARY KEY, -- 클러스터링 인덱스
col2 VARCHAR(100),
col3 VARCHAR(100),
INDEX idx_col2 (col2) -- col2 보조 인덱스
);
```
리프 노드에 **`col2` (인덱스 키) + `col1` (PK)** 가 함께 저장되어 있습니다.
### 케이스 A: 커버링 인덱스가 안 되는 경우
```sql
SELECT * FROM test_case WHERE col2 = 'ABC';
```
`SELECT *`는 `col1, col2, col3` 모두를 요구합니다. 그런데 `idx_col2` 리프에는 `col1, col2`만 있습니다. **`col3`은 없습니다.**
→ `col3`을 가져오려면 클러스터링 인덱스를 한 번 더 탐색해야 합니다. **두 번 탐색.**
EXPLAIN ANALYZE 결과:
```
-> Index lookup on test_case using idx_col2 (col2 = 'ABC')
```
**`Index lookup`** — 인덱스로 PK를 찾은 뒤 테이블에 접근했다는 의미입니다.
### 케이스 B: 커버링 인덱스가 되는 경우
```sql
SELECT col1 FROM test_case WHERE col2 = 'ABC';
```
이 쿼리가 요구하는 컬럼은 `col1, col2`뿐입니다. 그리고 `idx_col2` 리프에 **이미 둘 다 있습니다.**
→ 클러스터링 인덱스 재탐색 **불필요**. **한 번 탐색으로 끝.**
EXPLAIN ANALYZE 결과:
```
-> Covering index lookup on test_case using idx_col2 (col2 = 'ABC')
```
**`Covering index lookup`** — 커버링 인덱스로 처리됐다는 명시적 신호입니다.
### 왜 이게 강력한가
- 두 번째 탐색은 클러스터링 인덱스 B+Tree를 다시 내려가야 함
- 각 행마다 일어나서, 결과가 N개면 N번의 탐색
- 실제 데이터 페이지는 디스크에 흩어져 있어 **Random I/O**
- 버퍼풀에 없으면 매번 디스크 접근
커버링 인덱스는 **이 모든 비용을 한 번에 제거**합니다. 같은 쿼리가 **수십 배 빨라지는** 경우도 흔합니다.
> 💡 여기까지 읽었다면 커버링 인덱스의 핵심을 충분히 이해한 것입니다. [**퀴즈로 확인해보세요!**](quiz.html?set=D)
---
## 3. EXPLAIN으로 커버링 인덱스 식별하기
실무에서 "이 쿼리가 커버링 인덱스를 쓰고 있는가?"를 확인하는 방법은 두 가지입니다.
### 방법 1: EXPLAIN의 `Extra` 컬럼
```sql
EXPLAIN SELECT col1 FROM test_case WHERE col2 = 'ABC';
```
| id | select_type | table | type | key | ref | rows | Extra |
|----|-------------|-------|------|-----|-----|------|-------|
| 1 | SIMPLE | test_case | ref | idx_col2 | const | 1 | **Using index** |
**`Extra: Using index`** — 이게 커버링 인덱스의 신호입니다.
> ⚠️ **헷갈리지 말 것**
> - `Using index` = 커버링 인덱스 사용 (좋음)
> - `Using index condition` = Index Condition Pushdown 사용 (다른 개념)
> - `Using where` = 인덱스 활용 후 추가 필터링 (커버링 아님)
>
> **`Using index`** 만 정확히 커버링 인덱스를 의미합니다.
### 방법 2: EXPLAIN ANALYZE의 출력
```sql
EXPLAIN ANALYZE SELECT col1 FROM test_case WHERE col2 = 'ABC';
```
```
-> Covering index lookup on test_case using idx_col2 (col2 = 'ABC')
```
**`Covering index lookup`** 또는 **`Covering index scan`** 이 명시적으로 표시됩니다.
비교 — 커버링이 아닌 경우:
```
-> Index lookup on test_case using idx_col2 (col2 = 'ABC')
```
`Covering` 단어 한 개의 차이지만 의미는 큽니다.
---
## 4. 인덱스 설계 관점: 복합 인덱스를 짜는 법
여기까지 오면 자연스럽게 한 가지 질문이 생깁니다.
> "그럼 자주 같이 조회되는 컬럼들을 묶어서 복합 인덱스로 만들면, 더 많은 쿼리가 커버링 인덱스가 되지 않을까?"
정확합니다. 이게 **인덱스 설계의 핵심 통찰** 중 하나입니다.
### 사례 — 게시글 목록 조회
게시글 시스템을 떠올려봅시다. 메인 페이지에 "최근 게시글 목록"을 보여주는 쿼리:
```sql
SELECT id, title, created_at
FROM posts
WHERE author_id = 123
ORDER BY created_at DESC
LIMIT 20;
```
흔히 다음과 같이 인덱스를 만듭니다:
```sql
CREATE INDEX idx_author ON posts(author_id);
```
이 경우 동작은:
1. `idx_author`로 `author_id = 123`인 행 찾기
2. 각 행마다 클러스터링 인덱스로 가서 `title`, `created_at` 가져오기 (두 번 탐색)
3. `created_at`으로 정렬
4. 상위 20개 반환
여기서 **두 가지 비효율**이 있습니다:
- 매 행마다 클러스터링 인덱스 재탐색 (Random I/O)
- `created_at`으로 정렬을 따로 수행 (filesort)
### 복합 인덱스로 둘 다 해결
```sql
CREATE INDEX idx_author_created ON posts(author_id, created_at);
```
이렇게 짜면:
- `author_id = 123` 영역으로 좁힌 후, 그 안에서 이미 `created_at`으로 정렬되어 있음 → **정렬 비용 제로**
- 리프 노드에 `author_id, created_at, id`(PK)가 모두 있음
여기서 한 발 더 나아가, `title`까지 인덱스에 포함시키면:
```sql
CREATE INDEX idx_author_created_title ON posts(author_id, created_at, title);
```
이제 리프 노드에 `author_id, created_at, title, id`가 모두 있습니다. 쿼리가 요구하는 모든 컬럼이 인덱스에 있으니 **완전한 커버링 인덱스**가 됩니다. 클러스터링 인덱스 재탐색이 사라집니다.
### 복합 인덱스 설계의 일반 원칙
보통 다음 순서를 권장합니다:
1. **WHERE 절의 동등조건 컬럼**을 가장 왼쪽에 (Left-Most 활용)
2. 그 다음 **WHERE 절의 범위조건 컬럼** 또는 **ORDER BY 컬럼**
3. 마지막에 **SELECT에서만 쓰이는 추가 컬럼** (커버링용)
위 예시로 보면:
- `author_id` (동등) → 1순위
- `created_at` (정렬) → 2순위
- `title` (단순 조회) → 3순위
> 💡 **MySQL 8.0의 INVISIBLE INDEX 활용 팁**
> 컴포지트 인덱스를 새로 만들 때 기존 인덱스가 정말 불필요한지 확실치 않다면, MySQL 8.0의 `ALTER TABLE ... ALTER INDEX ... INVISIBLE`로 인덱스를 일시적으로 "사용 안 함" 상태로 두고 운영 영향을 관찰할 수 있습니다.
> 문제가 생기면 즉시 `VISIBLE`로 되돌릴 수 있어 안전합니다.
---
## 5. 커버링 인덱스의 비용과 한계
커버링 인덱스가 만능은 아닙니다. **트레이드오프**가 있습니다.
### 비용 1: 인덱스 크기 증가
인덱스에 컬럼을 더 넣을수록 인덱스 자체가 커집니다.
- 디스크 사용량 증가
- 버퍼풀 점유 공간 증가 (다른 인덱스/데이터가 메모리에서 밀려남)
- 인덱스 자체의 B+Tree 깊이가 깊어질 수 있음
특히 `VARCHAR` 같은 큰 컬럼을 무작정 인덱스에 포함시키면 인덱스가 비대해져 오히려 손해입니다.
### 비용 2: INSERT/UPDATE/DELETE 부담
5편에서 본 것처럼 인덱스가 많아지거나 커질수록 변경 작업이 느려집니다. 인덱스 컬럼이 변경되면 인덱스도 갱신해야 하고, 컬럼이 많을수록 갱신 비용이 큽니다.
### 비용 3: 모든 쿼리에 만능이 아님
커버링 인덱스는 **특정 쿼리 패턴에 최적화**된 인덱스입니다. 그 쿼리가 자주 쓰이지 않거나, 다른 쿼리 패턴이 더 중요하다면 굳이 만들 필요 없습니다.
### 결론 — 언제 만들까?
- **자주 실행되는 쿼리**가 있고
- **특정 컬럼 몇 개**만 SELECT하며
- **WHERE/ORDER BY 패턴이 일관적**이라면
- → 복합 인덱스로 커버링 인덱스를 만들 가치가 큽니다
반대로 `SELECT *`를 쓰는 쿼리는 커버링 인덱스를 만들 수 없습니다. 모든 컬럼을 인덱스에 넣을 수는 없으니까요. **`SELECT *`를 피하고 필요한 컬럼만 명시하는 습관**이 커버링 인덱스의 출발점이기도 합니다.
---
## 핵심 요약
이번 편에서 꼭 가져가야 할 한 가지:
> 🎯 **인덱스가 쿼리의 모든 요구를 커버하면, 두 번째 탐색은 사라진다**
> 보조 인덱스 자체에 필요한 컬럼이 다 있으면 클러스터링 인덱스로 다시 갈 필요가 없습니다.
> 이걸 **커버링 인덱스**라 하고, EXPLAIN의 `Using index`로 확인할 수 있습니다.
체크리스트:
- [x] 커버링 인덱스가 무엇이고, 왜 빠른지 설명할 수 있다 (두 번 탐색 → 한 번)
- [x] EXPLAIN의 `Using index`와 EXPLAIN ANALYZE의 `Covering index lookup`을 구분할 수 있다
- [ ] `Using index`와 `Using index condition`, `Using where`의 차이를 안다
- [ ] 컴포지트 인덱스 설계의 **3단계 순서**(동등 → 범위/정렬 → 추가 조회)를 안다
- [ ] 커버링 인덱스의 **비용**(크기 증가, 변경 부담)을 안다
- [ ] `SELECT *`를 피하는 습관이 왜 인덱스 최적화의 출발점인지 안다
> 📝 체크리스트를 다 채울 자신이 있다면? [**인덱스 활용2 퀴즈 도전하기**](quiz.html?set=D)
---
## 이제 퀴즈에 도전하기
2편에 걸쳐 인덱스 활용2의 모든 개념을 다뤘습니다.
- 함수로 변형된 조건은 인덱스를 못 탄다 (8편)
- AND/OR/IN/NOT IN의 결합 방식이 인덱스 활용을 결정한다 (8편)
- 커버링 인덱스 — 두 번 탐색을 한 번으로 (9편)
- 컴포지트 인덱스 설계의 3단계 순서 (9편)
> 🎯 [**인덱스 활용2 퀴즈 풀어보기**](quiz.html?set=D)
> 7문제 중 막히는 게 있다면, 해당 학습자료의 섹션으로 돌아와 다시 읽어보세요.
> 특히 **EXPLAIN 결과를 해석하는 능력**이 활용2의 핵심입니다.
> `type`, `key`, `Extra` 세 컬럼만 자신 있게 읽을 수 있어도 인덱스 관련 이슈의 절반은 잡을 수 있습니다.
---
## Reference
- MySQL 공식 문서: [InnoDB Index Types](https://dev.mysql.com/doc/refman/8.4/en/innodb-index-types.html)
- MySQL 공식 문서: [EXPLAIN Output Format](https://dev.mysql.com/doc/refman/8.4/en/explain-output.html)
- MySQL 공식 문서: [Invisible Indexes](https://dev.mysql.com/doc/refman/8.4/en/invisible-indexes.html)