느린 쿼리는 반드시 이유가 있습니다. EXPLAIN은 MySQL이 쿼리를 어떻게 실행하려는지 보여주며, 병목 구간을 정확히 짚어줍니다.
EXPLAIN 기본부터 FORMAT=JSON
, EXPLAIN ANALYZE
를 정리했습니다.
1) EXPLAIN 기본 사용법
EXPLAIN
SELECT o.id, o.user_id, u.name
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.created_at >= CURDATE() - INTERVAL 30 DAY
AND u.country = 'KR'
ORDER BY o.created_at DESC
LIMIT 20;
결과는 테이블(또는 서브쿼리) 단위로 한 행씩 출력됩니다. 위에서부터 실행 순서라고 생각하면 이해가 쉽습니다.
2) EXPLAIN 핵심 컬럼 빠르게 해석하기
컬럼 | 의미/해석 포인트 |
---|---|
id |
쿼리의 단계/순서. 숫자가 클수록 먼저 실행되는 경향(서브쿼리 등 구분). |
select_type |
SIMPLE, PRIMARY, SUBQUERY, DERIVED 등 쿼리 유형. |
table |
접근 중인 테이블(별칭 포함). |
partitions |
파티셔닝 사용 시 어떤 파티션을 읽는지. |
type |
접근 방식. 성능 중요도 1순위. 좋음: system > const > eq_ref > ref > range > index > ALL(테이블 풀스캔) |
possible_keys |
사용 가능했던 인덱스 후보. |
key |
실제로 선택된 인덱스. NULL이면 인덱스 미사용. |
key_len |
사용된 인덱스 길이(바이트). 선두 컬럼만 쓰였는지 추정할 때 유용. |
ref |
조인 시 어떤 컬럼/상수가 인덱스를 타는지. |
rows |
예상 스캔 행 수(작을수록 좋음). 추정치이므로 실제와 다를 수 있음. |
filtered |
필터 후 남는 비율(%) 추정. 낮으면 불필요한 읽기가 많은 것. |
Extra |
Using where , Using index , Using temporary , Using filesort 등 힌트성 메시지. |
체크포인트: type=ALL
또는 Extra
에 Using temporary
/Using filesort
가 계속 보인다면 개선 여지가 큽니다.
3) 예제로 보는 해석 & 튜닝
3-1. WHERE + ORDER BY가 느린 목록
EXPLAIN
SELECT * FROM posts
WHERE status = 'PUBLISHED'
ORDER BY published_at DESC
LIMIT 20;
문제 신호: type=ALL
, key=NULL
, rows
가 매우 큼, Extra=Using filesort
.
개선: (status, published_at)
순서의 복합 인덱스 추가.
CREATE INDEX ix_posts_status_published_at
ON posts (status, published_at DESC); -- MySQL 8.0는 DESC 인덱스 지원
EXPLAIN
SELECT * FROM posts
WHERE status = 'PUBLISHED'
ORDER BY published_at DESC
LIMIT 20;
이제 type=range/ref
로 바뀌고, 정렬이 인덱스만으로 처리되어 Using filesort
가 사라질 수 있습니다.
3-2. JOIN 순서와 key_len
EXPLAIN
SELECT o.id, u.name
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.user_id = 10
AND u.country = 'KR';
개선 아이디어:
orders(user_id)
,users(id)
,users(country)
인덱스 확인key_len
이 선두 컬럼까지만 사용되는지 체크(복합 인덱스 설계 재검토)
4) 더 풍부한 정보: EXPLAIN FORMAT=JSON
EXPLAIN FORMAT=JSON
SELECT o.id, u.name
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE u.country = 'KR';
JSON 출력에는 cost 정보, chosen range, attached conditions, used_columns 등 세부 계획이 포함돼 원인 분석이 쉬워집니다. 파싱해서 로그/대시보드에 넣어도 좋습니다.
5) 실제 실행 시간을 보자: EXPLAIN ANALYZE
(8.0.18+)
EXPLAIN ANALYZE
SELECT /* 실제 실행됨 */ *
FROM posts
WHERE status = 'PUBLISHED'
ORDER BY published_at DESC
LIMIT 20;
EXPLAIN
은 “계획”이고, EXPLAIN ANALYZE
는 쿼리를 실제로 실행하며 실측 시간과 각 단계의 rows examined를 보여줍니다. 계획 대비 오차를 바로 확인할 수 있어 튜닝 정확도가 높아집니다.
6) Extra 메시지 빠르게 판독
Using index
: 커버링 인덱스. 테이블 본문을 읽지 않음(좋음).Using where
: 인덱스로 후보를 좁힌 뒤 추가 필터 수행.Using temporary
: 임시 테이블 생성. GROUP BY/ORDER BY 조합에서 자주 발생(가능하면 인덱스로 해소).Using filesort
: 추가 정렬 수행. 정렬 키 포함 인덱스로 대체 고려.Range checked for each record
: 적절한 인덱스를 못 골라 레코드마다 범위 체크(인덱스 재설계 필요).
7) 튜닝 체크리스트 (바로 적용)
- WHERE, JOIN, ORDER BY, GROUP BY에 사용되는 컬럼부터 인덱스 후보에 올리기.
- 복합 인덱스는 선행 선택도 높은 컬럼 → 정렬/그룹 컬럼 순으로 설계.
- SELECT * 대신 필요한 컬럼만 선택 → 커버링 인덱스 유도.
- 큰 OFFSET 페이지네이션은 커서 방식(키 기반)으로 변경.
- 통계가 계획과 다르면
ANALYZE TABLE
또는 인덱스 재구축 고려. - 서브쿼리를 JOIN/EXISTS로 바꿔 계획 단순화 시도.
8) 자주 쓰는 EXPLAIN 스니펫
-- 1) 일반/JSON/ANALYZE 3종 비교
EXPLAIN SELECT ...;
EXPLAIN FORMAT=JSON SELECT ...;
EXPLAIN ANALYZE SELECT ...;
-- 2) 특정 인덱스 사용 유도(힌트, 8.0+)
SELECT /*+ INDEX(posts ix_posts_status_published_at) */
id, title
FROM posts
WHERE status='PUBLISHED'
ORDER BY published_at DESC
LIMIT 20;
-- 3) 통계 최신화
ANALYZE TABLE posts;
-- 4) 정렬/그룹 최적화용 복합 인덱스
CREATE INDEX ix_orders_user_date ON orders (user_id, created_at);
EXPLAIN → FORMAT=JSON → EXPLAIN ANALYZE 순으로 살펴보면 계획과 실제를 모두 파악할 수 있습니다.
핵심은 type, key, rows, Extra 해석과, 그에 맞춘 인덱스/쿼리 구조 재설계입니다. 작은 수정 하나가 응답 시간을 수십 배 줄일 수 있습니다.
'개발 > DB' 카테고리의 다른 글
MySQL에서 날짜와 시간 관련 함수 사용법 (0) | 2025.08.20 |
---|---|
MySQL에서 문자열 함수 활용법 (0) | 2025.08.19 |
MySQL에서 LIMIT와 OFFSET을 사용한 페이지네이션 (1) | 2025.08.17 |
MySQL 트랜잭션 격리 수준(ISOLATION LEVEL) 이해하기 (2) | 2025.08.16 |
MySQL에서 JSON 데이터 처리하기 (저장·조회·수정·인덱싱) (3) | 2025.08.15 |