Last Updated on 12월 26, 2025 by Jade(정현호)
Contents
New Feature
MySQL 8.0.18 버전은 2019년 10월 14일에 GA 가 되었으며 8.0.18 의 New Feature 로 EXPLAIN ANALYZE 기능이 추가 되었습니다.
EXPLAIN ANALYZE
MySQL 를 시작 하면서 실행계획 관련하여 처음 검색 과 확인 해본 내용이 통계정보를 기반의 estimated rows 가 아니라 actual rows 를 볼수 있는 기능이 있는지 확인해봤던 적이 있었습니다.
Oracle 에서는 10046 event 를 시작으로 gather_plan_statistics 힌트, _rowsource_execution_statistics 파라미터 , monitor 힌트 또는 5초 이상의 수행 등 과 같이 실제 수행된 SQL Plan 의 Actual Row Source Execution 정보를 확인 할 수 있는 방법이 있습니다.
하지만 MySQL 의 explain 은 통계정보를 기반으로 한 estimated rows 정보만 제공하고 있었기 때문에 오퍼레이션 별 정확한 행수가 아니었으며 일치되지 않았습니다.
8.0.18 에서 부터 explain analyze 기능이 추가되었으며 해당 기능은 쿼리 실행에 소비되는 시간과 수행에 처리되는 rows 그리고 측정한 결과 건수 등을 쿼리 결과 대신에 측정된 결과 값을 보여주게 됩니다 즉 수행한 SQL의 Actual rows 정보를 보여주게 됩니다.
간략하게 아래와 같이 테스트를 통해 내용을 확인 해보도록 하겠습니다.
[참고] 포스팅 테스트 MySQL 버전: 8.0.23
• 테스트 테이블 , 데이터 생성
mysql> create database test;
mysql> use test;
mysql> CREATE TABLE t1 (
c1 INTEGER DEFAULT NULL,
c2 INTEGER DEFAULT NULL
);
mysql> insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5);
mysql> insert into t1
select * from t1
union all
select * from t1
union all
select * from t1
union all
select * from t1;
-- insert select 문 x 3 더 수행
mysql> CREATE TABLE t2 (
c1 INTEGER DEFAULT NULL,
c2 INTEGER DEFAULT NULL
);
mysql> insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5);
mysql> insert into t2
select * from t2
union all
select * from t2
union all
select * from t2
union all
select * from t2;
-- insert select 문 x 3 더 수행
mysql> CREATE TABLE t3 (
pk INTEGER NOT NULL auto_increment PRIMARY KEY,
i INTEGER DEFAULT NULL
);
mysql> insert into t3(i) values (1),(2),(3),(4),(5);
mysql> insert into t3(i)
select i from t3
union all
select i from t3
union all
select i from t3
union all
select i from t3;
-- insert select 문 x 3 더 수행
• 쿼리 플랜 확인
쿼리 수행은 Count(*) 를 통해 쿼리의 결과 수를 확인 하고 기존 방식의 explain 을 수행 한 결과 그리고 explain analyze 를 수행한 결과 순으로 실행한 결과 입니다. explain 도 format=tree 를 사용하여 같은 출력 형태로 진행하였습니다.
---- Query 1
mysql> SELECT count(*) FROM t1 JOIN t2 ON (t1.c1 = t2.c2);
+----------+
| count(*) |
+----------+
| 1953125 |
+----------+
mysql> EXPLAIN format=tree
SELECT * FROM t1 JOIN t2 ON (t1.c1 = t2.c2)\G
*************************** 1. row ***************************
EXPLAIN: -> Inner hash join (t2.c2 = t1.c1) (cost=976879.74 rows=976563)
-> Table scan on t2 (cost=0.01 rows=3125)
-> Hash
-> Table scan on t1 (cost=314.75 rows=3125)
mysql> EXPLAIN ANALYZE
SELECT * FROM t1 JOIN t2 ON (t1.c1 = t2.c2)\G
*************************** 1. row ***************************
EXPLAIN: -> Inner hash join (t2.c2 = t1.c1) (cost=976879.74 rows=976563) (actual time=1.229..74.317 rows=1953125 loops=1)
-> Table scan on t2 (cost=0.01 rows=3125) (actual time=0.006..2.386 rows=3125 loops=1)
-> Hash
-> Table scan on t1 (cost=314.75 rows=3125) (actual time=0.017..0.983 rows=3125 loops=1)
---- Query 2
mysql> SELECT count(*) FROM t3 WHERE i > 2;
+----------+
| count(*) |
+----------+
| 1875 |
+----------+
mysql> EXPLAIN format=tree
SELECT * FROM t3 WHERE i > 2\G
*************************** 1. row ***************************
EXPLAIN: -> Filter: (t3.i > 2) (cost=314.50 rows=1042)
-> Table scan on t3 (cost=314.50 rows=3125)
mysql> EXPLAIN ANALYZE
SELECT * FROM t3 WHERE i > 2\G
*************************** 1. row ***************************
EXPLAIN: -> Filter: (t3.i > 2) (cost=314.50 rows=1042) (actual time=0.024..0.487 rows=1875 loops=1)
-> Table scan on t3 (cost=314.50 rows=3125) (actual time=0.022..0.350 rows=3125 loops=1)
---- Query 3
mysql> SELECT count(*) FROM t3 WHERE pk > 17;
+----------+
| count(*) |
+----------+
| 3108 |
+----------+
mysql> EXPLAIN format=tree
SELECT * FROM t3 WHERE pk > 17\G
*************************** 1. row ***************************
EXPLAIN: -> Filter: (t3.pk > 17) (cost=622.59 rows=3108)
-> Index range scan on t3 using PRIMARY (cost=622.59 rows=3108)
mysql> EXPLAIN ANALYZE
SELECT * FROM t3 WHERE pk > 17\G
*************************** 1. row ***************************
EXPLAIN: -> Filter: (t3.pk > 17) (cost=622.59 rows=3108) (actual time=0.057..0.604 rows=3108 loops=1)
-> Index range scan on t3 using PRIMARY (cost=622.59 rows=3108) (actual time=0.055..0.453 rows=3108 loops=1)
[참고] 포스팅에서 사용된 버전은 MySQL 8.0.23 으로 hash join 이 사용되고 있습니다.
explain analyze 사용시 확인 되는 내용으로는 먼저 최종 결과 건수가 실제 count 한 결과와 동일하며, 각 오퍼레이션 별 처리 건수 , Cost , actual time 정보 등을 확인 할 수 있습니다.
확인 되는 정보
explain analyze 를 통해 SQL 수행시 아래와 같은 정보를 확인 할 수 있습니다.
• Estimated execution cost
예상 실행 비용
(Some iterators are not accounted for by the cost model, and so are not included in the estimate.)
• Estimated number of returned rows
예상 반환 행 수로 옵티마이저가 쿼리를 실행하기 전에 "이 단계에서 대략 이 정도의 데이터가 나올 것 같다" 라고 추측한 수치
• Time to return first row
첫 번째 행을 반환하는 시간
• Time spent executing this iterator
이 이터레이터(및 자식 이터레이터 포함, 부모 이터레이터는 제외)를 실행하는 데 소요된 시간(밀리초 단위)
루프가 여러 번 발생하는 경우, 이 값은 루프당 평균 시간으로 표시
• Number of rows returned by the iterator
해당 단계(Iterator)가 실행을 마친 후, 실제로 상위 단계로 반환한 행(Row)의 총 개수
필터나 조인 단계를 거쳐서 살아남은 실제 데이터 건수
• Number of loops
수행되는 루프의 수, explain analyze 결과에서 "loops"
• explain analyze 는 항상 tree 출력 형식을 사용
• explain analyze 는 select 문은 물론 update 과 delete 문에서도 사용 가능
• 8.0.19 버전 부터는 TABLE 문과도 사용가능
관련 링크 : TABLE Statement [Link]
• 8.0.20 버전 부터 실행 중 CTRL+C 또는 Kill Query 를 통해서 종료 가능
이와 같은 여러 정보를 확인 할 수 있으며 새로 추가 된 explain analyze 를 통해 확인 가능한 실측 정보를 통해서 조금 더 SQL 최적화 나 튜닝하는데 기존 보다 더 도움이 될 것이라고 예상됩니다
루프(loops) 횟수에 따른 actual time 및 rows 해석
EXPLAIN ANALYZE 결과에서 가장 주의 깊게 봐야 할 지표는 바로 actual time과 rows 입니다. 하지만 이 숫자는 옆에 있는 loops 값이 얼마냐에 따라 그 의미가 달라집니다.
loops=1 일 경우
작업이 단 한 번만 실행된 경우입니다. 주로 테이블 전체를 한 번에 읽거나(Full Table Scan), 쿼리의 가장 상위 단계에서 발생합니다.
(actual time=0.05..2.50 rows=50 loops=1)
- 0.10ms (앞의 숫자): 첫 번째 행을 찾아 리턴하는 데 걸린 시간
- 2.50ms (뒤의 숫자): 모든 행(50건)을 처리하고 작업을 마칠 때까지 걸린 총 시간
- 50건 (rows): 이 작업으로 반환된 총 데이터 건수
loops>1 일 경우
loops가 1보다 큰 경우 시간(time)뿐만 아니라 행 수(rows) 역시 루프당 평균치를 나타냅니다.
-> Nested loop inner join (cost=125.00 rows=500) (actual time=0.10..25.50 rows=500 loops=1)
-> Filter: (orders.status = 'COMPLETED') (cost=10.00 rows=50) (actual time=0.04..2.40 rows=50 loops=1)
-> Index lookup on order_items using order_id (order_id=orders.id) (cost=2.00 rows=10) (actual time=0.01..0.45 rows=10 loops=50)
실제 총 시간 = actual time(뒤의 숫자) x loops
실제 총 행 수 = rows x loops
맨 아래의 "Index lookup on order_items" 에서 보면 다음과 같이 확인할 수 있습니다.
- 0.01ms (앞의 숫자): 첫 번째 데이터를 찾아 리턴하는 데까지 걸린 시간
- 0.45ms (뒤의 숫자): 평균 작업 완료 시간, 한 번의 루프에서 상세 내역 데이터를 모두 처리하고 마칠 때까지 걸린 시간들의 평
- 총 소요 시간: 0.45ms(평균) x 50(loops) = 22.5ms10건 (rows): 평균 반환 행 수
- rows=10: 평균 10건
- 10건(평균) x 50(loops) = 총 건수 500건
이 단계를 모두 거치고 나면 최종적으로 500건의 데이터가 상위 단계(Join)로 전달됩니다.
Reference
Reference link
dev.mysql.com/news-8-0-18.html [Link]
dev.mysql.com/explain.html [Link]
mysql-blog/mysql-explain-analyze [Link]
다른 연관된 글

Principal DBA(MySQL, AWS Aurora, Oracle)
핀테크 서비스인 핀다에서 데이터베이스를 운영하고 있어요(at finda.co.kr)
Previous - 당근마켓, 위메프, Oracle Korea ACS / Fedora Kor UserGroup 운영중
Database 외에도 NoSQL , Linux , Python, Cloud, Http/PHP CGI 등에도 관심이 있습니다
purityboy83@gmail.com / admin@hoing.io
