MySQL 쿼리가 느릴 때 많은 사람이 SQL 문장부터 다시 씁니다. 그런데 실제로는 문장 자체보다 MySQL이 그 쿼리를 어떤 접근 경로로 실행하려는지를 먼저 보는 편이 훨씬 빠를 때가 많습니다. 그때 가장 먼저 열어야 하는 도구가 EXPLAIN입니다.
중요한 점은 EXPLAIN이 “이 SQL이 좋은가 나쁜가”를 채점하는 도구가 아니라는 것입니다. EXPLAIN은 어떤 테이블부터 읽고, 어떤 인덱스를 쓰고, 얼마나 많은 row를 읽을 것으로 예상하고, 정렬이나 임시 작업이 필요한지를 보여주는 실행 계획의 요약입니다.
이 글에서는 아래를 중심으로 정리합니다.
EXPLAIN이 실제로 무엇을 보여주는지- 어떤 컬럼부터 보면 되는지
type,possible_keys,key,key_len,rows,filtered,Extra를 어떻게 해석할지- 초보자가 무엇을 과하게 걱정하고, 무엇을 먼저 의심해야 하는지
- 가능하다면
EXPLAIN ANALYZE를 언제 같이 써야 하는지
한 줄 요약은 이렇습니다. 느린 SQL을 고칠 때는 쿼리 문장을 예쁘게 바꾸기보다, 실행 계획에서 “어디서 너무 많이 읽고, 어디서 잘못된 인덱스를 타고, 어디서 추가 작업이 생기는지”를 먼저 읽는 편이 훨씬 낫습니다.

