#6. 인덱스가 통하는 쿼리, 안 통하는 쿼리
되는 쿼리 vs 안 되는 쿼리 기준
# 인덱스가 통하는 쿼리, 안 통하는 쿼리
> 📘 **학습자료 6편 / 7편** | 연결 퀴즈: [**인덱스 활용1 풀러가기**](quiz.html?set=C)
> 같은 인덱스라도 쿼리가 어떻게 생겼느냐에 따라 활용 여부가 갈립니다.
> 어떤 쿼리에서 인덱스가 일하고, 어떤 쿼리에서 무시되는지 — 그 규칙을 정렬 원리로 풀어봅니다.
> 📚 **이전 편**: [5편 - 인덱스는 언제 반영될까: 즉시 vs 지연]
> 5편까지 인덱스의 구조와 동작 원리를 다뤘다면, 이번 편부터는 **실무에서 인덱스를 진짜 활용하는 법**으로 넘어갑니다.
## 이 자료를 다 읽으면 알게 되는 것
- **Left-Most 원칙**이 뭐고, 왜 그런지 (정렬 원리)
- `LIKE 'AB%'`는 인덱스를 타고, `LIKE '%AB'`는 못 타는 이유
- **동등조건(`=`)**은 왜 인덱스를 가장 잘 활용하는지
- **비동등조건(`!=`, `<>`)**은 왜 인덱스를 못 타는지
- **묵시적 형변환**의 함정 — 인덱스를 탈 수도, 못 탈 수도 있는 미묘한 경계
---
## 📑 목차
- [1. 인덱스가 통하는 쿼리의 규칙: Left-Most 원칙](#1-인덱스가-통하는-쿼리의-규칙-left-most-원칙)
- [2. 단일 컬럼에서의 Left-Most](#2-단일-컬럼에서의-left-most)
- [3. 인덱스를 못 타는 LIKE 패턴](#3-인덱스를-못-타는-like-패턴)
- [4. 동등조건: 인덱스가 가장 잘 통하는 쿼리](#4-동등조건-인덱스가-가장-잘-통하는-쿼리)
- [5. 비동등조건: 인덱스를 써도 소용없는 쿼리](#5-비동등조건-인덱스를-써도-소용없는-쿼리)
- [6. 숨겨진 함정: 묵시적 형변환](#6-숨겨진-함정-묵시적-형변환)
- [핵심 요약](#핵심-요약)
- [다음 편 예고](#다음-편-예고)
---
## 1. 인덱스가 통하는 쿼리의 규칙: Left-Most 원칙
개발자가 가장 자주 마주치는 당황스러운 순간:
> "분명히 인덱스 걸었는데, 왜 쿼리가 안 빨라지지?"
이 질문에 답하려면 먼저 다음 원칙을 이해해야 됩니다.
### Left-Most 원칙
> **인덱스 키 컬럼은 왼쪽부터 차례대로(Left-Most) 정렬되어 있다.**
> **따라서 인덱스의 정렬 순서와 동일한 순서로 탐색 조건이 주어져야 인덱스 활용이 가능하다.**
이 한 문장이 활용1 세트 7문제 중 4문제의 답을 결정합니다. 천천히 풀어봅시다.
---
## 2. 단일 컬럼에서의 Left-Most
먼저 가장 단순한 케이스. 단일 컬럼 인덱스를 가진 테이블입니다.
```sql
CREATE TABLE book (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
word VARCHAR(255) NOT NULL,
INDEX idx_word (word)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
`word` 컬럼에 인덱스를 걸었습니다. 이 인덱스의 리프 노드는 **단어들을 ABC 순으로 정렬**해서 들고 있습니다.

여기서 핵심은 **"왼쪽 글자부터 정렬되어 있다"** 는 점입니다. 사전을 떠올려보세요. `APPLE`, `BANANA`, `CHERRY`...는 모두 첫 글자 기준으로 묶이고, 같은 첫 글자 안에서는 두 번째 글자, 세 번째 글자 순으로 정렬됩니다.
### Left-Most 조건이 만족될 때 — `LIKE 'E%'`
E로 시작하는 단어를 모두 찾는 쿼리:
```sql
SELECT * FROM book WHERE word LIKE 'E%';
```
이건 **인덱스를 탈 수 있습니다.** 단어들이 왼쪽 글자부터 정렬되어 있으니, "E로 시작하는 첫 단어"를 빠르게 찾고 거기서부터 옆으로 쭉 따라가면 됩니다.

EXPLAIN 결과로도 확인됩니다:
```sql
EXPLAIN SELECT * FROM person WHERE name LIKE 'A%';
```
| id | select_type | table | type | key | rows | Extra |
|----|-------------|-------|------|-----|------|-------|
| 1 | SIMPLE | p | **range** | idx_name | 378 | Using index condition |
`type = range`, `key = idx_name`. 인덱스를 활용한 범위 스캔이 일어났습니다.
> 💡 **복합 컬럼 인덱스에서도 Left-Most는 같은 원리**
> `(name, age)`로 복합 인덱스를 만들면, 데이터는 **name 기준으로 먼저 정렬**되고, name이 같은 것끼리만 age로 정렬됩니다.
> 그래서 `WHERE name LIKE '박%' AND age < 20`은 인덱스를 잘 활용하지만, `WHERE age < 20`만 있으면 (name 조건이 없으면) 정렬의 좌측 기준이 빠진 셈이라 인덱스 활용이 어려워집니다.
> 복합 컬럼 인덱스는 활용2에서 더 깊이 다룹니다.
> 💡 여기까지 읽었다면 Left-Most 원칙을 충분히 이해한 것입니다. [**퀴즈로 확인해보세요!**](quiz.html?set=C)
---
## 3. 인덱스를 못 타는 LIKE 패턴
이번엔 정반대의 쿼리. **"E로 끝나는 단어를 찾아라"**
```sql
SELECT * FROM book WHERE word LIKE '%E';
```
같은 인덱스, 비슷해 보이는 쿼리. 그런데 이건 **인덱스를 탈 수 없습니다.**

왜일까요? 인덱스 정렬은 **왼쪽 글자 기준**입니다. "E로 끝나는 단어"는 정렬 순서로 묶이지 않습니다.

E로 끝나는 단어는 인덱스 곳곳에 흩어져 있습니다.
- `A`로 시작하는 페이지에 `APPLE`, `ANGLE`...
- `E`로 시작하는 페이지에 `EXAMPLE`, `EXERCISE`...
- `F`로 시작하는 페이지에 `FILE`, `FIRE`...
결국 **모든 페이지를 다 뒤져봐야** 합니다. 이러면 인덱스를 쓰는 의미가 없죠. 그래서 옵티마이저는 **풀 테이블 스캔**을 선택합니다.
```sql
EXPLAIN SELECT * FROM person WHERE name LIKE '%A';
```
| id | select_type | table | type | key | rows | Extra |
|----|-------------|-------|------|-----|------|-------|
| 1 | SIMPLE | p | **ALL** | null | 9921 | Using where |
`type = ALL`, `key = null`. 풀 스캔입니다.
> ⚠️ **`LIKE` 와일드카드 위치 정리**
> - `LIKE 'AB%'` → 인덱스 활용 ✅ (왼쪽 고정)
> - `LIKE '%AB'` → 인덱스 활용 ❌ (왼쪽이 와일드카드)
> - `LIKE '%AB%'` → 인덱스 활용 ❌ (양쪽 와일드카드)
>
> 검색 기능 만들 때 `%검색어%` 형태로 쓰면 인덱스가 무용지물이 됩니다.
> 이런 경우엔 풀텍스트 검색이나 별도 검색 엔진을 고려해야 합니다.
---
## 4. 동등조건: 인덱스가 가장 잘 통하는 쿼리
LIKE만큼 자주 쓰는 게 동등조건(`=`)입니다. 결론부터 말하면, **동등조건은 인덱스를 가장 잘 활용할 수 있는 조건**입니다.
```sql
SELECT * FROM person WHERE name = 'Coli';
```
이 쿼리가 실행될 때 InnoDB가 하는 일:

1. B+Tree를 따라 내려가 `'Coli'`가 처음 나오는 리프 노드를 찾는다
2. 리프 노드에서 `'Coli'` 값을 가진 행을 옆으로 쭉 스캔한다 (동명이인이 있을 수 있으니까)
3. 각 행의 클러스터링 키로 실제 데이터에 접근해 최종 레코드를 읽는다

EXPLAIN 결과:
```sql
EXPLAIN SELECT * FROM person WHERE name = 'Coli';
```
| id | select_type | table | type | key | ref | rows | Extra |
|----|-------------|-------|------|-----|-----|------|-------|
| 1 | SIMPLE | p | **ref** | idx_name | **const** | 1 | null |
여기서 두 컬럼이 핵심입니다.
- **`type = ref`**: 인덱스를 활용해 **특정 값과 일치하는 행**을 찾았다는 뜻
- **`ref = const`**: 비교 대상이 **상수값**(여기선 `'Coli'`)이라는 뜻
`type`의 값에는 위계가 있습니다. 좋은 순서로:
```
const > eq_ref > ref > range > index > ALL
```
`ref`는 동등조건에서 일반적으로 나오는 값으로, 꽤 좋은 등급입니다.
---
## 5. 비동등조건: 인덱스를 써도 소용없는 쿼리
동등조건의 반대편엔 비동등조건(`!=`, `<>`)이 있습니다. 결론부터 말하면, **비동등조건은 인덱스를 거의 활용하지 못합니다.**
```sql
SELECT * FROM person WHERE name != 'Ever';
```

생각해봅시다. "이름이 Ever가 아닌 모든 행"은 사실상 **거의 모든 행**입니다. 인덱스로 접근하려 해도, 결국 전체를 다 봐야 합니다.
만약 인덱스를 활용한다면 다음 2단계를 거쳐 오히려 **더 느린 탐색**이 됩니다:
1. 모든 인덱스 키를 탐색하며 "Ever가 아닌지" 검증
2. Ever가 아닌 행마다 클러스터링 키로 데이터 페이지에 랜덤 I/O 접근

**(인덱스 → 랜덤 I/O) 두 단계** 보다 **풀 테이블 스캔(데이터 한 번만 순차 읽기)** 가 더 빠르다는 게 옵티마이저의 판단입니다.
```sql
EXPLAIN SELECT * FROM person WHERE name != 'Ever';
```
| id | select_type | table | type | possible_keys | key | rows | Extra |
|----|-------------|-------|------|---------------|-----|------|-------|
| 1 | SIMPLE | p | **ALL** | idx_name | **null** | 9921 | Using where |
주목할 점: `possible_keys = idx_name` (사용 가능한 인덱스는 있음). 그런데 `key = null` (실제론 안 씀).
**옵티마이저가 인덱스를 알지만 의도적으로 안 쓴 것**입니다. 풀 스캔이 더 빠르다고 판단했기 때문입니다.
> 💡 **"인덱스가 있어도 안 쓰는 경우가 있다"는 것은 중요한 통찰**
> 신입 개발자는 종종 "인덱스 걸었으니 빠를 것"이라고 가정합니다.
> 하지만 옵티마이저는 비용을 계산해 가장 빠른 길을 선택하며, 때로는 인덱스를 무시하는 게 더 빠릅니다.
> 다음 편(7편)에서 다룰 **30% 임계점**도 같은 맥락입니다.
---
## 6. 숨겨진 함정: 묵시적 형변환
마지막으로, 가장 미묘한 케이스. 신입 개발자가 모르고 지나치는 함정입니다.
다음 쿼리를 보세요. `age`는 `INT` 컬럼이고, 인덱스가 걸려 있다고 가정합니다.
```sql
SELECT * FROM student WHERE age = '30'; -- 문자열 '30'으로 비교
```
타입이 맞지 않습니다. 컬럼은 `INT`인데 값은 `VARCHAR`. 이래도 될까요?
### 결론: 잘 작동합니다 (이 케이스에서는)
MySQL은 호환되지 않는 타입이 만나면 **묵시적 형변환(implicit type conversion)** 을 시도합니다. 이 경우 문자열 `'30'`이 정수 `30`으로 변환되어, 결과적으로 다음 쿼리와 동일하게 실행됩니다.
```sql
SELECT * FROM student WHERE age = 30;
```
EXPLAIN으로 확인:
```sql
EXPLAIN SELECT * FROM student WHERE age = '30';
```
| id | select_type | table | type | key | ref | rows |
|----|-------------|-------|------|-----|-----|------|
| 1 | SIMPLE | student | **ref** | idx_student_age | **const** | 1 |
`type = ref`, `key = idx_student_age`. **인덱스를 잘 활용**했습니다.
### 그런데 — 반대 케이스는 위험합니다
방금은 운이 좋은 케이스였습니다. 다음을 보세요. `phone`이 **문자열 컬럼**이고 인덱스가 걸려 있습니다.
```sql
-- phone은 VARCHAR. 그런데 숫자로 비교
SELECT * FROM users WHERE phone = 01012345678;
```
이때 MySQL은 **컬럼 쪽**을 숫자로 변환하려 합니다. 즉 모든 행의 `phone`을 숫자로 바꿔서 비교하는 셈입니다.
이게 왜 문제냐면:
- 인덱스는 **원본 문자열**로 정렬되어 있는데
- 비교는 **변환된 숫자**로 일어남
- → 인덱스의 정렬 순서를 활용할 수 없음
- → 풀 테이블 스캔으로 떨어짐
**왜 두 케이스가 다를까?** MySQL의 형변환 규칙 때문입니다. 일반적으로 **문자열을 숫자로** 변환하지, **숫자 컬럼 전체를 문자열로** 변환하지는 않습니다. 그래서:
| 케이스 | 컬럼 타입 | 비교값 | 결과 |
|---|---|---|---|
| Case A | `INT` | `'30'` (문자열) | ✅ 비교값을 숫자로 변환 → 인덱스 활용 |
| Case B | `VARCHAR` | `30` (숫자) | ❌ 컬럼을 숫자로 변환 → 인덱스 무력화 |
> ⚠️ **실무 권장: 묵시적 형변환에 의존하지 말 것**
> 운 좋게 작동할 수도, 인덱스가 무력화될 수도 있는 경계가 미묘합니다.
> 그리고 무엇보다 **코드 리뷰에서 의도가 모호해 보입니다.**
>
> 권장 방법:
> - 비교 값의 타입을 컬럼 타입과 **명시적으로 일치**시킨다
> - 애플리케이션 레이어에서 타입을 통제한다 (Spring/JPA 등은 보통 자동 처리하지만, 직접 SQL을 짜는 곳에서 주의)
> - 의심되면 `EXPLAIN`으로 확인한다
---
## 핵심 요약
이번 편에서 꼭 가져가야 할 한 가지:
> 🎯 **인덱스가 일하려면 "쿼리가 정렬 순서를 따라가야" 한다**
> 인덱스는 단순한 자료구조가 아니라 "왼쪽부터 정렬된 사본"입니다.
> 쿼리가 그 정렬 순서를 활용할 수 있게 짜여있어야 인덱스가 의미가 있습니다.
체크리스트:
- [x] **Left-Most 원칙**이 무엇인지, 왜 그런지 설명할 수 있다
- [x] `LIKE 'AB%'`는 인덱스 활용 가능, `LIKE '%AB'`는 불가능한 이유를 안다
- [ ] 동등조건(`=`)이 EXPLAIN에서 `type = ref`, `ref = const`로 나타나는 의미를 안다
- [ ] 비동등조건(`!=`)이 인덱스를 못 쓰는 이유를 설명할 수 있다 (풀 스캔이 더 빠르기 때문)
- [ ] `type` 값의 위계를 안다: `const > eq_ref > ref > range > index > ALL`
- [ ] 묵시적 형변환에서 **안전한 케이스와 위험한 케이스**를 구분할 수 있다
> 📝 체크리스트를 다 채울 자신이 있다면? [**인덱스 활용1 퀴즈 도전하기**](quiz.html?set=C)
---
## 다음 편 예고
이번 편의 5번 섹션에서 잠깐 등장한 표현이 있습니다.
> "옵티마이저는 풀 스캔이 더 빠르면 인덱스가 있어도 안 쓴다"
다음 편에서는 이 통찰을 깊이 파고듭니다. **인덱스 레인지 스캔**이 어떻게 동작하는지, 그리고 옵티마이저가 인덱스 사용을 포기하는 **임계점**(흔히 말하는 "30% 룰")을 알아봅니다.
> "인덱스를 탔는데도 왜 느릴 수 있을까?"
> 답은 **Random I/O** 라는 보이지 않는 비용에 있습니다.
---
## Reference
- MySQL 공식 문서: [Type Conversion in Expression Evaluation](https://dev.mysql.com/doc/refman/8.4/en/type-conversion.html)
- MySQL 공식 문서: [EXPLAIN Output Format](https://dev.mysql.com/doc/refman/8.4/en/explain-output.html)