어제까지 빠르던 쿼리가 갑자기 느려졌습니다. 코드는 그대로인데 응답이 10배 느려졌다면, MySQL 옵티마이저가 다른 실행계획을 골랐을 가능성이 큽니다. 옵티마이저는 비용을 추정해 인덱스와 조인 순서를 자동으로 정하는데, 통계가 어긋나면 더 비싼 계획을 "싸다"고 착각합니다.
먼저 실제로 무엇을 골랐는지 확인합니다.
SQL
EXPLAIN SELECT * FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'paid' AND u.region = 'KR';
잘못된 계획의 신호
| 증상 | EXPLAIN에서 보이는 것 | 흔한 원인 |
|---|---|---|
| 엉뚱한 인덱스 | key가 기대와 다른 인덱스 | 통계 부정확 |
| 인덱스 무시 | key = NULL, type = ALL | 카디널리티 오판 |
| 잘못된 조인 순서 | 큰 테이블이 먼저 스캔됨 | rows 추정 오류 |
| 추정과 실제 괴리 | rows가 실제와 크게 다름 | 통계 오래됨 |
추정과 실제의 괴리는 EXPLAIN ANALYZE로 확인합니다. rows=100인데 실제 actual rows=50000이면 옵티마이저가 그 테이블을 과소평가한 것입니다.
순서대로 바로잡기
① 통계부터 갱신 — 힌트는 마지막 수단입니다. 대량 INSERT/DELETE 후에는 통계가 어긋나니 먼저 갱신합니다.
SQL
ANALYZE TABLE orders, users;
대부분의 "갑자기 느려짐"은 이 한 줄로 해결됩니다.
② 인덱스를 강제·차단 — 옵티마이저가 끝내 좋은 인덱스를 안 쓰면 강제합니다. 8.0부터는 옵티마이저 힌트가 권장됩니다.
SQL
SELECT /*+ INDEX(o idx_status_user) */ *
FROM orders o WHERE o.status = 'paid';
특정 인덱스를 못 쓰게 막을 때는 IGNORE INDEX 또는 /*+ NO_INDEX(...) */를 씁니다.
③ 조인 순서를 고정 — 큰 테이블을 먼저 읽어 비효율적일 때, JOIN_ORDER로 작은 결과부터 읽게 합니다.
SQL
SELECT /*+ JOIN_ORDER(u, o) */ *
FROM orders o JOIN users u ON u.id = o.user_id
WHERE u.region = 'KR';
u.region = 'KR'로 걸러진 적은 사용자부터 읽으면 검사 행 수가 크게 줄어듭니다.
요점 정리
- 갑자기 느려지면 코드보다 실행계획 변화를 의심한다.
ANALYZE TABLE로 통계를 갱신하는 게 1순위, 힌트는 마지막.- 추정
rows와EXPLAIN ANALYZE의 실제 행이 크게 다르면 통계 문제. - 힌트로 강제했다면 왜 필요했는지 주석으로 남겨, 데이터 분포가 바뀌면 다시 본다.
옵티마이저 힌트 전후의 실행계획 변화를 직접 측정하는 실습은 데이터베이스 트랙에서 회원가입 없이 무료로 할 수 있습니다.