Quick answer
실무에서는 아래 순서로 읽으면 됩니다.
EXPLAIN으로 어떤 테이블 순서와 접근 방식이 선택됐는지 봅니다.type으로 풀스캔이나 넓은 range 접근이 있는지 확인합니다.possible_keys,key,key_len로 기대한 인덱스가 실제로 선택됐는지 봅니다.rows와filtered로 읽는 양이 과한지 추정합니다.Extra에서Using filesort,Using temporary같은 추가 작업을 찾습니다.- 조인이 있다면 각 단계의 row 추정이 곱해지며 커지는지 봅니다.
- 가능하면 실제 데이터로 실행해 보고, MySQL 8 환경이라면
EXPLAIN ANALYZE도 함께 봅니다.
즉 EXPLAIN 읽기의 핵심은 “인덱스를 썼느냐”만 보는 게 아니라, 어떤 방식으로 얼마나 넓게 읽고 어떤 추가 비용이 붙는지를 같이 보는 것입니다.
1. EXPLAIN은 무엇을 보여주나
EXPLAIN 은 MySQL이 쿼리를 실행할 때 선택한 접근 계획을 요약해서 보여줍니다.
예를 들어:
EXPLAIN
SELECT id, created_at, total_amount
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 20;
이 결과를 통해 보통 아래를 추정할 수 있습니다.
- 어떤 테이블부터 읽는가
- 어떤 인덱스를 후보로 봤고, 실제로 어떤 인덱스를 택했는가
- 얼마나 많은 row를 읽을 것으로 예상하는가
- 정렬, 임시 테이블, 추가 필터링이 필요한가
그래서 EXPLAIN 은 느린 쿼리의 “정답”이 아니라, 왜 느릴 가능성이 높은지 방향을 잡아주는 지도에 가깝습니다.
2. 처음에는 어떤 컬럼부터 보면 좋을까
처음부터 모든 컬럼을 다 외울 필요는 없습니다. 아래 여섯 개를 먼저 보면 대부분의 입문 문제는 잡힙니다.
typepossible_keyskeykey_lenrowsExtra
여기에 조인이 있는 쿼리라면 table 과 filtered 도 같이 보면 좋습니다.
실무적으로는 이렇게 읽으면 편합니다.
- 어느 테이블이 먼저 읽히는가
- 그 테이블은 어떤 방식으로 읽히는가
- 기대한 인덱스를 실제로 탔는가
- 얼마나 많은 row를 읽을 것으로 보는가
- 정렬, 임시 테이블, 추가 필터 같은 후처리가 있는가
즉 EXPLAIN은 컬럼별 암기보다 질문 순서가 중요합니다.
3. type 은 “얼마나 효율적으로 읽는가”를 보여주는 힌트다
type 은 초보자가 가장 먼저 봐야 하는 컬럼입니다. MySQL이 테이블을 어떤 방식으로 읽는지 보여주기 때문입니다.
입문 단계에서는 아래 정도만 익혀도 충분히 도움이 됩니다.
const: 아주 제한적인 단건 조회에 가까운 경우eq_ref: 조인에서 매우 선택적인 접근ref: 인덱스를 통한 일반적인 참조range: 범위 조건 기반 접근index: 인덱스 전체를 훑는 경우ALL: 테이블 전체 스캔 가능성
여기서 흔한 오해가 하나 있습니다. ALL 이 무조건 나쁜 것은 아니고, 작은 테이블에서는 괜찮을 수도 있습니다. 반대로 range 나 ref 가 떴다고 해서 자동으로 빠른 것도 아닙니다.
그래도 초보자 입장에서 가장 먼저 의심하면 좋은 패턴은 이렇습니다.
- 큰 테이블에서
ALL - 기대보다 지나치게 넓은
range - 조인 여러 단계에서 모두 많은 row를 읽는 패턴
즉 type 은 “인덱스를 썼다/안 썼다”보다 얼마나 넓은 길로 읽고 있는지를 보여주는 신호로 보는 편이 좋습니다.
4. possible_keys, key, key_len 은 인덱스 선택의 맥락을 보여준다
많은 글이 key 만 보라고 하지만, 실제로는 셋을 같이 봐야 해석이 쉬워집니다.
possible_keys
MySQL이 이 쿼리에서 후보로 고려할 수 있다고 본 인덱스입니다.
이 값이 비어 있거나 기대한 인덱스가 없다면:
- 애초에 적절한 인덱스가 없거나
- 쿼리 형태가 인덱스를 타기 어렵거나
- 함수, 형변환, 표현식 때문에 인덱스 활용성이 떨어질 수 있습니다
key
실제로 선택한 인덱스입니다.
여기서 봐야 할 것은 단순히 NULL 여부만이 아닙니다.
- 기대한 인덱스가 맞는가
- 다른 인덱스를 탔다면 왜 그런가
- 정렬까지 커버하지 못해서 다른 후처리가 붙었는가
key_len
선택한 인덱스 중 얼마만큼을 활용하는지 짐작하게 해주는 값입니다.
입문자에게 가장 중요한 포인트는 아주 단순합니다.
- 복합 인덱스를 만들었더라도 일부 컬럼만 활용될 수 있다
WHERE조건과 정렬 순서에 따라 기대보다 짧은 prefix만 탈 수 있다
즉 인덱스는 “있다”보다 얼마나 적절하게 활용되었는가가 더 중요합니다.
인덱스 구조 자체를 더 깊게 보려면 MySQL 인덱스 설계 가이드가 바로 이어집니다.
5. rows 와 filtered 는 읽는 양을 추정하게 해준다
느린 쿼리를 볼 때 실제로 가장 큰 힌트를 주는 경우가 많은 게 rows 입니다. MySQL이 해당 단계에서 얼마나 많은 row를 읽을 것으로 예상하는지 보여주기 때문입니다.
중요한 점은 rows 가 실제 실행 결과 row 수가 아니라 읽어볼 것으로 예상하는 양이라는 점입니다.
예를 들어 결과는 20건만 나와도, 그 20건을 만들기 위해 수만 row를 읽을 수 있습니다.
여기서 같이 보면 좋은 게 filtered 입니다.
rows: 얼마나 많이 읽을 것으로 보는가filtered: 읽은 것 중 조건을 통과할 비율을 얼마나 보는가
이 둘을 같이 보면 “많이 읽고 많이 버리는” 쿼리인지 감이 잡힙니다.
특히 조인에서는 한 단계의 rows 가 다음 단계 입력으로 이어지기 때문에, 각 단계 추정치가 누적되며 비용이 커질 수 있습니다. 그래서 조인 쿼리에서는 단일 숫자 하나보다 전체 읽기 흐름을 보는 습관이 중요합니다.
즉 rows 가 크면 “결과가 많다”보다 읽는 과정이 넓다고 해석하는 편이 더 실무적입니다.
6. Extra 는 추가 비용의 흔적을 보여준다
Extra 는 초보자가 보기엔 잡다해 보이지만, 실제로는 성능 문제의 냄새를 가장 빨리 풍기는 컬럼 중 하나입니다.
처음에는 아래 몇 개만 기억해도 충분합니다.
Using whereUsing indexUsing filesortUsing temporary
이 중 특히 자주 경계하는 건 아래입니다.
Using filesort
정렬을 위해 추가 작업이 필요하다는 뜻입니다. 무조건 문제는 아니지만, 큰 입력 집합 위에서 발생하면 비용이 커지기 쉽습니다.
Using temporary
임시 테이블이 필요한 경우입니다. GROUP BY, DISTINCT, 복잡한 정렬 조합에서 자주 보이며, 데이터량이 커질수록 병목이 되기 쉽습니다.
Using index
커버링 인덱스처럼 테이블 본문 접근 없이 인덱스만으로 읽는 상황에서 자주 보일 수 있습니다. 보통 긍정적 신호로 볼 수 있지만, 이것만으로 모든 문제가 해결됐다고 보면 안 됩니다.
즉 Extra 는 “좋다/나쁘다”를 딱 잘라 말하기보다 정렬, 임시 작업, 후처리 비용이 붙는가를 알려주는 힌트입니다.
커버링 인덱스 관점은 MySQL covering index 가이드와 이어서 읽기 좋습니다.
7. 조인 쿼리에서는 “한 단계씩” 보는 습관이 중요하다
단일 테이블 조회는 비교적 단순하지만, 조인이 들어가면 EXPLAIN 해석이 갑자기 어려워집니다. 이때는 전체를 한 번에 이해하려 하지 말고, 각 테이블이 어떤 순서로 들어오고 각 단계에서 얼마나 넓게 읽는지를 차례로 봐야 합니다.
특히 아래 질문이 중요합니다.
- 첫 번째로 읽는 테이블이 너무 큰가
- 첫 단계에서 이미 너무 많은 row를 끌고 오지 않는가
- 조인 키에 적절한 인덱스가 있는가
- 정렬이나 그룹화가 조인 뒤에서 큰 비용으로 터지지 않는가
조인에서 느린 이유는 종종 “조인 자체가 나빠서”가 아니라, 앞단에서 너무 많은 입력을 만든 상태로 다음 단계에 넘기기 때문입니다.
그래서 조인 EXPLAIN은 “어느 테이블부터 읽느냐”와 “rows 가 어디서 급격히 커지느냐”를 먼저 보면 방향이 잡힙니다.
조인 자체 최적화는 MySQL JOIN 성능 가이드에서 더 자세히 볼 수 있습니다.
8. EXPLAIN 결과가 좋아 보여도 끝이 아닌 이유
EXPLAIN은 강력하지만, 추정 기반이라는 한계가 있습니다.
- 실제 데이터 분포와 통계가 다를 수 있고
- 운영 환경의 파라미터나 캐시 상태는 그대로 반영되지 않을 수 있고
- 애플리케이션이 같은 쿼리를 너무 많이 호출하는 문제는 EXPLAIN만으로 안 보일 수 있습니다
그래서 아래 같은 경우에는 EXPLAIN만 믿고 끝내면 아쉽습니다.
- plan은 괜찮아 보이는데 실제 latency가 높은 경우
- row 추정이 현실과 크게 어긋나는 경우
- 운영에서는 느린데 로컬에서는 안 느린 경우
MySQL 8 환경이라면 가능할 때 EXPLAIN ANALYZE 도 함께 보는 편이 좋습니다. plain EXPLAIN 이 예상 계획을 보여준다면, EXPLAIN ANALYZE 는 실제 실행 중 각 단계의 row 수와 시간을 더 현실적으로 보여줍니다.
물론 이것도 production traffic 전체를 완벽히 복제하지는 못합니다. 하지만 적어도 예상과 실제가 얼마나 어긋나는지를 잡는 데 큰 도움이 됩니다.
9. 자주 하는 오해
1. key 값이 있으면 인덱스 문제는 끝났다
아닙니다. 잘못된 인덱스를 잡았을 수도 있고, 일부 prefix만 썼을 수도 있고, 정렬 비용은 그대로일 수 있습니다.
2. rows 가 작으면 무조건 빠르다
정렬, 임시 테이블, 반복 실행, 조인 fan-out 때문에 느릴 수 있습니다.
3. ALL 은 항상 악이다
아주 작은 테이블이라면 문제 아닐 수 있습니다. 중요한 건 테이블 크기와 실제 비용 맥락입니다.
4. EXPLAIN만 읽을 줄 알면 최적화가 끝난다
실제 데이터 분포, 호출 빈도, lock wait, 애플리케이션의 N+1 패턴까지 같이 봐야 제대로 튜닝이 됩니다.
FAQ
Q. EXPLAIN을 언제 먼저 봐야 하나요?
느린 쿼리를 찾았을 때, 인덱스를 추가하기 전후를 비교할 때, 정렬과 조인 비용이 의심될 때 가장 먼저 열어볼 만합니다.
Q. 초보자는 어떤 경고 신호부터 기억하면 되나요?
큰 테이블에서 type = ALL, key = NULL, 매우 큰 rows, 그리고 Extra 의 Using filesort 와 Using temporary 정도만 먼저 익혀도 충분히 도움이 됩니다.
Q. EXPLAIN 결과가 좋아 보이는데도 느리면 어떻게 하나요?
실제 호출 횟수, 데이터 분포, lock 대기, EXPLAIN ANALYZE 결과를 같이 봐야 합니다. 단일 plan 해석만으로는 놓치는 경우가 많습니다.
Read Next
- 전체적인 병목 분류 순서는 MySQL Query Optimization Checklist에서 이어서 볼 수 있습니다.
- 조인 구조와 fan-out 문제는 MySQL JOIN 성능 가이드가 연결됩니다.
- 인덱스 선택과 설계는 MySQL 인덱스 설계 가이드와 MySQL covering index 가이드를 함께 보면 좋습니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis, RabbitMQ, Kafka 중 어디부터 볼까 Redis, RabbitMQ, Kafka가 함께 있는 시스템에서 지금 보이는 장애가 어느 계층에 더 가까운지, 첫 10분 안에 무엇을 확인하고 어떤 글로 들어가야 하는지 정리한 실전 허브 가이드입니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Astro 기술 블로그 SEO 체크리스트: 트래픽 기다리기 전에 먼저 고칠 것 Astro 기술 블로그를 위한 실전 SEO 체크리스트입니다. 배포 호스트 확인, robots.txt, sitemap, canonical, hreflang, 구조화 데이터, 페이지별 메타데이터, noindex 판단, 검증 명령까지 우선순위대로 정리합니다.
- 다국어 블로그 canonical과 hreflang 설정 가이드: 무엇을 확인하고 어디서 깨질까 다국어 블로그에서 canonical과 hreflang을 어떻게 설정해야 하는지 실전 기준으로 정리합니다. self-canonical, 상호 연결되는 hreflang 묶음, x-default, 카테고리 페이지, 최종 렌더 HTML 점검, 한 언어 버전이 다른 언어 버전을 눌러버리는 실수까지 다룹니다.
- OpenAI Codex CLI 설치 가이드: 설치, 인증, 첫 작업까지 OpenAI Codex CLI를 실전 기준으로 설치하는 방법을 정리했다. 설치, 로그인, 첫 실행, Windows 주의점, 첫 작업을 어떻게 시작하면 좋은지까지 다룬다.