C 세트

#6. 인덱스가 통하는 쿼리, 안 통하는 쿼리

되는 쿼리 vs 안 되는 쿼리 기준

학습자료를 불러오는 중...

#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 순으로 정렬**해서 들고 있습니다. ![이미지 인덱스 정렬](https://quiz-solution-images.s3.ap-northeast-2.amazonaws.com/quiz16/16-1.png) 여기서 핵심은 **"왼쪽 글자부터 정렬되어 있다"** 는 점입니다. 사전을 떠올려보세요. `APPLE`, `BANANA`, `CHERRY`...는 모두 첫 글자 기준으로 묶이고, 같은 첫 글자 안에서는 두 번째 글자, 세 번째 글자 순으로 정렬됩니다. ### Left-Most 조건이 만족될 때 — `LIKE 'E%'` E로 시작하는 단어를 모두 찾는 쿼리: ```sql SELECT * FROM book WHERE word LIKE 'E%'; ``` 이건 **인덱스를 탈 수 있습니다.** 단어들이 왼쪽 글자부터 정렬되어 있으니, "E로 시작하는 첫 단어"를 빠르게 찾고 거기서부터 옆으로 쭉 따라가면 됩니다. ![이미지 LIKE 'E%' 인덱스 활용](https://quiz-solution-images.s3.ap-northeast-2.amazonaws.com/quiz16/16-2.png) 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'; ``` 같은 인덱스, 비슷해 보이는 쿼리. 그런데 이건 **인덱스를 탈 수 없습니다.** ![이미지 LIKE '%E' 정렬](https://quiz-solution-images.s3.ap-northeast-2.amazonaws.com/quiz17/17-1.png) 왜일까요? 인덱스 정렬은 **왼쪽 글자 기준**입니다. "E로 끝나는 단어"는 정렬 순서로 묶이지 않습니다. ![이미지 LIKE '%E' 흩어진 결과](https://quiz-solution-images.s3.ap-northeast-2.amazonaws.com/quiz17/17-2.png) 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가 하는 일: ![이미지 동등조건 인덱스](https://quiz-solution-images.s3.ap-northeast-2.amazonaws.com/quiz18/18-1.png) 1. B+Tree를 따라 내려가 `'Coli'`가 처음 나오는 리프 노드를 찾는다 2. 리프 노드에서 `'Coli'` 값을 가진 행을 옆으로 쭉 스캔한다 (동명이인이 있을 수 있으니까) 3. 각 행의 클러스터링 키로 실제 데이터에 접근해 최종 레코드를 읽는다 ![이미지 동등조건 동작 과정](https://quiz-solution-images.s3.ap-northeast-2.amazonaws.com/quiz18/18-2.png) 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'; ``` ![이미지 비동등조건](https://quiz-solution-images.s3.ap-northeast-2.amazonaws.com/quiz19/19-1.png) 생각해봅시다. "이름이 Ever가 아닌 모든 행"은 사실상 **거의 모든 행**입니다. 인덱스로 접근하려 해도, 결국 전체를 다 봐야 합니다. 만약 인덱스를 활용한다면 다음 2단계를 거쳐 오히려 **더 느린 탐색**이 됩니다: 1. 모든 인덱스 키를 탐색하며 "Ever가 아닌지" 검증 2. Ever가 아닌 행마다 클러스터링 키로 데이터 페이지에 랜덤 I/O 접근 ![이미지 비동등조건 비효율](https://quiz-solution-images.s3.ap-northeast-2.amazonaws.com/quiz19/19-2.png) **(인덱스 → 랜덤 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)

이 세트 퀴즈 풀기 | 홈으로 돌아가기