데이터베이스 용어 사전
쿼리 최적화EXPLAIN · execution plan · EXPLAIN ANALYZE

실행계획

옵티마이저가 고른 SQL 실행 방법. 느린 쿼리는 추측이 아니라 이것부터 봐야 한다.

옵티마이저가 이 SQL을 실제로 어떻게 실행할지 결정한 결과물. 튜닝의 출발점이다.

왜 계획이 여러 개인가

SQL은 "무엇"만 선언하고 "어떻게"는 안 적는다. SELECT * FROM orders o JOIN member m ON ... WHERE ... 한 줄을 실행하는 방법은 수십 가지다.

어느 테이블을 먼저 읽을까 인덱스를 탈까 풀 스캔할까 조인을 Nested Loop · Hash · Merge 중 무엇으로 할까

그리고 방법 사이의 속도 차이가 수천 배까지 난다. 그 선택을 옵티마이저가 대신 하고, 결과가 실행계획이다.

EXPLAIN과 EXPLAIN ANALYZE는 다르다

  • EXPLAIN — 계획만 보여준다 (추정치). 쿼리를 실행하지 않는다
  • EXPLAIN ANALYZE — 실제로 실행해 진짜 시간과 행 수까지 보여준다

진단할 때는 반드시 ANALYZE를 본다. 추정만 봐서는 통계가 거짓말을 하는지 알 수 없기 때문이다.

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE member_id = 42 ORDER BY created_at DESC LIMIT 20;
Limit  (actual time=0.05..0.07 rows=20)
  ->  Index Scan using idx_orders_member_created on orders    ← 인덱스로 바로 진입(좋음)
        Index Cond: (member_id = 42)
        (estimated rows=18   actual rows=20)                  ← 추정 ≈ 실제 (통계 건강)

읽는 세 가지 요령

① 가장 안쪽(들여쓰기가 깊은) 노드부터 실행된다. 데이터가 안에서 밖으로 흐른다.

estimated rows vs actual rows의 차이를 본다. 추정 100인데 실제 100만이면 통계가 거짓말을 한 것이고, 나쁜 계획의 원흉이다. 이 경우 인덱스나 쿼리를 고치기 전에 통계부터 갱신한다.

③ 큰 테이블에 Seq Scan이 보이면 인덱스 누락이나 인덱스 무력화를 의심한다.

MySQL은 type 칼럼이 핵심 신호다

  • 좋은 순서
  • const — PK로 1행
  • eq_ref — 유니크 인덱스 조인
  • ref — 인덱스 동치
  • range — 범위
  • index — 인덱스 풀스캔
  • ALL — 풀 테이블 스캔 ← 큰 테이블이면 빨간불

Extra 칼럼의 Using filesort·Using temporary 도 경고다 — 정렬이나 임시 테이블을 따로 만든다는 뜻이라 비싸다. 인덱스 순서를 ORDER BY에 맞추면 filesort가 사라진다.

면접 함정

  • "EXPLAIN이 쿼리를 실행한다"EXPLAIN만으로는 실행하지 않는다. ANALYZE를 붙여야 실행한다(그래서 UPDATE/DELETE에 쓸 때는 트랜잭션으로 감싸고 롤백한다).
  • "계획이 항상 같다" → 통계·데이터량·파라미터에 따라 바뀐다. 운영과 개발 DB의 계획이 다른 게 정상이다.

UPDATE·DELETE의 계획도 안전하게 볼 수 있다

-- ANALYZE 는 실제로 실행하므로 트랜잭션으로 감싸고 롤백한다
BEGIN;
EXPLAIN (ANALYZE, BUFFERS) DELETE FROM orders WHERE created_at < '2020-01-01';
ROLLBACK;

계획이 바뀌는 걸 잡아내기

-- PostgreSQL: 느린 쿼리를 총합 기준으로 뽑는다
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT calls, mean_exec_time, total_exec_time, query
  FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;

여기서 중요한 통찰 하나 — "가장 느린 한 방"보다 "자주 불려 총합이 큰 쿼리" 가 대개 더 아프다.

1ms 짜리가 100만 번총 1,000초
1초 짜리가 10번총 10초
  • 우선순위는 평균이 아니라 호출수 × 평균 의 총합으로 잡는다

MySQL은 slow query log와 sys 스키마로 같은 일을 한다.

SELECT * FROM sys.statement_analysis ORDER BY total_latency DESC LIMIT 10;

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 쿼리 최적화 — 옵티마이저·실행계획·EXPLAIN