MySQL EXPLAIN 기본 해석: type, key, rows, filtered, Extra의 의미
MySQL EXPLAIN의 핵심 컬럼을 실행 경로, 인덱스 선택, row 추정, Extra 메시지 중심으로 운영 관점에서 해석한다.
1. 왜 EXPLAIN 해석이 운영자의 기본 언어인가
MySQL 성능 문제를 다룰 때 가장 먼저 확인하는 도구는 대개 EXPLAIN이다. 느린 SQL을 만났을 때 실행 계획을 보지 않고 인덱스를 추가하거나 쿼리를 고치는 것은, 대기 이벤트와 로그를 보지 않고 장애 원인을 단정하는 것과 비슷하다. EXPLAIN은 MySQL Optimizer가 선택한 접근 경로를 보여 주며, 그 안에는 어떤 테이블을 먼저 읽는지, 어떤 인덱스를 쓰는지, 얼마나 많은 row를 읽을 것으로 추정하는지, 정렬이나 임시 테이블이 필요한지에 대한 단서가 들어 있다.
그러나 EXPLAIN 결과는 처음 보면 오해하기 쉽다. type이 ALL이면 항상 나쁜가, key가 보이면 무조건 인덱스를 잘 탄 것인가, rows는 실제 읽은 row 수인가, filtered가 낮으면 반드시 문제가 있는가 같은 질문이 반복된다. 운영 환경에서는 이런 단순 해석이 잘못된 대응으로 이어질 수 있다. 작은 차원 테이블의 full scan은 정상일 수 있고, 인덱스를 사용했더라도 선택도가 낮으면 전체 성능은 나쁠 수 있다. 반대로 Extra에 Using filesort가 보인다고 해서 항상 디스크 정렬 장애를 의미하지도 않는다.
이 글은 EXPLAIN의 기본 컬럼인 type, key, rows, filtered, Extra를 중심으로 해석 기준을 정리한다. 목표는 컬럼 이름을 암기하는 것이 아니라, 실행 계획을 통해 Optimizer의 판단을 역추적하고 운영 의사결정으로 연결하는 것이다.
2. EXPLAIN이 보여 주는 것과 보여 주지 않는 것
EXPLAIN은 SQL을 실제로 끝까지 실행한 결과가 아니라, Optimizer가 선택한 실행 계획의 설명이다. 일반적인 EXPLAIN SELECT ...는 “이 SQL을 실행한다면 이런 접근 경로를 사용할 것이다”에 가깝다. 따라서 rows와 filtered는 실제 측정값이 아니라 통계 기반 추정값이다. MySQL 8.0의 EXPLAIN ANALYZE는 실제 실행 시간과 row 흐름을 보여 주지만, 대상 SQL을 실제로 실행하므로 운영 환경에서는 더 조심해서 사용해야 한다.
기본 흐름은 다음과 같다.
flowchart TD
A[SQL 입력] --> B[Parser와 Resolver]
B --> C[Optimizer 후보 계획 생성]
C --> D[통계와 Cost Model로 비용 계산]
D --> E[선택된 실행 계획]
E --> F[EXPLAIN 결과]
E --> G[Executor 실제 실행]
G --> H[실제 row 수와 대기 시간]
F -.추정값.-> I[rows, filtered, key, Extra]
H -.실측값.-> J[slow log, Performance Schema, EXPLAIN ANALYZE]
EXPLAIN을 읽을 때는 다음 구분이 중요하다.
| 구분 | 의미 | 운영상 해석 |
|---|---|---|
| 계획 정보 | type, possible_keys, key, Extra |
Optimizer가 선택한 접근 경로 |
| 추정 정보 | rows, filtered |
통계와 조건식으로 계산한 예상 row 흐름 |
| 실제 실행 정보 | slow log, Performance Schema, EXPLAIN ANALYZE |
실제 읽기량, 지연 시간, 대기 이벤트 |
즉, EXPLAIN은 성능 분석의 출발점이지 최종 판결문이 아니다. 좋은 운영 절차는 EXPLAIN으로 가설을 세우고, slow log·Performance Schema·실제 실행 시간·데이터 분포로 검증하는 방식이다.
3. 기본 EXPLAIN 출력 구조 확인
먼저 MySQL 8.0에서 전통적인 EXPLAIN 결과가 어떤 컬럼을 갖는지 확인한다. 아래 예제는 별도 테이블 없이 실행할 수 있는 최소 예제다.
SELECT VERSION() AS mysql_version;
EXPLAIN SELECT 1 AS value_one;
실행 결과(MySQL 8.0.x):
mysql> SELECT VERSION() AS mysql_version;
+---------------+
| mysql_version |
+---------------+
| 8.0.46 |
+---------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT 1 AS value_one;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
1 row in set (0.00 sec)
EXPLAIN의 전통 출력은 보통 다음 컬럼을 포함한다.
id: SELECT 단위 또는 query block의 식별자다.select_type: 단순 SELECT, derived table, union 등 SELECT의 형태다.table: 접근 대상 테이블 또는 derived object다.partitions: partition pruning 결과다. partition을 쓰지 않으면NULL일 수 있다.type: 접근 방식이다. 인덱스와 row 접근 효율을 빠르게 판단하는 핵심 컬럼이다.possible_keys: 사용할 가능성이 있었던 인덱스 목록이다.key: 실제 선택된 인덱스다.key_len: 선택된 인덱스에서 사용된 key part 길이다.ref: 인덱스 lookup에 사용된 상수 또는 컬럼이다.rows: 읽을 것으로 추정한 row 수다.filtered: 해당 테이블에서 조건을 통과할 것으로 추정한 비율이다.Extra: 정렬, 임시 테이블, covering index, where 적용 등 추가 실행 특성이다.
운영자는 모든 컬럼을 같은 비중으로 보지 않는다. 초기 진단에서는 table → type → possible_keys/key → rows/filtered → Extra 순서로 읽는 편이 실용적이다. 어떤 테이블을 어떤 순서로 읽는지 확인하고, 접근 방식과 인덱스 선택을 본 뒤, row 추정과 부가 작업을 확인하는 것이다.
4. type: 접근 방식의 압축 요약
type은 테이블에서 row를 찾는 방법을 압축해서 보여 준다. 이름은 type이지만 실제 의미는 access type 또는 join type에 가깝다. 자주 보는 값은 다음과 같다.
| type | 대략적 의미 | 해석 기준 |
|---|---|---|
system, const |
한 번만 읽으면 되는 단일 row | 매우 좋은 접근이다. Primary key 또는 unique key 동등 조건에서 자주 나온다. |
eq_ref |
조인에서 앞 테이블의 각 row마다 정확히 한 row lookup | PK/Unique key 조인에서 기대하는 좋은 형태다. |
ref |
non-unique index 동등 조건 lookup | 일반적인 인덱스 조건 접근이다. 선택도가 중요하다. |
range |
인덱스 범위 scan | 범위 조건, BETWEEN, prefix range, 일부 IN 조건에서 나온다. 읽는 범위가 크면 비용이 커진다. |
index |
인덱스 전체 scan | 테이블 전체 scan보다 나을 수도 있지만, 많은 인덱스 엔트리를 읽는다. |
ALL |
테이블 전체 scan | 큰 테이블에서는 위험 신호지만, 작은 테이블이나 선택도가 낮은 조건에서는 합리적일 수 있다. |
중요한 점은 type만으로 좋고 나쁨을 확정하지 않는 것이다. ALL이어도 20 row짜리 코드 테이블이면 문제가 아닐 수 있다. ref라도 조건이 전체의 80%를 반환하면 비효율적일 수 있다. range도 범위가 작으면 좋지만, 월 단위 파티션 없는 대형 로그 테이블에서 6개월 범위를 읽는다면 매우 비싸다.
5. key와 possible_keys: 인덱스 후보와 실제 선택
possible_keys는 Optimizer가 조건식과 조인 조건을 보고 사용할 수 있다고 판단한 인덱스 후보 목록이다. key는 그중 실제로 선택한 인덱스다. 다음 세 가지 상황을 구분해야 한다.
possible_keys와key가 모두NULL이다. 조건에 맞는 인덱스 후보가 없거나, 조건식이 인덱스를 사용할 수 없는 형태일 가능성이 높다.possible_keys에는 후보가 있지만key가NULL이다. 인덱스 후보는 있었지만 full scan이 더 싸다고 판단했을 수 있다.key가 표시된다. 인덱스를 선택했지만, 그 인덱스가 충분히 좋은지는type,rows,filtered,Extra와 함께 봐야 한다.
key_len도 중요하다. 복합 인덱스에서 어느 key part까지 사용했는지 추정하는 단서가 되기 때문이다. 예를 들어 (customer_id, status, created_at) 인덱스가 있는데 customer_id까지만 사용되었는지, status까지 사용되었는지에 따라 읽는 범위가 크게 달라질 수 있다.
6. rows와 filtered: 실제값이 아니라 row 흐름 추정치
rows는 해당 접근 방식으로 읽을 것으로 예상한 row 수다. filtered는 읽은 row 중 조건을 통과할 것으로 예상한 비율이다. 두 값을 곱하면 다음 단계로 넘어갈 row 수를 대략적으로 추정할 수 있다.
다음 단계 예상 row 수 ≒ rows × filtered / 100
예를 들어 rows=100000, filtered=1.0이면 10만 row를 읽어 약 1천 row가 남는다는 뜻이다. 반대로 rows=100, filtered=100.0이면 100 row를 읽어 대부분 다음 단계로 넘긴다는 뜻이다. 둘 중 어느 쪽이 나쁜지는 쿼리 목적, 조인 순서, 정렬 여부, row 크기에 따라 달라진다.
운영에서 주의할 점은 rows가 실제 읽은 row 수가 아니라는 것이다. 통계가 오래되었거나 데이터 분포가 치우쳐 있으면 rows 추정이 크게 틀릴 수 있다. 앞선 글에서 다룬 Persistent Statistics, Histogram, Cost Model과 연결되는 지점이 바로 여기다. EXPLAIN이 이상하게 보일 때는 인덱스만 볼 것이 아니라 통계가 현재 데이터를 반영하는지도 확인해야 한다.
7. Extra: 부가 작업과 최적화 단서
Extra는 짧은 문자열이지만 매우 중요한 힌트를 제공한다. 자주 보는 메시지는 다음과 같다.
| Extra | 의미 | 주의점 |
|---|---|---|
Using where |
스토리지 엔진에서 가져온 row에 서버 계층 조건을 적용한다 | 정상적인 경우도 많다. 단독으로 문제라고 보지 않는다. |
Using index |
covering index로 테이블 row 접근 없이 인덱스만으로 처리한다 | 보통 좋은 신호다. 단, 인덱스 전체 scan이면 읽기량은 여전히 클 수 있다. |
Using index condition |
Index Condition Pushdown을 사용한다 | InnoDB가 인덱스 조건 일부를 더 아래에서 평가한다. |
Using filesort |
정렬 작업이 필요하다 | 반드시 디스크 파일 정렬을 뜻하지 않는다. 메모리 정렬도 포함하는 용어다. |
Using temporary |
임시 테이블이 필요하다 | GROUP BY, DISTINCT, 일부 정렬과 조합될 때 비용이 커질 수 있다. |
특히 Using filesort라는 이름은 자주 오해된다. MySQL의 filesort는 “인덱스 순서만으로 결과를 만들 수 없어 별도 정렬 알고리즘을 사용한다”는 의미에 가깝다. 항상 파일 시스템에 큰 임시 파일을 쓴다는 뜻은 아니다. 정렬 대상 row 수, row width, sort_buffer_size, LIMIT 여부, 임시 테이블 사용 여부를 함께 봐야 한다.
8. 예제로 보는 key, rows, Extra 해석
다음 예제는 주문 테이블을 만들고 두 가지 조건의 실행 계획을 비교한다. 첫 번째 쿼리는 customer_id와 status 조건을 사용한다. 두 번째 쿼리는 status와 created_at 인덱스를 이용해 최근 주문을 정렬 없이 가져올 수 있는지 확인한다.
DROP TABLE IF EXISTS explain_orders;
CREATE TABLE explain_orders (
order_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL,
created_at DATETIME NOT NULL,
amount DECIMAL(10,2) NOT NULL,
KEY idx_customer_status (customer_id, status),
KEY idx_status_created (status, created_at)
) ENGINE=InnoDB;
INSERT INTO explain_orders
(order_id, customer_id, status, created_at, amount)
VALUES
(1, 101, 'PAID', '2026-07-01 10:00:00', 120.00),
(2, 101, 'READY', '2026-07-01 11:00:00', 80.00),
(3, 101, 'PAID', '2026-07-02 09:00:00', 200.00),
(4, 102, 'CANCELLED', '2026-07-02 10:00:00', 30.00),
(5, 103, 'PAID', '2026-07-03 12:00:00', 300.00),
(6, 104, 'READY', '2026-07-03 13:00:00', 70.00);
ANALYZE TABLE explain_orders;
EXPLAIN
SELECT order_id, amount
FROM explain_orders
WHERE customer_id = 101
AND status = 'PAID';
EXPLAIN
SELECT order_id, customer_id, created_at
FROM explain_orders
WHERE status = 'PAID'
ORDER BY created_at
LIMIT 3;
DROP TABLE explain_orders;
실행 결과(MySQL 8.0.x):
mysql> CREATE TABLE explain_orders (...);
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO explain_orders ...;
Query OK, 6 rows affected (0.01 sec)
Records: 6 Duplicates: 0 Warnings: 0
mysql> ANALYZE TABLE explain_orders;
+--------------------------------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+--------------------------------+---------+----------+----------+
| mysql_tech_note.explain_orders | analyze | status | OK |
+--------------------------------+---------+----------+----------+
1 row in set (0.00 sec)
mysql> EXPLAIN
-> SELECT order_id, amount
-> FROM explain_orders
-> WHERE customer_id = 101
-> AND status = 'PAID';
+----+-------------+----------------+------------+------+----------------------------------------+---------------------+---------+-------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------------+------------+------+----------------------------------------+---------------------+---------+-------------+------+----------+-------+
| 1 | SIMPLE | explain_orders | NULL | ref | idx_customer_status,idx_status_created | idx_customer_status | 90 | const,const | 2 | 100.00 | NULL |
+----+-------------+----------------+------------+------+----------------------------------------+---------------------+---------+-------------+------+----------+-------+
1 row in set (0.00 sec)
mysql> EXPLAIN
-> SELECT order_id, customer_id, created_at
-> FROM explain_orders
-> WHERE status = 'PAID'
-> ORDER BY created_at
-> LIMIT 3;
+----+-------------+----------------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | explain_orders | NULL | ref | idx_status_created | idx_status_created | 82 | const | 3 | 100.00 | NULL |
+----+-------------+----------------+------------+------+--------------------+--------------------+---------+-------+------+----------+-------+
1 row in set (0.00 sec)
mysql> DROP TABLE explain_orders;
Query OK, 0 rows affected (0.00 sec)
이 예제에서 첫 번째 EXPLAIN은 idx_customer_status를 선택하는 것이 자연스럽다. customer_id와 status가 복합 인덱스의 선두부터 맞기 때문이다. 두 번째 EXPLAIN은 idx_status_created가 status 필터링과 created_at 정렬 순서에 모두 도움을 줄 수 있다. 실제 운영에서는 데이터 분포와 LIMIT 크기에 따라 다른 계획이 나올 수 있지만, 해석 방향은 같다.
읽을 때는 다음 순서를 권장한다.
table이 어떤 순서로 나오는지 본다.- 각 테이블의
type이ref,range,ALL중 어디에 가까운지 본다. possible_keys와key를 비교해 후보와 실제 선택이 어긋났는지 확인한다.rows와filtered로 다음 단계 row 수를 추정한다.Extra에서 정렬, 임시 테이블, covering index 여부를 확인한다.
9. 조인 계획에서 EXPLAIN을 읽는 방법
조인 SQL에서는 EXPLAIN의 각 행이 테이블 접근 단계를 나타낸다. 먼저 읽는 테이블의 row 수 추정이 틀리면 뒤 테이블 lookup 횟수도 함께 틀어진다. 아래 예제는 고객과 주문을 조인할 때 PK lookup과 보조 인덱스 lookup이 어떻게 표시되는지 확인한다.
DROP TABLE IF EXISTS explain_join_orders;
DROP TABLE IF EXISTS explain_customers;
CREATE TABLE explain_customers (
customer_id BIGINT PRIMARY KEY,
customer_name VARCHAR(100) NOT NULL,
grade VARCHAR(20) NOT NULL,
KEY idx_grade (grade)
) ENGINE=InnoDB;
CREATE TABLE explain_join_orders (
order_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
KEY idx_customer_status (customer_id, status)
) ENGINE=InnoDB;
INSERT INTO explain_customers VALUES
(101, 'customer-a', 'GOLD'),
(102, 'customer-b', 'SILVER'),
(103, 'customer-c', 'GOLD'),
(104, 'customer-d', 'BRONZE');
INSERT INTO explain_join_orders VALUES
(1, 101, 'PAID', 120.00),
(2, 101, 'READY', 80.00),
(3, 103, 'PAID', 300.00),
(4, 104, 'CANCELLED', 30.00);
ANALYZE TABLE explain_customers, explain_join_orders;
EXPLAIN
SELECT c.customer_id, c.customer_name, o.order_id, o.amount
FROM explain_customers c
JOIN explain_join_orders o
ON o.customer_id = c.customer_id
WHERE c.grade = 'GOLD'
AND o.status = 'PAID';
DROP TABLE explain_join_orders;
DROP TABLE explain_customers;
실행 결과(MySQL 8.0.x):
mysql> CREATE TABLE explain_customers (...);
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE explain_join_orders (...);
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO explain_customers VALUES (...);
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql> INSERT INTO explain_join_orders VALUES (...);
Query OK, 4 rows affected (0.01 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql> ANALYZE TABLE explain_customers, explain_join_orders;
+-------------------------------------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+-------------------------------------+---------+----------+----------+
| mysql_tech_note.explain_customers | analyze | status | OK |
| mysql_tech_note.explain_join_orders | analyze | status | OK |
+-------------------------------------+---------+----------+----------+
2 rows in set (0.00 sec)
mysql> EXPLAIN
-> SELECT c.customer_id, c.customer_name, o.order_id, o.amount
-> FROM explain_customers c
-> JOIN explain_join_orders o
-> ON o.customer_id = c.customer_id
-> WHERE c.grade = 'GOLD'
-> AND o.status = 'PAID';
+----+-------------+-------+------------+--------+---------------------+---------+---------+-------------------------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------------+---------+---------+-------------------------------+------+----------+-------------+
| 1 | SIMPLE | o | NULL | ALL | idx_customer_status | NULL | NULL | NULL | 4 | 25.00 | Using where |
| 1 | SIMPLE | c | NULL | eq_ref | PRIMARY,idx_grade | PRIMARY | 8 | mysql_tech_note.o.customer_id | 1 | 50.00 | Using where |
+----+-------------+-------+------------+--------+---------------------+---------+---------+-------------------------------+------+----------+-------------+
2 rows in set (0.00 sec)
mysql> DROP TABLE explain_join_orders;
Query OK, 0 rows affected (0.00 sec)
mysql> DROP TABLE explain_customers;
Query OK, 0 rows affected (0.00 sec)
조인 계획에서는 eq_ref와 ref의 차이를 특히 자주 본다. eq_ref는 앞 테이블의 각 row마다 뒤 테이블에서 unique key 또는 primary key로 최대 한 row를 찾는다는 뜻이다. ref는 non-unique index로 여러 row가 나올 수 있는 lookup이다. 둘 다 인덱스 기반 접근이지만, 뒤 테이블에서 몇 row가 나올지에 따라 비용 차이가 커질 수 있다.
또 하나의 중요한 지점은 조인 순서다. SQL에 쓴 테이블 순서가 실제 실행 순서와 항상 같지는 않다. Optimizer는 조건 선택도, 인덱스, 통계, 조인 비용을 보고 순서를 바꿀 수 있다. 따라서 EXPLAIN 행의 순서를 보면서 “왜 이 테이블을 먼저 읽는가”를 질문해야 한다. 먼저 읽는 테이블이 충분히 row를 줄이지 못하면 뒤 테이블 lookup이 폭증할 수 있다.
10. 운영 환경에서의 해석 절차
실제 장애나 성능 저하 상황에서는 다음 절차로 접근하는 것이 안전하다.
10.1 SQL과 바인드 값의 대표성 확인
같은 SQL이라도 바인드 값에 따라 선택도가 크게 달라질 수 있다. 예를 들어 tenant_id = 1이 전체 데이터의 70%를 차지하고 tenant_id = 999가 0.01%만 차지한다면, 같은 인덱스라도 성능 특성이 다르다. slow log에서 실제 느렸던 값, 애플리케이션에서 사용한 조건, 시간 범위를 함께 확인해야 한다.
10.2 EXPLAIN으로 계획의 큰 방향 확인
먼저 full scan, 잘못된 조인 순서, 과도한 rows, Using temporary, Using filesort가 있는지 본다. 이 단계에서는 결론을 내리기보다 의심 지점을 좁힌다.
10.3 실제 실행 정보와 대조
EXPLAIN의 rows는 추정값이다. 실제 문제를 확인하려면 slow log의 Rows_examined, Performance Schema의 statement 통계, 애플리케이션 지연 시간, 필요 시 안전한 환경의 EXPLAIN ANALYZE를 함께 본다.
10.4 대응책을 분류
대응은 보통 네 가지로 나뉜다.
- 통계 보정:
ANALYZE TABLE, Histogram, persistent statistics 확인. - 인덱스 조정: 조건식과 정렬/조인 순서를 반영한 복합 인덱스 설계.
- SQL 재작성: 비SARGable 조건 제거, 불필요한 함수 적용 제거, 조인 조건 명확화.
- 업무 조건 조정: 과도한 기간 조회 제한, pagination 방식 변경, 배치 처리 분리.
운영에서는 인덱스 추가가 가장 눈에 잘 띄지만 항상 정답은 아니다. 잘못된 인덱스는 쓰기 비용과 버퍼 풀 압박을 늘리고, 통계가 틀린 상태에서는 새 인덱스도 기대한 대로 선택되지 않을 수 있다.
11. Aurora MySQL에서의 해석 차이
Aurora MySQL은 MySQL 호환 Optimizer를 사용하므로 EXPLAIN의 기본 의미는 Community MySQL과 크게 다르지 않다. type, key, rows, filtered, Extra를 읽는 방식도 동일한 기준에서 출발한다. 다만 운영 해석에서는 다음 차이를 고려해야 한다.
- Aurora의 스토리지 계층은 분산 스토리지 구조이므로 I/O 지연과 캐시 상태의 양상이 self-managed InnoDB와 다를 수 있다.
- Writer와 Reader 인스턴스의 버퍼 캐시 상태, 통계 갱신 시점, 파라미터 그룹 차이가 실행 시간 차이를 만들 수 있다.
- Performance Insights, Enhanced Monitoring, CloudWatch 지표를 함께 보면
EXPLAIN이 말하지 않는 대기 이벤트를 확인할 수 있다. - 읽기 복제본에서만 느린 쿼리는 실행 계획뿐 아니라 replication lag, reader class, 캐시 warm-up, 병렬 부하를 함께 점검해야 한다.
즉, Aurora에서도 EXPLAIN은 계획 해석의 핵심 도구지만, 스토리지와 인스턴스 운영 지표를 함께 보아야 한다. 계획은 같아도 지연 원인은 다를 수 있다.
12. 흔한 오해와 주의사항
12.1 key가 있으면 좋은 계획이라는 오해
key가 표시되면 인덱스를 사용했다는 뜻이다. 그러나 인덱스를 사용했다는 사실과 좋은 계획이라는 판단은 다르다. 선택도가 낮은 인덱스를 통해 대량 row를 읽고 다시 테이블 row를 lookup하면, full scan보다 더 비쌀 수도 있다.
12.2 rows를 실제 row 수로 보는 오해
rows는 추정값이다. 실제 row 수가 아니다. 실제값과 추정값이 크게 다르면 통계, 데이터 분포, 조건식 형태, Histogram 필요 여부를 의심해야 한다.
12.3 Using filesort를 디스크 장애로 단정하는 오해
Using filesort는 별도 정렬 알고리즘 사용을 뜻한다. 반드시 디스크 파일을 쓴다는 의미가 아니다. 정렬 대상이 작고 LIMIT가 작으면 큰 문제가 아닐 수 있다. 반대로 대량 row 정렬과 임시 테이블이 결합되면 심각한 병목이 될 수 있다.
12.4 작은 테이블의 ALL을 과잉 대응하는 오해
작은 lookup table이나 매우 작은 dimension table은 full scan이 더 단순하고 빠를 수 있다. 모든 ALL을 없애려는 접근은 불필요한 인덱스와 복잡도를 만들 수 있다.
12.5 EXPLAIN만 보고 운영 변경을 확정하는 오해
실행 계획은 중요한 증거지만 전체 증거는 아니다. 실제 지연 시간, row examined, lock wait, I/O wait, CPU 사용률, 애플리케이션 호출 패턴을 함께 보아야 한다.
13. DBA 점검 체크리스트
EXPLAIN을 볼 때 다음 항목을 순서대로 점검한다.
-
EXPLAIN - 테이블 접근 순서가 예상과 다른 경우, 그 이유를
rows - 큰 테이블에서
type=ALL또는 넓은range -
possible_keys는 있는데key=NULL - 선택된
key -
rows × filtered / 100 -
Using temporary,Using filesort - slow log의
Rows_examined와EXPLAIN rows
14. 결론: EXPLAIN은 Optimizer와 대화하는 방법이다
EXPLAIN은 단순한 표가 아니라 MySQL Optimizer가 SQL을 어떻게 이해했는지 보여 주는 인터페이스다. type은 접근 방식, key는 선택된 인덱스, rows와 filtered는 row 흐름 추정, Extra는 부가 작업과 최적화 단서를 제공한다. 이 컬럼들을 따로 외우기보다 하나의 실행 경로로 연결해서 읽어야 한다.
운영자가 해야 할 일은 EXPLAIN 결과를 보고 즉시 결론을 내리는 것이 아니다. 계획을 통해 가설을 세우고, 실제 실행 정보로 검증하며, 통계·인덱스·SQL 구조·업무 조건 중 어느 층을 바꿀지 결정하는 것이다. 다음 글들에서는 이 기본 해석을 바탕으로 EXPLAIN FORMAT=JSON, EXPLAIN ANALYZE, 조인 순서와 인덱스 설계의 세부 판단을 더 깊게 다룰 수 있다.