MySQL 인덱스 최적화 전략 — EXPLAIN으로 쿼리 분석하기
[WHERE column = value]
|
+--> [Index lookup]
| |
| v
| small search range
|
+--> [Full table scan]
|
v
read every row
인덱스 튜닝의 핵심은 “인덱스를 만들면 빨라진다”가 아니라, 옵티마이저가 작은 탐색 범위로 좁혀 들어갈 수 있느냐입니다. 즉 인덱스는 자료구조 자체보다도, 쿼리가 얼마나 빠르게 후보 집합을 줄일 수 있는지의 문제로 이해하는 편이 훨씬 실무적입니다.
EXPLAIN으로 쿼리 분석
EXPLAIN SELECT * FROM users WHERE email = 'hoon@test.com';
| 컬럼 | 의미 |
|---|---|
| type | 접근 방식 (ALL > index > range > ref > const) |
| key | 사용된 인덱스 |
| rows | 예상 스캔 행 수 |
| Extra | Using index (커버링), Using filesort (정렬 필요) |
인덱스 생성
-- 단일 인덱스
CREATE INDEX idx_email ON users(email);
-- 복합 인덱스 (순서가 중요!)
CREATE INDEX idx_status_created ON orders(status, created_at);
-- 유니크 인덱스
CREATE UNIQUE INDEX idx_unique_email ON users(email);
복합 인덱스 설계 원칙
-- ✅ 올바른 순서 — 선택도 높은 컬럼 먼저
CREATE INDEX idx_status_date ON orders(status, order_date);
SELECT * FROM orders WHERE status = 'PAID' AND order_date >= '2026-01-01';
-- ❌ 인덱스 미사용 — 왼쪽 컬럼(status) 없이 검색
SELECT * FROM orders WHERE order_date >= '2026-01-01';
커버링 인덱스
-- 인덱스만으로 쿼리 완성 — 테이블 조회 없음
CREATE INDEX idx_covering ON posts(user_id, title, created_at);
SELECT user_id, title, created_at FROM posts WHERE user_id = 1;
-- Extra: Using index ✅
인덱스가 안 타는 경우
-- ❌ 함수 사용
WHERE YEAR(created_at) = 2026
-- ✅ 범위로 변환
WHERE created_at BETWEEN '2026-01-01' AND '2026-12-31'
-- ❌ LIKE 앞에 와일드카드
WHERE name LIKE '%hoon%'
-- ✅ 뒤에만 와일드카드
WHERE name LIKE 'hoon%'
-- ❌ 형변환
WHERE user_id = '123' -- user_id가 INT인 경우
-- ✅
WHERE user_id = 123
슬로우 쿼리 로그
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 1초 이상
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
운영 환경에서 어려워지는 지점
- MySQL 인덱스 최적화는 기계적으로 인덱스를 추가하는 일이 아니라 workload 선택도와 접근 경로를 이해하는 일이다.
- 인덱스가 있어도 predicate 순서와 covering이 맞지 않으면 느린 쿼리는 그대로 느릴 수 있다.
- 쓰기 비중이 높은 시스템은 인덱스 확산으로 크게 고통받을 수 있다.
중요한 아키텍처 결정
WHERE,ORDER BY, 조인 형태를 함께 보고 인덱스를 설계한다.- 겹치는 단일 목적 인덱스 여러 개보다 잘 맞춘 복합 인덱스 몇 개를 선호한다.
EXPLAIN과 운영에 가까운 cardinality로 가정을 검증한다.
실무 예시
인덱스는 옵티마이저가 실제로 활용할 수 있는 쿼리 경로와 맞아야만 가치가 있다.
WHERE tenant_id = ? AND deleted = 0 ORDER BY created_at DESC
-> INDEX (tenant_id, deleted, created_at)
피해야 할 안티패턴
- 복합 인덱스의 left-prefix 동작을 무시하는 것.
- 사소한 변형만 있는 중복 인덱스를 계속 추가하는 것.
- 로컬 테스트 데이터 cardinality가 운영 실행 계획을 예측한다고 믿는 것.
운영 체크리스트
- slow query log를 정기적으로 본다.
- 실제 cardinality와 filtered rows를 확인한다.
- 기능 변경 뒤 중복 인덱스를 정리한다.
- 인덱스 추가 뒤 쓰기 지연을 측정한다.
최종 판단
MySQL 인덱스 튜닝은 workload 매칭 작업이다. 보통 이기는 전략은 더 많은 인덱스가 아니라 더 적고 더 잘 맞는 인덱스와 더 나은 쿼리 형태다.
Continue Reading
다음으로 읽기 좋은 글
SQL 성능 최적화 실전 가이드
SQL 튜닝을 문장 다듬기가 아니라 workload 설계 문제로 봅니다. execution plan을 읽고, 데이터 접근량을 줄이고, 인덱스를 정직하게 설계하는 실무 기준을 정리합니다.
🗄️ Database쿼리 플랜 회귀를 막는 데이터베이스 가드
인덱스 변경, 통계 갱신, 배포 이후 쿼리 실행 계획이 나빠지는 문제를 사전에 감지하는 방법을 정리합니다.
🖥️ FrontendCore Web Vitals 최적화 — LCP, CLS, INP 실전 가이드
Core Web Vitals를 체크리스트 수준이 아니라 사용자 체감 성능과 렌더링 구조의 관점에서 정리합니다. LCP, CLS, INP가 왜 나빠지는지, 무엇부터 측정하고 어떤 순서로 최적화해야 하는지 실무 예제로 설명합니다.
📈 최신 동향PostgreSQL 18 최신 동향: 실무에서 진짜 중요한 변화
PostgreSQL 18은 단순 업그레이드 뉴스가 아닙니다. AIO, skip scan, 업그레이드 후 성능 회복, OAuth, generated columns까지 운영팀과 개발팀 모두에게 영향이 큰 변화가 들어왔습니다.
다음 탐색