MySQL 조회가 오래 걸릴 때는 EXPLAIN으로 실행 계획부터 확인할 수 있습니다. 처음에는 table, type, possible_keys, key, rows, Extra를 순서대로 읽으면 됩니다. 이때 rows는 실제 조회 건수가 아니라 옵티마이저가 추정한 검사 행 수입니다.
이 글은 MySQL 8.4 공식 매뉴얼 기준입니다. 운영 데이터베이스에서 쿼리를 실행하거나 성능을 측정한 결과는 아닙니다. 예제의 테이블과 인덱스 이름은 설명용이며 실제 테이블 구조에 맞게 바꿉니다.
1. 표 형태로 실행 계획 출력하기
EXPLAIN FORMAT=TRADITIONAL
SELECT id, created_at
FROM qa_orders
WHERE customer_id = 12
ORDER BY created_at DESC
LIMIT 20;
FORMAT=TRADITIONAL을 적어 표 형태를 명시했습니다. 기본 출력 형식은 서버 설정의 영향을 받을 수 있습니다. 먼저 어떤 테이블에서, 어떤 조건으로, 어떤 정렬을 요청하는지 쿼리 자체를 읽고 실행 계획과 나란히 놓습니다.
SQL 문법 오류 때문에 실행 계획이 나오지 않는다면 MySQL 1064의 오류 위치 확인부터 진행하세요. 문법 확인과 조회 경로 분석은 단계가 다릅니다.
2. type과 key를 먼저 연결해서 읽기
| 열 | 확인할 질문 |
| table | 어느 테이블의 접근 단계인가? |
| type | 어떤 방식으로 행을 찾는가? |
| possible_keys | 행을 찾는 데 후보가 되는 인덱스는? |
| key | 실제로 선택한 인덱스는? |
| rows | 검사할 것으로 추정한 행 수는? |
| Extra | 추가 필터링·정렬 정보가 있는가? |
type=ALL은 전체 테이블 스캔, type=range는 범위 접근을 뜻합니다. type=ref는 인덱스의 비교 조건에 맞는 여러 행을 찾을 수 있는 접근입니다. 이름 하나만으로 쿼리 전체의 속도를 확정할 수는 없습니다. 작은 테이블에서는 전체 스캔이 선택될 수도 있습니다.
possible_keys에 인덱스가 있어도 key에는 다른 값이나 NULL이 나올 수 있습니다. 후보 목록과 선택 결과를 구분해야 합니다. 또 key가 표시되어도 전체 인덱스를 훑는 type=index라면 읽는 양을 함께 확인합니다.
SHOW INDEX FROM qa_orders;
SHOW INDEX에서 인덱스에 포함된 열과 순서를 확인합니다. 쿼리 조건과 일치하는 이름만 찾기보다 실제 인덱스 열 구성을 읽는 것이 다음 판단에 도움이 됩니다.
3. rows는 추정값, 실제 시간은 별도
다음 숫자는 읽는 방법을 설명하기 위한 가상 예시입니다. 측정 결과나 개선 전후 수치가 아닙니다.
| 가상 실행 계획 값 | 해석 |
| rows=1000 | 이 접근에서 검사할 행을 약 1,000개로 추정 |
| filtered=10.00 | 조건을 통과할 비율을 약 10%로 추정 |
| 1000 × 10% | 다음 단계로 약 100개가 이어질 것으로 추정 |
InnoDB에서 rows는 추정치이며 실제 행 수와 다를 수 있습니다. 조인 쿼리는 앞 단계에서 몇 번 반복되는지도 함께 읽어야 합니다. rows가 줄었다는 이유만으로 사용자 응답 시간이 같은 비율로 줄었다고 표현할 수는 없습니다.
LIMIT 20도 검사할 행을 항상 20개로 제한한다는 뜻은 아닙니다. 조건에 맞는 행을 찾고 정렬하는 과정에서 더 많은 행을 읽을 수 있습니다. 정렬과 조건을 인덱스로 처리할 수 있는지에 따라 계획이 달라집니다.
4. Extra와 EXPLAIN ANALYZE 구분하기
| 표시 | 읽는 방법 |
| Using where | 조건으로 행을 필터링함 |
| Using index | 필요한 열을 인덱스에서 읽을 수 있는 접근 |
| Using filesort | 인덱스 순서만으로 끝내지 못한 추가 정렬 |
Using filesort라는 이름만으로 디스크 정렬이 발생했다고 단정할 수는 없습니다. 추가 정렬을 한다는 뜻으로 읽고, 데이터 양과 실행 조건을 함께 확인합니다. Using index도 인덱스를 사용한 모든 상황에 붙는 일반 표시와 구분해야 합니다.
EXPLAIN ANALYZE
SELECT id, created_at
FROM qa_orders
WHERE customer_id = 12
ORDER BY created_at DESC
LIMIT 20;
EXPLAIN ANALYZE는 쿼리를 실제 실행하고 시간·행 수·반복 횟수 정보를 제공합니다. 따라서 일반 EXPLAIN처럼 계획만 확인하는 명령으로 취급하면 안 됩니다. 위 예제도 테스트 데이터베이스에서 부하와 권한을 확인한 다음 사용합니다. 이 글에서는 실행하지 않았습니다.
인덱스를 추가할지는 조회 조건뿐 아니라 정렬, 데이터 분포, 쓰기 비용까지 보고 결정합니다. 조건 열이 customer_id와 created_at이라고 해서 모든 환경에서 같은 복합 인덱스가 선택되지는 않습니다. 저장 방식의 배경은 InnoDB와 MySQL의 관계를 참고할 수 있습니다.
자주 묻는 질문
key가 NULL이면 바로 인덱스를 추가하나요?
먼저 테이블 크기, 조건의 선택도, 기존 인덱스 열 순서를 확인합니다. 옵티마이저가 전체 스캔을 선택한 이유를 살핀 뒤 테스트 환경에서 후보를 비교합니다.
EXPLAIN과 EXPLAIN ANALYZE의 숫자가 달라도 되나요?
하나는 추정 계획을, 다른 하나는 실제 실행 정보를 포함합니다. 차이가 크면 데이터 분포와 통계 상태를 살펴볼 근거가 됩니다. 같은 데이터와 조건으로 비교해야 의미가 있습니다.
MySQL 5.7에서도 ANALYZE를 쓸 수 있나요?
EXPLAIN ANALYZE는 MySQL 8.0.18에서 도입됐습니다. 5.7에서는 같은 문법을 사용할 수 없습니다. MariaDB 역시 지원 문법과 출력 의미를 해당 제품 문서로 따로 확인합니다.
근거: MySQL EXPLAIN 문법, 실행 계획 출력의 의미. 본문의 수치는 해석용 예시이며, 인덱스 생성이나 운영 쿼리 실행은 수행하지 않았습니다.
'개발 문제 해결' 카테고리의 다른 글
| git stash 사용법: 작업 중인 수정을 잠깐 치우고 다시 꺼내는 순서 (0) | 2026.09.18 |
|---|---|
| Git worktree 사용법: 작업 중인 코드를 그대로 두고 다른 브랜치 열기 (0) | 2026.09.18 |
| Docker 로그 확인: 최근 100줄·시간 범위·Compose 조회 (0) | 2026.09.12 |
| curl 응답 확인: 상태 코드·헤더·본문·종료 코드 구분하기 (0) | 2026.09.12 |
| 파이썬 JSON 파일 저장·읽기: 한글과 JSONDecodeError 확인 (0) | 2026.09.12 |
댓글