SQL/성능 개선

EXPLAIN ANALYZE 활용법

가지코딩 2025. 7. 4. 01:15

EXPLAIN ANALYZE란?

MySQL에서 쿼리 실행 계획뿐만 아니라, 쿼리를 실제로 실행해 본 후 실제 실행 시간과 처리된 행 수 같은 실행 통계까지 함께 보여주는 명령어

  • 단순 EXPLAIN과 달리, 쿼리를 실제 실행하므로 정확한 성능 정보를 알 수 있다.
  • 옵티마이저가 예측한 실행 계획과 실제 수행 결과를 비교해, 실행 계획의 적절성을 검증할 수 있다.

EXPLAIN ANALYZE 활용법 및 개선 방향

  • 실제 실행 시간과 옵티마이저 예상 비용을 비교한다.
  • 조인 방식(Nested Loop, Hash Join 등)과 인덱스 활용 여부를 확인한다.
  • 불필요한 컬럼 조회를 피하고, 필요한 컬럼만 선택한다. (커버링 인덱스 유도)
  • 통계 정보를 최신으로 유지해 옵티마이저가 정확한 계획을 세우도록 한다.
  • 변경 전후 성능을 수치로 비교해 튜닝 효과를 검증한다.

EXPLAIN ANALYZE 활용 예제

EXPLAIN ANALYZE
SELECT o.order_id, o.total_price, c.customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.total_price > 1000000
AND c.region = 'Seoul';

 

예상 결과 예시

  • orders 테이블이 테이블 스캔을 수행하고 있음 (cost=15000, 실제 120ms 소요)
  • customers 테이블 인덱스 조회는 인덱스가 있으나, orders 테이블에서 4800건을 모두 스캔한 후 각각에 대해 4800번 조회 수행 (loops=4800)
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                                                                                             |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop join                                                                                                                                                |
|    -> Table scan on orders  (cost=15000 rows=5000) (actual time=120.500..122.700 rows=4800 loops=1)                                                                |
|    -> Index lookup on customers using idx_customers_region (region = 'Seoul')  (cost=100 rows=50) (actual time=0.5..0.6 rows=48 loops=4800)                         |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.130 sec)

 

 

결과 해석

  • Nested loop join 방식으로 두 테이블을 조인함을 확인할 수 있다.
    • 두 테이블을 조인할 때, 외부 테이블(orders)을 한 행씩 순회하면서 내부 테이블(customers)에서 해당 조건에 맞는 행을 찾는 방식
  • orders 테이블에 대해 풀 테이블 스캔을 수행하고, 그 행(4800개) 각각에 대해 customers 테이블에서 region='Seoul' 인덱스를 이용해 고객을 찾는다.
  • 내부 테이블 인덱스 조회가 외부 테이블의 모든 행마다 반복되므로, 비용이 많이 든다.

 

쿼리 튜닝 방법

더보기

1. 조인 방식 변경

  • MySQL 기본 조인 방식은 Nested Loop Join이다.
  • 큰 테이블을 먼저 필터링하거나 작은 테이블을 먼저 탐색하도록 조인 순서를 조정하여 성능 개선 가능
  • STRAIGHT_JOIN 키워드로 조인 순서를 강제할 수 있다.
  • 서브쿼리, 윈도우 함수 등 MySQL 8+ 기능을 활용해 복잡한 조인을 간결하게 표현하고 최적화 시도 가능

2. 인덱스 추가

  • 조인 조건 컬럼(customer_id 등)과 WHERE 절 필터 컬럼(total_price, region)에 인덱스를 추가
  • 복합 인덱스 생성으로 다중 컬럼 조건을 한 번에 커버해 디스크 I/O 감소
  • 인덱스 컬럼 순서도 중요, WHERE와 JOIN 조건 우선순위에 맞게 설계
  • 불필요한 인덱스는 제거해 쓰기 성능 저하 방지

3. 쿼리 리팩토링

  • 필요한 컬럼만 조회 (SELECT * 지양)
  • 복잡한 쿼리는 뷰(View)나 임시 테이블로 분할해 가독성 및 유지보수성 향상
  • 서브쿼리를 조인으로 변환해 옵티마이저가 더 좋은 계획 선택하도록 유도
  • 불필요한 함수 호출, 계산 제거로 비용 절감
  • LIMIT, OFFSET 등 페이징 조건 최적화 (예: keyset pagination)

4. 기타 팁

  • ANALYZE TABLE로 통계 정보 갱신
  • 실행 계획과 실제 실행 시간 간 차이가 크면 데이터 분포 및 통계 문제 의심
  • 대용량 데이터는 파티셔닝도 검토

 

 

리팩토링 후 쿼리 예시

  • orders 테이블에 total_price에 대한 인덱스 추가
  • 서브쿼리로 필터링된 고객만 조인
EXPLAIN ANALYZE
SELECT o.order_id, o.total_price, c.customer_name
FROM orders o
JOIN (
  SELECT customer_id, customer_name
  FROM customers
  WHERE region = 'Seoul'
) c ON o.customer_id = c.customer_id
WHERE o.total_price > 1000000;

 

예상 결과 예시

  • orders 테이블이 Index lookup을 수행해 필터링 비용이 크게 줄었음 (cost=500, 실제 5ms)
  • customers 테이블도 인덱스 조회를 하고, orders에서 필터링된 190건에 대해서만 190번 루프 실행 (loops=190)
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                                                                                             |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop join                                                                                                                                                |
|    -> Index lookup on orders using idx_orders_total_price (total_price > 1000000)  (cost=500 rows=200) (actual time=5.0..5.5 rows=190 loops=1)                      |
|    -> Index lookup on customers using idx_customers_region (region = 'Seoul')  (cost=100 rows=50) (actual time=0.5..0.6 rows=48 loops=190)                          |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.008 sec)

 

결과 해석

  • Nested loop join 방식 사용
    • MySQL 에서는 조인 알고리즘을 직접 지정하기 어렵다...
    • 특정 DBMS 에서는 조인 힌트(Hint) 를 통해 조인 방식을 명시하거나 유도할 수 있다
  • orders 테이블에서 total_price > 1000000 조건으로 인덱스를 통해 190개 정도의 행만 필터링해서 가져옴 → 훨씬 효율적이다.
  • 이후 각 190개의 주문 행마다 customers 테이블에서 region='Seoul' 조건으로 인덱스 조회를 수행함.
  • orders 테이블에 인덱스를 활용해 필터링 후 조인하여, 외부 루프가 훨씬 줄어든 덕분에 쿼리가 더 빠르게 실행되었다.

'SQL > 성능 개선' 카테고리의 다른 글

Index Lookup vs Covering Index Lookup  (1) 2025.07.04