1. 문서 제목
- 인덱스 레인지 스캔, 랜덤 I/O, 검색엔진 도입 기준 정리
2. 기술 개요 요약
인덱스 레인지 스캔은 B-Tree 계열 인덱스에서 특정 범위의 키를 찾아 순서대로 읽는 방식이다. WHERE age > 20, BETWEEN, LIKE 'abc%'처럼 범위 조건을 사용할 때 대표적으로 등장한다. 다만 InnoDB에서 secondary index를 사용할 경우 인덱스 리프에는 실제 row 전체가 아니라 primary key 값이 들어 있고, 이 primary key로 clustered index를 다시 찾아 실제 row를 읽는다. 이 과정에서 데이터 페이지 접근이 흩어지면 랜덤 I/O가 발생한다.
반면 쿼리에 필요한 컬럼이 모두 인덱스에 포함되어 있으면 테이블 row를 다시 읽지 않아도 되므로 covering index 효과를 얻을 수 있다. 텍스트 검색에서는 B-Tree 인덱스가 앞쪽 와일드카드 없는 LIKE 'keyword%'에는 활용될 수 있지만, LIKE '%keyword%'처럼 시작 지점을 특정할 수 없는 패턴에는 적합하지 않다. 데이터가 많고 자연어 검색 기능이 필요해지면 Elasticsearch 같은 검색엔진을 별도 조회 인덱스로 도입하는 것을 검토한다.
3. 핵심 기능/개념 정리
3-1. 인덱스 레인지 스캔
| 항목 | 설명 | 실무 포인트 |
|---|---|---|
| Index Range Scan | 인덱스에서 시작 지점을 찾고 조건을 만족하는 범위를 순서대로 읽는 방식 | >, <, BETWEEN, prefix LIKE 조건에서 자주 사용 |
| B-Tree/B+Tree | 정렬된 키를 기반으로 탐색과 범위 조회를 빠르게 수행하는 인덱스 구조 | MySQL 문서에서는 B-Tree라고 표현하지만 InnoDB 인덱스는 clustered/secondary 구조를 이해해야 함 |
| 시작 지점 탐색 | 조건을 만족하는 첫 key 위치를 찾는 과정 | 등호/범위 조건과 복합 인덱스 순서가 중요 |
| 범위 스캔 | 시작 지점 이후 조건을 만족하는 key들을 연속적으로 읽음 | 읽는 범위가 넓어지면 인덱스가 항상 유리하지 않을 수 있음 |
예시:
SELECT *
FROM users
WHERE age BETWEEN 20 AND 29;
age 인덱스가 있다면 MySQL은 age = 20 근처의 시작 지점을 찾고, age <= 29 범위까지 인덱스를 따라 읽을 수 있다.
3-2. InnoDB clustered index와 secondary index
| 구분 | 설명 | 실무 포인트 |
|---|---|---|
| Clustered Index | InnoDB 테이블에서 primary key를 기준으로 구성되는 인덱스 | 리프 노드에 row 데이터가 함께 저장됨 |
| Secondary Index | primary key가 아닌 컬럼에 만든 인덱스 | 리프 노드에 secondary key와 primary key 값이 저장됨 |
| Table Lookup | secondary index에서 얻은 primary key로 clustered index를 다시 탐색하는 과정 | 필요한 컬럼이 인덱스에 없으면 추가 row lookup 발생 |
| Covering Index | 쿼리에 필요한 컬럼이 모두 인덱스에 포함되어 테이블 row를 다시 읽지 않아도 되는 상태 | EXPLAIN의 Extra에 Using index가 표시될 수 있음 |
InnoDB에서는 secondary index record가 해당 row의 primary key 값을 포함한다. 따라서 secondary index만으로 필요한 데이터를 모두 해결할 수 없다면, primary key를 이용해 clustered index를 다시 찾아가야 한다.
3-3. 랜덤 I/O가 발생하는 이유
| 단계 | 동작 | I/O 특성 |
|---|---|---|
| 1. Secondary index range scan | secondary index에서 조건을 만족하는 key와 PK를 순서대로 읽음 | 인덱스 내부에서는 비교적 순차적 접근 |
| 2. PK 기반 clustered index lookup | 수집한 PK로 실제 row를 읽음 | PK 순서와 secondary key 순서가 다르면 페이지 접근이 흩어질 수 있음 |
| 3. 결과 row 반환 | 필요한 컬럼을 조합해 결과 생성 | row 수가 많을수록 lookup 비용 증가 |
예시:
SELECT *
FROM users
WHERE age >= 20;
age secondary index를 사용하면 age 순서로 PK를 모은다. 하지만 실제 row는 clustered index의 PK 순서로 저장되어 있으므로, age 순서와 PK 순서가 다르면 여러 데이터 페이지를 왔다 갔다 하게 된다. 이때 랜덤 I/O 비용이 커질 수 있다.
3-4. Covering Index
Covering index는 특정 쿼리에 필요한 모든 컬럼이 인덱스 안에 들어 있어, 테이블 row를 다시 읽지 않아도 되는 상태를 말한다.
CREATE INDEX idx_users_age_name ON users(age, name);
SELECT age, name
FROM users
WHERE age BETWEEN 20 AND 29;
위 쿼리는 age, name만 필요하고 두 컬럼이 모두 idx_users_age_name에 들어 있으므로 테이블 row lookup을 줄일 수 있다.
| 장점 | 주의점 |
|---|---|
| 랜덤 row lookup 감소 | 인덱스 컬럼이 많아지면 저장공간과 쓰기 비용 증가 |
| 읽기 성능 개선 가능 | 모든 쿼리를 covering index로 만들려고 하면 인덱스 난립 |
EXPLAIN으로 확인 가능 |
쿼리 패턴 기준으로 선별 필요 |
3-5. LIKE 검색과 B-Tree 인덱스
MySQL 문서 기준으로 B-Tree 인덱스는 LIKE 비교에서도 사용할 수 있지만, 조건 문자열이 와일드카드로 시작하지 않아야 한다.
| 패턴 | 예시 | 인덱스 활용 가능성 | 이유 |
|---|---|---|---|
| Prefix 검색 | LIKE 'kim%' |
가능 | 시작 문자열이 고정되어 범위 스캔 가능 |
| Suffix 검색 | LIKE '%kim' |
어려움 | 시작 지점을 특정할 수 없음 |
| Contains 검색 | LIKE '%kim%' |
어려움 | 앞부분이 열려 있어 B-Tree 정렬을 활용하기 어려움 |
LIKE 'kim%'는 대략 다음 범위 검색처럼 동작할 수 있다.
name >= 'kim' AND name < 'kin'
반면 LIKE '%kim%'는 문자열 중간에 kim이 포함되어 있는지를 봐야 하므로, B-Tree 인덱스의 정렬된 시작 지점을 활용하기 어렵다.
정확히 말하면 LIKE '%kim%'에서 B-Tree 인덱스가 "절대 사용되지 않는다"기보다, 문자열 시작점이 고정되지 않아 선택적인 range seek가 어렵다. 옵티마이저가 covering index scan 같은 형태를 선택할 수는 있지만, 검색어 위치를 이용해 좁은 범위만 바로 찾아가는 효과는 기대하기 어렵다.
3-6. 검색엔진과 역색인
Elasticsearch 같은 검색엔진은 텍스트를 분석기로 토큰화하고, 토큰별로 어떤 문서에 등장했는지 저장하는 inverted index를 사용한다.
| 개념 | 설명 | 실무 포인트 |
|---|---|---|
| Analyzer | 텍스트를 검색 가능한 token으로 변환하는 처리 파이프라인 | 언어별 분석기 선택이 중요 |
| Tokenizer | 텍스트를 token 단위로 나누는 구성요소 | 검색 품질의 출발점 |
| Inverted Index | term -> document list 형태의 색인 |
포함 검색과 relevance scoring의 기반 |
| Relevance Score | 검색어와 문서의 관련도를 점수화한 값 | 단순 포함 여부보다 검색 UX 개선 가능 |
| Nori Analyzer | Elasticsearch의 한국어 분석 플러그인 | 한국어 형태소 분석이 필요한 경우 검토 |
예시:
문서1: "빨간 사과"
문서2: "파란 하늘"
문서3: "빨간 자동차"
역색인:
"빨간" -> [문서1, 문서3]
"사과" -> [문서1]
"파란" -> [문서2]
"하늘" -> [문서2]
"자동차" -> [문서3]
3-7. RDBMS LIKE와 검색엔진 비교
| 항목 | RDBMS LIKE |
검색엔진 |
|---|---|---|
| 기본 목적 | 관계형 데이터 조건 검색 | 전문 검색, 검색 UX, 로그/문서 검색 |
| 주요 인덱스 | B-Tree | Inverted Index |
| Prefix 검색 | 비교적 적합 | 가능 |
| Contains 검색 | 데이터 증가 시 비효율적일 수 있음 | 상대적으로 적합 |
| 관련도 정렬 | 기본 기능 아님 | _score 기반 정렬 가능 |
| 형태소 분석 | 제한적 | analyzer 기반 처리 가능 |
| 오타/동의어/자동완성 | 직접 구현 필요 | 검색엔진 기능으로 구성 가능 |
| 정합성 | 원본 DB에서 즉시 조회 | 동기화 지연 가능 |
| 트랜잭션 | ACID 트랜잭션 가능 | RDBMS source of truth 대체 용도로 부적합 |
4. 사용 예시 및 코드 스니펫
4-1. 인덱스 레인지 스캔 예시
CREATE INDEX idx_users_age ON users(age);
EXPLAIN
SELECT *
FROM users
WHERE age BETWEEN 20 AND 29;
확인할 포인트:
| EXPLAIN 항목 | 의미 |
|---|---|
type = range |
범위 조건으로 인덱스를 사용 |
key = idx_users_age |
사용된 인덱스 |
rows |
MySQL이 읽을 것으로 추정한 row 수 |
Extra |
Using index, Using where 등 추가 실행 정보 |
4-2. Covering Index 예시
CREATE INDEX idx_users_age_name ON users(age, name);
EXPLAIN
SELECT age, name
FROM users
WHERE age BETWEEN 20 AND 29;
이 쿼리는 필요한 컬럼이 age, name뿐이고 둘 다 인덱스에 포함되어 있다. MySQL이 인덱스만으로 결과를 만들 수 있으면 EXPLAIN의 Extra에 Using index가 표시될 수 있다.
4-3. LIKE 검색 예시
CREATE INDEX idx_users_name ON users(name);
-- 인덱스 활용 가능성이 높음
SELECT *
FROM users
WHERE name LIKE 'kim%';
-- B-Tree 인덱스 활용이 어려움
SELECT *
FROM users
WHERE name LIKE '%kim%';
LIKE 'kim%'는 시작 문자열이 고정되어 있으므로 B-Tree 인덱스의 범위 스캔으로 처리될 수 있다. LIKE '%kim%'는 시작 지점을 특정할 수 없어 데이터가 많아질수록 풀스캔 또는 비효율적인 탐색으로 이어질 수 있다.
4-4. Elasticsearch 역색인 개념 예시
POST products/_doc/1
{
"name": "빨간 사과",
"description": "달콤한 국산 사과"
}
검색엔진은 name, description 같은 텍스트 필드를 분석해 token으로 나누고, 각 token이 어떤 문서에 등장했는지 inverted index에 저장한다.
검색 예시:
GET products/_search
{
"query": {
"multi_match": {
"query": "빨간 사과",
"fields": ["name^2", "description"]
}
}
}
이런 구조를 사용하면 단순 포함 여부뿐 아니라 필드 가중치, 관련도 점수, 형태소 분석, 동의어, 자동완성 등을 검색 요구사항에 맞게 조정할 수 있다.
5. 실제 사용 시 주의점 / Best Practice
5-1. 인덱스를 탄다고 항상 빠른 것은 아니다
읽어야 하는 row 비율이 높아지면 secondary index range scan 후 clustered index lookup을 반복하는 비용이 커질 수 있다. 이 경우 랜덤 I/O가 많아져 차라리 table scan이 더 유리할 수 있다. 실행 계획과 실제 실행 시간을 함께 봐야 한다.
5-2. SELECT *는 covering index를 방해하기 쉽다
필요한 컬럼만 조회하면 인덱스만으로 결과를 만들 가능성이 커진다. 반대로 SELECT *는 인덱스에 없는 컬럼까지 요구하므로 clustered index lookup을 유발하기 쉽다.
5-3. Secondary index는 primary key 크기의 영향을 받는다
InnoDB secondary index는 primary key 값을 함께 저장한다. 따라서 primary key가 길면 secondary index 크기도 커질 수 있다. UUID 같은 긴 PK를 사용할 때는 secondary index 저장공간과 캐시 효율을 함께 고려해야 한다.
5-4. LIKE '%keyword%'는 데이터가 작을 때는 괜찮아 보일 수 있다
작은 테이블에서는 문제가 드러나지 않지만 데이터가 커지면 contains 검색은 급격히 느려질 수 있다. 검색 요구사항이 커질 가능성이 있다면 초기에 검색 대상, 트래픽, 정렬 기준, 자동완성 필요 여부를 따져봐야 한다.
5-5. 검색엔진은 source of truth가 아니라 조회용 인덱스다
Elasticsearch 같은 검색엔진은 검색과 분석에 강하지만, 결제·재고·회원 상태 같은 강한 정합성이 필요한 원본 저장소를 대체하는 용도로 쓰면 안 된다. 일반적으로 RDBMS를 source of truth로 두고, 검색엔진은 비동기 동기화된 조회 모델로 운영한다.
5-6. 검색엔진 도입 전 동기화 전략을 먼저 정한다
검색엔진을 도입하면 RDBMS와 검색 인덱스 사이에 데이터 동기화 문제가 생긴다.
| 방식 | 설명 | 주의점 |
|---|---|---|
| Dual Write | 애플리케이션이 DB와 검색엔진에 동시에 기록 | 한쪽만 성공하는 partial failure 처리 필요 |
| CDC | DB 변경 로그를 기반으로 검색엔진에 반영 | Debezium 등 운영 복잡도 존재 |
| Batch Reindex | 주기적으로 전체 또는 일부 데이터를 재색인 | 실시간성이 낮고 대량 작업 비용 발생 |
| Outbox Pattern | DB 트랜잭션 안에 이벤트를 기록하고 별도 프로세스가 전달 | 구현 복잡도는 있지만 유실 방지에 유리 |
5-7. 한국어 검색은 analyzer 품질이 중요하다
한국어는 띄어쓰기와 형태소 문제가 있어 단순 공백 기준 tokenization으로는 검색 품질이 낮을 수 있다. Elasticsearch에서는 Nori analyzer 같은 한국어 분석기를 검토할 수 있고, 도메인 용어가 많다면 사용자 사전도 함께 관리해야 한다.
5-8. 검색엔진 도입은 기능 요구와 운영 비용을 함께 본다
검색엔진은 contains 검색, 형태소 분석, 관련도 정렬, 자동완성 같은 검색 UX에 강하지만, 색인 동기화와 장애 대응 비용이 생긴다. 단순 prefix 검색이나 작은 데이터셋이라면 RDBMS 인덱스와 쿼리 설계만으로 충분할 수 있다.
5-9. 면접 답변으로 압축하기
인덱스 레인지 스캔은 B-Tree 인덱스에서 시작 지점을 찾고 조건을 만족하는 범위를 순서대로 읽는 방식입니다. 다만 InnoDB secondary index는 실제 row 전체가 아니라 primary key 값을 가지고 있기 때문에, 필요한 컬럼이 인덱스에 없으면 PK로 clustered index를 다시 찾아가야 합니다. 이 table lookup 과정에서 데이터 페이지 접근이 흩어지면 랜덤 I/O가 발생합니다. 이를 줄이려면 필요한 컬럼을 인덱스에 포함한 covering index를 고려할 수 있습니다.
LIKE 'keyword%'는 시작 문자열이 고정되어 인덱스를 활용할 수 있지만,LIKE '%keyword%'는 시작점을 특정할 수 없어 대량 데이터에서는 비효율적입니다. 검색 대상이 크고 관련도 정렬, 형태소 분석, 오타 허용, 자동완성 같은 검색 UX가 필요하면 Elasticsearch 같은 검색엔진을 별도 조회 인덱스로 도입하되, 동기화 지연과 운영 복잡도를 감수해야 합니다.
검증 메모
- InnoDB secondary index가 primary key 값을 포함한다는 설명은 MySQL 공식 문서 기준으로 확인했다.
LIKE '%keyword%'는 선택적인 B-Tree range seek가 어렵다는 표현으로 보완했다.- Elasticsearch/Nori 설명은 Elastic 공식 문서 기준으로 검색용 색인과 원본 저장소 역할을 분리했다.
6. 참고자료 / 공식 문서 출처
- MySQL Reference Manual: Clustered and Secondary Indexes
https://dev.mysql.com/doc/refman/9.7/en/innodb-index-types.html - MySQL Reference Manual: How MySQL Uses Indexes
https://dev.mysql.com/doc/en/mysql-indexes.html - MySQL Reference Manual: B-Tree Index Characteristics
https://dev.mysql.com/doc/en/index-btree-hash.html - MySQL Reference Manual: Range Optimization
https://dev.mysql.com/doc/en/range-optimization.html - MySQL Reference Manual: EXPLAIN Output Format
https://dev.mysql.com/doc/en/explain-output.html - Elastic Docs: How full-text search works
https://www.elastic.co/docs/solutions/search/full-text/how-full-text-works - Elastic Docs: Text analysis
https://www.elastic.co/docs/manage-data/data-store/text-analysis - Elastic Docs: Korean (nori) analysis plugin
https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-nori - Elastic Docs: nori analyzer
https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-nori-analyzer
'DB > MySQL' 카테고리의 다른 글
| [MySQL] 인덱스 심화 — 10%룰, B+Tree 리밸런싱, 복합 인덱스 컬럼 순서 (0) | 2026.07.12 |
|---|---|
| DB 커넥션 풀 사이징과 데드락 — API가 느려질 때 원인을 좁히는 순서 (0) | 2026.07.12 |
| [MySQL] Replication과 InnoDB Cluster, Sharding은 무엇이 다를까? (0) | 2026.07.11 |
| [MySQL] SHOW PROCESSLIST와 DB 모니터링 메트릭 정리 (0) | 2026.07.10 |
| [MySQL] EXPLAIN 실행계획과 파티셔닝 정리 (0) | 2026.07.10 |
댓글