#14. ICP(Index Condition Pushdown)
스토리지 엔진에 필터링을 맡기기
# Index Condition Pushdown — 스토리지 엔진에 필터링을 맡기기
> 📘 **학습자료 15편** | 연결 퀴즈: [**인덱스 고급1 풀러가기**](quiz.html?set=G)
> MySQL 5.6부터 도입된 최적화 기법.
> "인덱스에 있는 컬럼이지만 인덱스 탐색 조건으로는 못 쓰는" 애매한 조건들을, 스토리지 엔진이 **인덱스 안에서 미리 걸러주는** 기법입니다.
> 이걸 알면 EXPLAIN의 `Using index condition` 한 줄이 갑자기 의미를 가지기 시작합니다.
> 📚 **이전 편**: [14편 - 인덱스와 정렬]
> 9편의 `Using index` / `Using where` / `Using index condition` 셋 중에서,
> 마지막 하나가 이번 편의 주제입니다. 9편을 한 번 더 떠올리면 도움이 됩니다.
## 이 자료를 다 읽으면 알게 되는 것
- MySQL이 **MySQL 엔진**과 **스토리지 엔진**으로 나뉘어 동작한다는 사실
- ICP가 없을 때(MySQL 5.5 이하) 발생했던 **불필요한 랜덤 I/O** 문제
- ICP가 있을 때(5.6+) 어떻게 그 문제를 해결하는지
- EXPLAIN의 `Using index condition`이 의미하는 것
---
## 📑 목차
- [1. MySQL의 두 층: MySQL 엔진과 스토리지 엔진](#1-mysql의-두-층-mysql-엔진과-스토리지-엔진)
- [⭐ 2. ICP가 없을 때의 문제](#2-icp가-없을-때의-문제)
- [⭐ 3. ICP가 있을 때: 스토리지 엔진에 필터링을 맡긴다](#3-icp가-있을-때-스토리지-엔진에-필터링을-맡긴다)
- [4. EXPLAIN으로 ICP 식별하기](#4-explain으로-icp-식별하기)
- [5. ICP를 끄고 비교해보기](#5-icp를-끄고-비교해보기)
- [핵심 요약](#핵심-요약)
---
## 1. MySQL의 두 층: MySQL 엔진과 스토리지 엔진
ICP를 이해하려면 먼저 MySQL의 내부 구조를 한 번 짚어야 합니다.
MySQL은 **두 개의 층**으로 동작합니다.

MySQL 엔진이 "이런 데이터 가져와줘" 라고 시키면, 스토리지 엔진이 가져옵니다. 둘은 **인터페이스를 통해 통신**합니다.
여기서 중요한 점: **"무엇을 어디까지 스토리지 엔진에 맡기느냐"** 가 성능에 큰 영향을 미칩니다. 스토리지 엔진이 미리 걸러서 적은 양만 올려보내면 효율적이고, 많이 올려보내고 MySQL 엔진이 일일이 거르면 비효율적이죠.
이 분배 방식의 변화가 ICP의 핵심입니다.
---
## 2. ICP가 없을 때의 문제
> ⭐ **이 섹션이 ICP의 필요성을 이해하는 핵심입니다.**
다음 상황을 봅시다.
```sql
-- (num, name) 복합 인덱스 존재
SELECT *
FROM test_icp
WHERE num = 1
AND name LIKE '%q'; -- ← 좌측 와일드카드, 인덱스 탐색 불가
```
조건을 살펴보면:
- `num = 1`: 동등 조건. 인덱스 탐색에 사용 가능
- `name LIKE '%q'`: 좌측 와일드카드. **인덱스 탐색에 사용 불가** (8편 참고)
근데 `name`은 인덱스에 포함되어 있긴 합니다. 그러나, Left-Most 정렬의 특성으로 인덱스를 효율적으로 활용하지는 못하고 한행한행 탐색이 필요합니다.
### MySQL 5.5 이전: 조건 자체를 스토리지 엔진에 안 넘김
5.5 이전 MySQL은 이렇게 동작했습니다:

1. MySQL 엔진이 `num = 1` 조건만 스토리지 엔진에 전달
2. 스토리지 엔진이 인덱스로 `num = 1`인 행들 찾기 (예: 1,000건)
3. 각 PK로 클러스터링 인덱스에 가서 **실제 데이터 행 가져오기** (1,000번의 랜덤 I/O)
4. 1,000건을 MySQL 엔진에 전달
5. MySQL 엔진이 `name LIKE '%q'` 조건을 한 건 한 건 검사

### 무엇이 문제인가
만약 `name LIKE '%q'`까지 만족하는 행이 **단 3건**뿐이라면? **나머지 997건은 헛수고**입니다.
역시나 **불필요한 랜덤 I/O**문제가 생깁니다. `name` 정보가 인덱스에 있는데도 활용을 못 하니까, 일단 테이블에 다녀와서 검사하게 됩니다.
> ⚠️ **여기서 중요한 점**
> `name` 조건이 "인덱스 탐색"으로는 못 써도, "인덱스에서 값을 읽어 비교"하는 건 가능합니다.
> 5.5 이전에는 이 차이를 활용하지 못했고, ICP는 정확히 이 부분을 개선합니다.
---
## 3. ICP가 있을 때: 스토리지 엔진에 필터링을 맡긴다
> ⭐ **이게 ICP의 핵심 아이디어입니다.**
MySQL 5.6부터는 위 시나리오가 다음과 같이 바뀝니다.

1. MySQL 엔진이 `num = 1` **그리고** `name LIKE '%q'` 조건을 둘 다 스토리지 엔진에 전달
2. 스토리지 엔진이 인덱스로 `num = 1`인 항목 찾기 (1,000건)
3. **그 1,000건의 인덱스 안에서 `name LIKE '%q'`를 미리 검사** → 3건만 통과
4. 그 3건의 PK로 클러스터링 인덱스에 다녀와서 데이터 가져오기 (3번의 랜덤 I/O)
5. 3건만 MySQL 엔진에 전달
### 무엇이 달라졌나
| 단계 | ICP 없음 | ICP 있음 |
|---|---|---|
| 인덱스에서 좁힘 | 1,000건 | 1,000건 (동일) |
| 테이블 다녀오기 | **1,000번** | **3번** |
| MySQL 엔진이 받는 양 | 1,000건 | 3건 |
| MySQL 엔진의 사후 필터링 | 필요 (1,000건 검사) | 불필요 |
랜덤 I/O가 **1,000번 → 3번** 으로 줄었습니다. 약 333배 차이.
### 핵심을 한 줄로
> 🎯 **인덱스로 직접 탐색은 못 해도, 인덱스에 있는 값이라면 스토리지 엔진이 인덱스 안에서 미리 걸러낼 수 있다.**
> 이 작업을 MySQL 엔진이 아닌 스토리지 엔진에 "밀어내려(push down)" 보낸다고 해서 **Index Condition Pushdown** 입니다.
> 💡 **인덱스에 있는 컬럼이어야 한다는 점이 중요**
> 인덱스에 없는 컬럼의 조건은 ICP 대상이 아닙니다. 스토리지 엔진이 인덱스만 보고 판단할 수 없으니까요.
> 위 예시에서 `name`이 `(num, name)` 인덱스에 포함되어 있어서 가능했던 겁니다.
---
## 4. EXPLAIN으로 ICP 식별하기
ICP가 적용됐는지 확인하는 방법은 EXPLAIN의 `Extra` 컬럼입니다.
```sql
EXPLAIN
SELECT *
FROM test_icp
WHERE num = 1 AND name LIKE '%q';
```
| id | type | key | rows | filtered | Extra |
|---|---|---|---|---|---|
| 1 | ref | idx_num_name | 9 | 11.11 | **Using index condition** |
**`Using index condition`** — 이게 ICP의 명시적 신호입니다.
### 9편의 세 가지 키워드 다시 보기
9편에서 본 `Extra`의 세 가지 키워드를 ICP까지 포함해서 정리하면:
| Extra | 의미 | 비유 |
|---|---|---|
| `Using index` | 커버링 인덱스 — 인덱스만으로 답이 나옴 | 사전만 보고 끝 |
| `Using index condition` | ICP — 스토리지 엔진이 인덱스에서 추가 필터링 | 사전에서 걸러서 일부만 책장에 다녀옴 |
| `Using where` | MySQL 엔진이 사후 필터링 | 책장에서 다 가져온 다음 거르기 |
세 키워드가 단계적으로 이어집니다:
- 가장 빠른 건 `Using index` (테이블 안 다녀옴)
- 그 다음이 `Using index condition` (테이블에 적게 다녀옴)
- 가장 느린 건 `Using where` (다 다녀온 후 거름)
같은 쿼리도 인덱스 설계와 MySQL 버전에 따라 이 셋 중 어느 쪽이든 될 수 있습니다.
---
## 5. ICP를 끄고 비교해보기
ICP가 실제로 효과가 있는지 확인하려면, ICP를 꺼서 5.5 시절의 동작을 재현해볼 수 있습니다.
```sql
-- ICP 끄기
SET optimizer_switch = 'index_condition_pushdown=off';
```
이 상태에서 같은 쿼리를 EXPLAIN 해보면:
| id | type | key | rows | filtered | Extra |
|---|---|---|---|---|---|
| 1 | ref | idx_num_name | 9 | 11.11 | **Using where** |
`Extra`가 `Using index condition`에서 `Using where`로 바뀌었습니다. ICP 없이 MySQL 엔진이 사후 필터링을 한 모습입니다.
> 💡 **실무 팁**
> 일반적으로 ICP는 **켜두는 게 거의 항상 유리**합니다. MySQL 8.0에서는 기본 ON.
> 끄는 경우는 매우 특수한 케이스(특정 쿼리에서 옵티마이저가 잘못된 판단을 할 때)에 한정됩니다.
> 평소엔 신경 쓸 필요 없고, EXPLAIN에서 `Using index condition`이 보이면 "잘 동작하고 있구나" 정도로 이해하면 됩니다.
---
## 핵심 요약
이번 편에서 꼭 가져가야 할 한 가지:
> 🎯 **인덱스에 포함된 컬럼이라면, 인덱스 탐색 조건으로 못 쓰더라도 스토리지 엔진이 인덱스 안에서 미리 걸러줄 수 있다**
> 이게 ICP. EXPLAIN의 `Using index condition`으로 확인. MySQL 5.6+에서 기본 ON.
체크리스트:
- [x] MySQL이 MySQL 엔진과 스토리지 엔진의 두 층으로 동작한다는 걸 안다
- [x] EXPLAIN의 `Using index condition`을 보고 의미를 안다
- [ ] ICP가 없으면 발생하는 불필요한 랜덤 I/O 문제를 설명할 수 있다
- [ ] `Using index` / `Using index condition` / `Using where`의 차이를 안다
- [ ] 인덱스에 포함된 컬럼의 조건이어야 ICP가 적용된다는 제약을 안다
> 📝 체크리스트를 다 채울 자신이 있다면? [**인덱스 고급1 퀴즈 도전하기**](quiz.html?set=G)
---
## 이제 퀴즈에 도전하기
고급1 시리즈를 모두 마쳤습니다. 14편과 15편을 합쳐 고급1 퀴즈에 필요한 개념을 다뤘습니다.
- 인덱스의 정렬 성질을 ORDER BY에 그대로 활용 (14편)
- ASC는 정순 스캔, DESC는 역순 스캔 (`Backward index scan`) (14편)
- 정순이 역순보다 빠른 이유 + 8.0+ 내림차순 인덱스 (14편)
- MySQL의 두 층 구조와 ICP의 의미 (15편)
- ICP의 명시적 신호 `Using index condition` (15편)
> 🎯 [**인덱스 고급1 퀴즈 풀어보기**](quiz.html?set=G)
> 막히는 게 있다면 해당 학습자료의 섹션으로 돌아와 다시 읽어보세요.
> 고급1의 핵심은 **EXPLAIN의 `Extra` 컬럼이 들려주는 이야기를 끝까지 듣는 것** 입니다.
> `Using index`, `Using index condition`, `Using where`, `Backward index scan`, `Using filesort` — 이 다섯 키워드를 자신 있게 구분할 수 있다면 인덱스 진단의 90%는 됩니다.
---
## Reference
- MySQL 공식 문서: [Index Condition Pushdown Optimization](https://dev.mysql.com/doc/refman/8.4/en/index-condition-pushdown-optimization.html)
- MySQL 공식 문서: [EXPLAIN Output Format](https://dev.mysql.com/doc/refman/8.4/en/explain-output.html)
- 참고 글: [Index Condition Pushdown 이해하기 (jojoldu)](https://jojoldu.tistory.com/474)