SQL 튜닝 입문 — 실행계획(EXPLAIN)으로 성능 분석하기 실무 가이드
SQL은 한 줄로 데이터를 꺼내는 단순한 언어처럼 보이지만, 성능이 나쁘면 서비스 전체가 느려집니다.
이번 글에서는 실행계획(EXPLAIN)을 활용해 SQL이 실제로 어떻게 동작하는지, 어떤 인덱스를 사용하는지, 그리고 어떤 부분을 개선해야 하는지를 초보자도 따라 할 수 있도록 단계별로 설명합니다.

1. 실행계획(EXPLAIN)이란?
DB는 단순히 SELECT 문을 실행하는 게 아니라, 내부적으로 “어떤 테이블부터 읽고, 어떤 인덱스를 사용할지”를 계획(plan)으로 세운 뒤 실행합니다.
이 과정을 눈으로 확인하게 해주는 명령이 바로 EXPLAIN입니다.
EXPLAIN SELECT * FROM users WHERE age >= 30;
이 명령을 실행하면 DB는 테이블을 어떻게 읽을지 요약해 보여줍니다.
MySQL, MariaDB, PostgreSQL 등 대부분의 DBMS가 이 기능을 제공합니다.
2. 실행계획 결과에서 꼭 봐야 할 컬럼
MySQL 기준으로 EXPLAIN의 결과는 다음과 같습니다.
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+
| id | select_type | table | type | key | rows | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+
| 1 | SIMPLE | users | ALL | NULL | 1000 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+
각 컬럼의 의미를 알아봅시다.
- type: 테이블 접근 방식 (ALL, index, range, ref, eq_ref, const …)
- key: 사용된 인덱스
- rows: 예상 읽기 행 수 (작을수록 좋음)
- Extra: 추가 정보 (Using where, Using filesort 등)
💡 핵심: type이 ALL이면 전체 테이블 스캔으로 느립니다. 인덱스를 타면 range나 ref로 표시됩니다.
3. 인덱스 없이 느린 쿼리의 예
예를 들어, 회원 테이블이 다음과 같다고 가정해 봅시다.
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
age INT,
city VARCHAR(50)
);
다음 쿼리를 실행해볼까요?
EXPLAIN SELECT * FROM users WHERE city = 'Seoul';
결과의 type이 ALL이라면 인덱스가 없다는 뜻입니다.
즉, 모든 행을 읽어서 비교하는 “Full Table Scan”이 일어납니다.
💡 해결:ALTER TABLE users ADD INDEX idx_city (city);
다시 실행하면 type이 ref 또는 range로 바뀌며, rows 수가 눈에 띄게 줄어듭니다.
4. JOIN 쿼리의 실행계획 이해
JOIN은 SQL 튜닝의 핵심입니다.
실행계획을 보면 어떤 테이블이 먼저 읽히고, 어떤 조건에서 JOIN이 일어나는지를 알 수 있습니다.
EXPLAIN
SELECT u.name, o.order_date
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.order_date >= '2025-11-01';
결과 예시:
+----+-------------+-------+------------+------+---------------+------+---------+------+----------------+
| id | select_type | table | type | key | rows | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+----------------+
| 1 | SIMPLE | o | range | idx_order_date | 200 | Using where |
| 1 | SIMPLE | u | eq_ref | PRIMARY | 1 | |
+----+-------------+-------+------------+------+---------------+------+---------+------+----------------+
💡 읽는 요령:
- id가 같은 값이면 같은 쿼리 블록
- 위에 있는 테이블(o)이 먼저 읽힘 (드라이빙 테이블)
- u는 eq_ref → PK 매칭으로 효율적 JOIN
💬 문제 패턴: JOIN 순서가 잘못되면 rows가 급증하고 느려집니다. MySQL 8 이상에서는 EXPLAIN FORMAT=JSON으로 더 직관적인 시각적 트리를 확인할 수 있습니다.
5. Extra 컬럼에서 성능 힌트 찾기
Extra 컬럼은 실행계획의 숨은 보석입니다.
다음 단어가 있으면 반드시 확인하세요.
- Using filesort: 정렬용 임시 파일 생성 → 인덱스로 대체 가능
- Using temporary: 임시 테이블 생성 → GROUP BY/ORDER BY 최적화 필요
- Using where: 조건 필터링 발생 → 인덱스 누락 가능성
💡 예시:Using filesort가 나온다면 ORDER BY 절에 인덱스 컬럼을 포함시켜야 합니다.
SELECT * FROM users ORDER BY city;
-- 해결
CREATE INDEX idx_city ON users(city);
6. 실행계획 비교로 쿼리 개선하기
다음 두 쿼리를 비교해봅시다.
-- 느린 쿼리
EXPLAIN SELECT * FROM orders WHERE YEAR(order_date) = 2025;
-- 개선된 쿼리
EXPLAIN SELECT * FROM orders WHERE order_date BETWEEN '2025-01-01' AND '2025-12-31';
💡 차이점:
- 첫 번째 쿼리는 YEAR() 함수로 컬럼을 가공 → 인덱스 미사용
- 두 번째 쿼리는 범위 조건 → 인덱스 사용 가능
즉, 컬럼을 함수로 감싸면 인덱스를 탈 수 없습니다.
이런 미세한 차이가 성능을 수십 배 좌우합니다.
7. 쿼리 실행 순서 시각화 (FORMAT=JSON)
MySQL 8.0 이상에서는 EXPLAIN에 FORMAT=JSON 옵션을 주면 실행계획이 트리 구조로 표시됩니다.
EXPLAIN FORMAT=JSON
SELECT * FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'PAID';
💡 이 결과를 Visual Explain Tool에 붙여넣으면 시각적으로 어떤 테이블이 먼저 읽히는지 확인할 수 있습니다.
8. 실무 튜닝 루틴 — 성능 점검 절차
SQL 성능 문제를 해결할 때는 아래 절차를 따라가면 좋습니다.
- 느린 쿼리 로그(slow query log)에서 후보 쿼리 추출
- EXPLAIN으로 실행계획 확인
- Full Table Scan, Using filesort 등 비효율 요소 찾기
- 인덱스 추가 또는 쿼리 재작성
- 성능 재측정 (rows, time 비교)
💬 보너스 팁:ANALYZE TABLE 명령으로 통계 정보를 갱신하면 EXPLAIN의 추정치가 더 정확해집니다.
9. 쿼리 캐시와 실제 실행 시간 비교
EXPLAIN은 ‘예상 계획’입니다. 실제 속도는 SHOW PROFILES 명령이나 EXPLAIN ANALYZE로 측정할 수 있습니다.
EXPLAIN ANALYZE SELECT * FROM users WHERE city = 'Seoul';
결과에는 실제 소요 시간(ms)과 읽은 행 수가 함께 표시됩니다.
💡 해석 팁: - “actual rows” 값이 예상보다 많으면 통계 불일치 가능성 - “actual time”이 긴 경우 디스크 I/O 또는 조인 순서 문제
마무리
SQL 튜닝은 단순한 기술이 아니라, 데이터를 효율적으로 다루는 사고방식입니다.
EXPLAIN을 습관처럼 실행하고 결과를 읽는 개발자는 문제의 원인을 추측하지 않고 데이터로 증명합니다.
이제 로그에서 문제를 찾고, 실행계획으로 병목을 분석하는 단계까지 왔습니다.
다음 글에서는 “JDBC 커넥션 풀(Connection Pool) 이해하기 — HikariCP 실무 설정”을 다루며 DB 연결 효율까지 완성해보겠습니다.
'Database & Storage' 카테고리의 다른 글
| API 로그 추적과 모니터링 — APM으로 성능 시각화하기 실무 가이드 (1) | 2025.11.19 |
|---|---|
| JDBC 커넥션 풀(Connection Pool) 이해하기 — HikariCP 실무 설정 가이드 (0) | 2025.11.17 |
| 개발자가 자주 잊어버리는 SQL 기본 명령어 7가지 (JOIN, GROUP BY 실습) (0) | 2025.11.10 |
| SQL 쿼리 정렬과 주석 달기 실전 가이드 — 초보 개발자도 읽히는 쿼리 쓰는 법 (1) | 2025.11.07 |
| Loki + Promtail + Grafana로 로그 수집·검색·알림: 가벼운 ELK 대안 구축(단일 서버 → 다중 서버 확장) (0) | 2025.10.28 |