blog post cover

EXPLAIN과 병목 탐색

이 파트는 “느린 이유를 증명하는 과정”이다. gym_booking에서 가장 느린 쿼리는 활성 회원 집계였다. EXPLAIN을 보면 왜 그런지 바로 드러난다.


기준 쿼리

EXPLAIN ANALYZE
SELECT g.id AS gym_id, g.name,
       COUNT(*) AS active_members_count
FROM gyms g
JOIN memberships m ON m.gym_id = g.id
JOIN user_memberships um ON um.membership_id = m.id
WHERE NOW() BETWEEN um.started_at AND um.ended_at
GROUP BY g.id, g.name
ORDER BY active_members_count DESC
LIMIT 10;

현재 인덱스 상태

  • user_memberships: PRIMARY(id), fk_um_user(user_id), fk_um_membership(membership_id)
  • memberships: PRIMARY(id), fk_memberships_gym(gym_id)
  • started_at, ended_at 인덱스 없음

실제 EXPLAIN ANALYZE 출력

-> Limit: 10 row(s)  (actual time=2683..2683 rows=10 loops=1)
    -> Sort: active_members_count DESC, limit input to 10 row(s) per chunk
       (actual time=2683..2683 rows=10 loops=1)
        -> Table scan on <temporary>
           (actual time=2672..2679 rows=49965 loops=1)
            -> Aggregate using temporary table
               (actual time=2672..2672 rows=49965 loops=1)
                -> Nested loop inner join  (cost=199355 rows=90802)
                   (actual time=5.76..2539 rows=56926 loops=1)
                    -> Nested loop inner join  (cost=107714 rows=90802)
                       (actual time=5.29..1928 rows=56926 loops=1)
                        -> Filter: (<cache>(now()) between um.started_at
                           and um.ended_at)  (cost=11997 rows=90802)
                           (actual time=4.76..461 rows=56926 loops=1)
                            -> Table scan on um  (cost=11997 rows=817304)
                               (actual time=4.45..394 rows=901312 loops=1)
                        -> Single-row index lookup on m using PRIMARY
                           (id=um.membership_id)  (cost=0.954 rows=1)
                           (actual time=0.0255..0.0256 rows=1 loops=56926)
                    -> Single-row index lookup on g using PRIMARY
                       (id=m.gym_id)  (cost=0.909 rows=1)
                       (actual time=0.0105..0.0105 rows=1 loops=56926)

EXPLAIN ANALYZE 출력

판단

  • 부적절: 핵심 테이블 user_memberships가 풀스캔으로 접근된다.
  • 근거: “90만 행을 읽어 56,926행만 통과한다.”
  • 원인: started_at/ended_at 조건이 인덱스를 타지 못한다.
  • 힌트: 더미 데이터 특성상 started_at <= NOW()는 항상 참이다. 실질 필터는 ended_at >= NOW() 하나다.

즉, 인덱스도, 조건도 다시 생각해야 한다.


병목을 찾는 절차

  1. EXPLAIN ANALYZE 실행
  2. 실제 시간과 rows가 큰 구간 확인
  3. 필터가 인덱스를 타는지 확인
  4. 인덱스/조건 재설계
  5. 다시 EXPLAIN으로 검증

다음 파트에서 실제로 인덱스를 적용하고, 조건을 바꾸면서 실행 계획을 변화시킨다.