#17. 인덱스 머지
옵티마이저는 인덱스를 합쳐 쓴다
# 인덱스 머지 — 옵티마이저는 인덱스를 합쳐 쓴다
> 📘 **학습자료 18편 / H세트 3편** | 연결 퀴즈: [**인덱스 고급2 풀러가기**](quiz.html?set=H)
> "쿼리 하나에 인덱스 하나"가 기본이지만, 옵티마이저는 때때로 **여러 인덱스를 동시에** 사용합니다.
> 이걸 **인덱스 머지(Index Merge)** 라 부르고, 교집합과 합집합 두 가지 패턴이 있습니다.
> 📚 **이전 편**: [17편 - 함수 기반 인덱스]
> 8편(여러 조건이 결합된 쿼리에서 인덱스)에서 복합 인덱스의 동작을 다뤘습니다.
> 그런데 복합 인덱스가 없고 단일 인덱스만 따로 있다면? 옵티마이저는 어떤 선택을 할까요?
> 그 답이 인덱스 머지입니다.
## 이 자료를 다 읽으면 알게 되는 것
- **인덱스 머지(Index Merge)** 가 무엇이고, 왜 만들어졌는지
- **교집합(Intersection)** 머지가 적용되는 세 가지 조건
- **합집합(Union)** 머지가 적용되는 세 가지 조건
- EXPLAIN에서 `Using intersect`, `Using union` 을 식별하는 법
- 인덱스 머지가 항상 좋은 건 아닌 이유와 옵티마이저가 잘못 판단할 수 있는 사례
---
## 📑 목차
- [1. 출발점: 단일 인덱스의 한계](#1-출발점-단일-인덱스의-한계)
- [2. 인덱스 머지의 세 가지 종류](#2-인덱스-머지의-세-가지-종류)
- [⭐ 3. 교집합 머지: AND 조건의 최적화](#3-교집합-머지-and-조건의-최적화)
- [⭐ 4. 합집합 머지: OR 조건의 최적화](#4-합집합-머지-or-조건의-최적화)
- [5. 인덱스 머지의 한계 — 옵티마이저가 틀릴 때](#5-인덱스-머지의-한계--옵티마이저가-틀릴-때)
- [6. 인덱스 머지를 막거나 유도하기](#6-인덱스-머지를-막거나-유도하기)
- [핵심 요약](#핵심-요약)
---
## 1. 출발점: 단일 인덱스의 한계
다음 쿼리를 봅시다.
```sql
SELECT * FROM employees
WHERE first_name = 'Georgi'
AND emp_no BETWEEN 10000 AND 20000;
```
테이블에는 두 개의 단일 인덱스가 있습니다:
- `ix_firstname` (first_name 단일 인덱스)
- `PRIMARY` (emp_no 클러스터링 인덱스)
복합 인덱스 `(first_name, emp_no)` 는 없는 상황입니다.
옵티마이저는 어떻게 처리할까요? 8편에서 배운 대로라면 두 가지 선택지가 있습니다:
**선택지 A**: `
ix_firstname`으로 'Georgi' 인 사람 3,000명을 찾고, 그중 emp_no 조건을 만족하는 사람을 필터링
**선택지 B**:
PK 인덱스로 emp_no 10000~20000 인 직원 10,000명을 찾고, 그중 first_name 조건을 만족하는 사람을 필터링
어느 쪽이든 한쪽 인덱스로 거른 결과가 수천~수만 건이고, 그걸 또 일일이 필터링해야 합니다.
그런데 **둘 다 만족하는 행은 고작 10여 건**일 수 있습니다. 그렇다면 옵티마이저가 더 영리한 카드를 꺼낼 수 있을까요?
---
## 2. 인덱스 머지의 세 가지 종류
> ⭐ **인덱스 머지의 핵심 아이디어:**
>
> **여러 인덱스로 각각 검색한 뒤, 그 결과를 집합 연산으로 합친다.**
MySQL의 인덱스 머지는 세 가지 종류가 있습니다.
| 종류 | 조건 결합 | 동작 |
|---|---|---|
| Intersection (교집합) | AND | 각 인덱스 결과의 **교집합** 반환 |
| Union (합집합) | OR | 각 인덱스 결과의 **합집합** 반환 |
| Sort-Union | OR (범위 조건 포함) | 정렬 후 합집합 |
이번 편에서는 가장 자주 보게 될 **Intersection** 과 **Union** 을 중심으로 다룹니다.
---
## 3. 교집합 머지: AND 조건의 최적화
> ⭐ **이 자료에서 가장 중요한 섹션 ①**
### 적용 조건
옵티마이저는 다음 **세 조건**이 모두 만족될 때 교집합 머지를 시도합니다.
| 조건 | 의미 |
|---|---|
| ① AND로 결합된 조건 | 여러 조건이 AND로 묶여 있어야 함 |
| ② 각 조건마다 인덱스가 있음 | 각 조건이 단일 인덱스로 검색 가능해야 함 |
| ③ 교집합 비용이 단일 인덱스 + 필터링보다 저렴 | 옵티마이저의 비용 판단 |
### 동작 방식
위에서 본 쿼리를 다시 봅시다:
```sql
SELECT * FROM employees
WHERE first_name = 'Georgi'
AND emp_no BETWEEN 10000 AND 20000;
```
옵티마이저는 이렇게 생각합니다:
> "first_name 조건은 ix_firstname으로 PK 리스트 3,000개를 얻을 수 있고,
> emp_no 조건은 PRIMARY로 PK 리스트 10,000개를 얻을 수 있다.
> 이 두 PK 리스트의 **교집합** 을 구하면 최종 결과 PK 10여 개를 얻을 수 있다.
> 그러고 나서 그 PK들로만 실제 데이터를 가져오면, 단일 인덱스 + 필터링보다 훨씬 빠르겠네."
이게 교집합 머지의 핵심 아이디어입니다. **각 인덱스에서 PK 리스트만 가볍게 뽑아서 교집합을 구한 뒤, 최종 PK들로만 데이터를 읽는** 거죠.
### EXPLAIN으로 확인
```sql
EXPLAIN SELECT * FROM employees
WHERE first_name = 'Georgi'
AND emp_no BETWEEN 10000 AND 20000;
```
| id | type | key | Extra |
|---|---|---|---|
| 1 | **index_merge** | ix_firstname,PRIMARY | **Using intersect(ix_firstname,PRIMARY); Using where** |
세 가지 신호:
- `type: index_merge` — 인덱스 머지가 발생했다
- `key: ix_firstname,PRIMARY` — 두 개의 인덱스를 모두 사용했다
- `Extra: Using intersect(...)` — 교집합 머지다
> 💡 **자주 닿지 않는 직관**
> "인덱스 두 개를 동시에 쓴다고? 인덱스는 쿼리당 하나 아니었나?" 라고 생각할 수 있습니다.
> 일반적으로는 그게 맞습니다. 옵티마이저가 단일 인덱스 하나를 골라 쓰는 게 기본입니다.
> 하지만 두 인덱스를 합쳤을 때 비용이 더 작다고 판단하면 옵티마이저는 두 인덱스를 같이 씁니다. 인덱스 머지는 그 예외적인 경우입니다.
---
## 4. 합집합 머지: OR 조건의 최적화
> ⭐ **이 자료에서 가장 중요한 섹션 ②**
### 적용 조건
옵티마이저는 다음 **세 조건**이 모두 만족될 때 합집합 머지를 시도합니다.
| 조건 | 의미 |
|---|---|
| ① OR로 결합된 동등 조건 | 여러 조건이 OR로 묶여 있어야 함 |
| ② 각 조건마다 단일 인덱스가 있음 | 각 조건이 단일 인덱스로 검색 가능해야 함 |
| ③ 인덱스 스캔이 풀 테이블 스캔보다 충분히 선택적 | 결과가 일부에 한정되어야 함 |
### 동작 방식
다음 쿼리를 봅시다:
```sql
SELECT * FROM index_test
WHERE col1 = 1 OR col2 = 2;
```
`col1`, `col2` 각각에 단일 인덱스가 있다고 합시다.
만약 인덱스 머지가 없다면, OR 조건은 인덱스를 활용하기 어렵습니다. `col1 = 1` 인 행을 찾는 인덱스 검색과 `col2 = 2` 인 행을 찾는 인덱스 검색이 별개의 작업이기 때문이죠. 결국 풀 테이블 스캔으로 떨어지기 쉽습니다.
옵티마이저는 합집합 머지로 이렇게 처리합니다:
1. `idx_col1` 으로 `col1 = 1` 인 행의 PK 리스트 추출
2. `idx_col2` 로 `col2 = 2` 인 행의 PK 리스트 추출
3. 두 PK 리스트의 **합집합** 을 구함 (중복 제거)
4. 최종 PK들로 실제 데이터 읽기
> 💡 **합집합의 의미**
> 결과는 (A 결과) ∪ (B 결과) 입니다. 단순히 더하는 게 아니라 **중복 제거** 가 들어갑니다.
> 예: col1 = 1 인 행이 5,000개, col2 = 2 인 행이 7,000개 일 때, 그중 둘 다 만족하는 행이 1,000개라면 최종 결과는 5,000 + 7,000 - 1,000 = **11,000개**.
### EXPLAIN으로 확인
```sql
EXPLAIN SELECT * FROM index_test
WHERE col1 = 1 OR col2 = 2;
```
| id | type | key | Extra |
|---|---|---|---|
| 1 | **index_merge** | idx_col1,idx_col2 | **Using union(idx_col1,idx_col2); Using where** |
신호 패턴은 교집합과 거의 같지만 `Using union` 이라는 점이 다릅니다.
### 교집합과 합집합 한눈에 비교
| 항목 | 교집합 (Intersection) | 합집합 (Union) |
|---|---|---|
| 결합 연산자 | AND | OR |
| EXPLAIN Extra | `Using intersect(...)` | `Using union(...)` |
| 결과 | 모든 조건을 만족하는 행 | 어느 하나라도 만족하는 행 |
| 효과 | 결과가 매우 적을 때 큰 이득 | 풀 스캔을 피할 때 큰 이득 |
---
## 5. 인덱스 머지의 한계 — 옵티마이저가 틀릴 때
인덱스 머지는 강력하지만, **항상 좋은 선택은 아닙니다.**
옵티마이저는 히스토그램 통계나 샘플링으로 비용을 추정합니다. 통계가 부정확하거나 데이터 분포가 특이하면 옵티마이저의 판단이 틀릴 수 있습니다.
### 옵티마이저가 잘못 판단하는 시나리오
```sql
-- 통계상 일치 행이 적을 것 같은데, 실제로는 매우 많을 때
SELECT * FROM big_table
WHERE col_a = 'common_value' AND col_b = 'common_value';
```
옵티마이저는 "둘 다 만족하는 행이 적을 것"이라 추정하고 교집합 머지를 선택했습니다. 하지만 실제로는 두 조건 모두 흔한 값이라 결과가 수백만 건이라면?
이 경우 인덱스 머지의 오버헤드 (두 인덱스 검색 + 교집합 연산) 가 단일 인덱스 + 필터링보다 더 비싸질 수 있습니다.
> ⚠️ **실제 인덱스 머지가 비효율적이었던 사례**
> Mattermost는 [인덱스 머지 교집합이 오히려 성능을 떨어뜨린 사례](https://mattermost.com/blog/tuning-mysql-and-the-ghost-of-index-merge-intersection/)를 블로그에 공유했습니다. 옵티마이저가 비용을 잘못 추정해 인덱스 머지를 골랐는데, 실제 측정에서는 일반 인덱스 스캔이 훨씬 빨랐다는 내용입니다.
### 더 좋은 대안: 복합 인덱스
인덱스 머지가 자주 일어나는 쿼리라면, 보통은 **복합 인덱스를 만드는 게 더 좋습니다.**
위 쿼리의 경우 복합 인덱스 `(first_name, emp_no)` 를 만들면 인덱스 한 번의 탐색으로 끝납니다. 두 인덱스를 따로 검색하고 교집합을 구하는 비용이 없어지죠.
> 💡 **인덱스 머지가 보이면 의심하라**
> EXPLAIN에서 `index_merge`가 자주 보이는 쿼리는, 옵티마이저가 "차선의 카드"를 꺼내고 있다는 신호일 수 있습니다. 그 쿼리에 맞는 복합 인덱스를 검토해보는 게 일반적인 튜닝 방향입니다.
> (8편의 복합 인덱스 설계 가이드 참고)
---
## 6. 인덱스 머지를 막거나 유도하기
### 인덱스 머지를 끄고 싶을 때
옵티마이저가 인덱스 머지를 잘못 선택하고 있다고 판단되면, 옵티마이저 스위치로 끌 수 있습니다.
```sql
-- 세션 단위로 교집합 머지 끄기
SET optimizer_switch = 'index_merge_intersection=off';
-- 모든 인덱스 머지 끄기
SET optimizer_switch = 'index_merge=off';
```
전역으로 끄는 것은 다른 쿼리에 영향을 주므로, 보통은 문제가 되는 쿼리에만 힌트로 적용하는 게 안전합니다.
```sql
-- 특정 쿼리에서만 특정 인덱스 강제
SELECT * FROM employees USE INDEX (ix_firstname)
WHERE first_name = 'Georgi'
AND emp_no BETWEEN 10000 AND 20000;
```
### 옵티마이저 스위치 조회
현재 어떤 옵티마이저 기능이 켜져있는지 확인:
```sql
SHOW VARIABLES LIKE 'optimizer_switch';
```
결과에서 `index_merge=on`, `index_merge_intersection=on`, `index_merge_union=on`, `index_merge_sort_union=on` 등을 확인할 수 있습니다.
---
## 핵심 요약
이번 편에서 꼭 가져가야 할 것들:
> 🎯 **인덱스 머지는 옵티마이저가 한 쿼리에 여러 인덱스를 동시에 사용하는 최적화다**
> 각 인덱스에서 PK 리스트를 뽑아 집합 연산(교집합/합집합)으로 합친다.
> 🎯 **교집합 머지는 AND 조건, 합집합 머지는 OR 조건에 적용된다**
> 둘 다 각 조건마다 단일 인덱스가 있어야 하고, 옵티마이저가 이 방식이 더 싸다고 판단해야 한다.
> 🎯 **인덱스 머지가 보이면 복합 인덱스를 검토하라**
> 인덱스 머지는 강력하지만 차선책일 때가 많다. 자주 일어나는 쿼리라면 복합 인덱스로 한 번에 처리하는 게 보통 더 빠르다.
체크리스트:
- [ ] 인덱스 머지가 무엇이고 왜 도입됐는지 설명할 수 있다
- [ ] 교집합 머지의 세 가지 적용 조건을 안다
- [ ] 합집합 머지의 세 가지 적용 조건을 안다
- [ ] EXPLAIN의 `Using intersect`, `Using union` 을 구분해서 읽을 수 있다
- [ ] 인덱스 머지가 항상 최선이 아닌 이유를 설명할 수 있다
- [ ] 인덱스 머지가 자주 보이는 쿼리에서 어떤 튜닝을 시도해야 하는지 안다
> 📝 체크리스트를 다 채울 자신이 있다면? [**인덱스 고급2 퀴즈 도전하기**](quiz.html?set=H)
---
## 이제 퀴즈에 도전하기
이번 편에서 익힌 개념들을 다시 정리하면:
- 인덱스 머지는 여러 인덱스로 각각 검색한 뒤 PK 리스트를 집합 연산으로 합치는 최적화
- 교집합 머지(intersection): AND 조건, 각각 단일 인덱스, 교집합 비용이 더 저렴할 때
- 합집합 머지(union): OR 조건, 각각 단일 인덱스, 풀 스캔보다 인덱스 스캔이 충분히 선택적일 때
- 옵티마이저의 비용 추정이 틀려서 인덱스 머지가 오히려 느릴 수 있음
- 자주 보이는 인덱스 머지는 복합 인덱스로 대체할 여지가 있음
H세트를 모두 마쳤습니다. 16편(페이지 분할), 17편(함수 기반 인덱스), 18편(인덱스 머지)을 통해 인덱스의 **고급 최적화 메커니즘** 을 살펴봤습니다.
> 🎯 [**인덱스 고급2 퀴즈 풀어보기**](quiz.html?set=H)
> H세트 문제들은 EXPLAIN 결과를 직접 읽고, 옵티마이저의 판단을 추론하는 능력을 묻습니다.
> 막히는 게 있다면 해당 학습자료의 섹션으로 돌아와 다시 읽어보면 됩니다.
---
## 다음 학습자료
H세트를 마치며 인덱스 시리즈는 마무리에 가까워졌습니다. 인덱스의 자료구조에서 시작해 옵티마이저의 비용 모델, 그리고 페이지 단위의 물리적 동작과 머지 최적화까지 — 인덱스를 보는 시야가 한 단계씩 깊어지셨을 것입니다.
다음 주제는 인덱스를 넘어선 또 다른 영역 — JOIN 최적화나 트랜잭션, 락 등을 다룰 예정입니다.
---
## Reference
- MySQL 공식 문서: [Index Merge Optimization](https://dev.mysql.com/doc/refman/8.4/en/index-merge-optimization.html)
- MySQL 공식 문서: [Index Merge Intersection](https://dev.mysql.com/doc/refman/8.4/en/index-merge-optimization.html#index-merge-intersection)
- MySQL 공식 문서: [Index Merge Union](https://dev.mysql.com/doc/refman/8.4/en/index-merge-optimization.html#index-merge-union)
- Mattermost: [Tuning MySQL and the Ghost of Index Merge Intersection](https://mattermost.com/blog/tuning-mysql-and-the-ghost-of-index-merge-intersection/)