개발 고민

[개발 고민]MySQL 조인 순서 최적화로 쿼리 속도 개선하기

woopii 2026. 1. 16. 12:36

MySQL 쿼리 성능 개선 분석


1. 기존 쿼리 (느림)

sql
SELECT store_repo_rfc.id    AS storeRepoId
     , store_repo_rfc.name  AS storeRepoName
     , SUM(dsst.tot_amt)    AS totAmt
     , SUM(dsst.net_amt)    AS netAmt
  FROM day_sum_sale_teams dsst
 INNER JOIN minor_codes store_mnc ON dsst.store_cd_id = store_mnc.minor_id
 INNER JOIN (
    SELECT rfc.ref_code AS code
         , mnc.minor_name AS name
         , mnc.minor_id AS id
      FROM minor_codes mnc
     INNER JOIN major_codes mjc ON mjc.major_code_id = mnc.major_code_id
     INNER JOIN ref_codes rfc ON rfc.minor_code_id = mnc.minor_id
     WHERE mjc.account_id = 9
       AND mjc.major_code = 'store_repo_map'
       AND mjc.use_yn = 'Y'
       AND mjc.deleted_at IS NULL
       AND mnc.use_yn = 'Y'
       AND mnc.deleted_at IS NULL
       AND rfc.use_yn = 'Y'
       AND rfc.deleted_at IS NULL
) store_repo_rfc ON store_mnc.minor_code = store_repo_rfc.code
 WHERE account_id = 9
   AND visit_date BETWEEN '20250101' AND '20251219'
 GROUP BY store_repo_rfc.id;

실행 계획 (느린 쿼리)

 

순서 테이블 타입 Rows Filtered 실제 처리량
1 mjc ref idx_major_codes_unique 1 10% 1건
2 rfc ALL - 1,236 1% 1,236건 (Full Scan)
3 mnc eq_ref PRIMARY 1 5% ~12건
4 dsst range PRIMARY 213,234 100% 213,234건 전체
5 store_mnc eq_ref PRIMARY 1 10% ~21,323건

문제점

  1. dsst 테이블을 먼저 읽음
    • 213,234건을 모두 메모리에 로드
    • filtered = 100% → 필터링 없이 전체 데이터 처리
  2. 비효율적인 조인 순서
 
   mjc(1건) → rfc(1,236건) → mnc(12건) → dsst(213,234건) → store_mnc
  • 큰 테이블(dsst)을 먼저 처리 후 작은 테이블과 조인
  1. Hash Join 사용
    • 큰 데이터셋으로 Hash Table 생성 → 메모리 부하

2. 개선 쿼리 (빠름)

 
 
sql
SELECT mnc.minor_id      AS storeRepoId
     , mnc.minor_name    AS storeRepoName
     , SUM(dsst.tot_amt) AS totAmt
     , SUM(dsst.net_amt) AS netAmt
  FROM major_codes mjc
 INNER JOIN minor_codes mnc ON mjc.major_code_id = mnc.major_code_id
 INNER JOIN ref_codes rfc ON rfc.minor_code_id = mnc.minor_id
 INNER JOIN minor_codes store_mnc ON store_mnc.minor_code = rfc.ref_code
 INNER JOIN day_sum_sale_teams dsst ON dsst.store_cd_id = store_mnc.minor_id
   AND dsst.account_id = 9
   AND dsst.visit_date BETWEEN '20250101' AND '20251219'
 WHERE mjc.account_id = 9
   AND mjc.major_code = 'store_repo_map'
   AND mjc.use_yn = 'Y'
   AND mjc.deleted_at IS NULL
   AND mnc.use_yn = 'Y'
   AND mnc.deleted_at IS NULL
   AND rfc.use_yn = 'Y'
   AND rfc.deleted_at IS NULL
   AND store_mnc.use_yn = 'Y'
   AND store_mnc.deleted_at IS NULL
 GROUP BY mnc.minor_id, mnc.minor_name;

실행 계획 (빠른 쿼리)

순서 테이블 타입 Rows Filtered 실제 처리량
1 mjc ref idx_major_codes_unique 1 10% 1건
2 store_mnc ALL - 1,400 1% 1,400건 (Full Scan)
3 rfc ref idx_ref_codes_02 5 1% ~14건
4 mnc eq_ref PRIMARY 1 5% ~14건
5 dsst range PRIMARY 213,234 10% ~21,323건만 처리

개선 포인트

  1. store_mnc를 먼저 필터링
    • 1,400건에서 매칭되는 store 목록 먼저 추출
    • 이후 dsst에서 해당 store_cd_id만 조회
  2. 효율적인 조인 순서
 
   mjc(1건) → store_mnc(1,400건) → rfc(14건) → mnc(14건) → dsst(21,323건)
  • 작은 테이블들로 먼저 필터링
  • dsst는 필터링된 store_cd_id만 조회
  1. dsst 처리량 90% 감소
    • filtered = 10% → 213,234건 중 21,323건만 실제 처리
    • 불필요한 데이터 로드 방지

3. 핵심 차이점 요약

데이터 처리량 비교

단계 기존 개선 차이
초기 필터링 dsst 213,234건 전체 store_mnc 1,400건 99% 감소
dsst 처리량 213,234건 (100%) 21,323건 (10%) 90% 감소
최종 조인 전체 데이터 조인 후 필터링 필터링 후 필요한 것만 조인 -

왜 빨라졌는가?

  1. 조인 순서 변경
    • 작은 테이블(store_mnc) 먼저 → 필터링된 목록 생성
    • 큰 테이블(dsst)은 마지막에 필터링된 조건으로만 조회
  2. 메모리 사용량 감소
    • Hash Join에 사용되는 메모리: 213,234건 → 21,323건
    • 90% 메모리 절약
  3. 디스크 I/O 감소
    • dsst에서 읽어야 할 페이지 수 감소
    • PRIMARY KEY range scan 효율 향상

4. 결론

주요 변경점 요인

- 조인 순서를 변경하여 큰 테이블(dsst)을 처리하기 전에 작은 테이블들로 필터링 조건을 먼저 생성

(작은 테이블로 먼저 필터링 → 큰 테이블의 필요한 부분만 조회하는 원칙을 가져감)

 

성능 개선 메커니즘:

기존: dsst(213K 전체) → store_mnc 매칭 → 결과 필터링
개선: store_mnc 필터링 → dsst(21K만) 조회 → 결과 집계

측정 가능한 개선:

  • 쿼리 실행 시간: 1.5초 → 0.5~0.8초 (약 50% 개선)
  • dsst 처리량: 213,234건 → 21,323건 (90% 감소)
  • 메모리 사용량: 약 90% 절약

 

추가 고려사항

처음 쿼리에 서브쿼리를 사용한 이유는 가독성을 위해서 였는데

조인 순서를 조절하기 위해서 서브쿼리 없앴다. 그래서 가독성이 떨어지게 됨

서브쿼리를 유지한 채로 순서만 바꾼다면 조인 순서를 변경할 수 있는거 아닌가?

(store_mnc -> dsst)

 

❌ 테스트 결과 -> 순서 바뀌지 않음

 

✅ 이유

옵티마이저가 조인절에 있는 서브쿼리가 조인도 3개나 걸려있고,

예상 비용(컴퓨팅 자원)을 정확하게 예측할 수 없어서

보수적으로 소비되는 비용이 많을것이다 라고 판단하여(불확실한 판단)
쿼리 상에서 (store_mnc -> dsst) 이렇게 하더라도 옵티마이저 판단으로 dsst를 먼저 읽은 것으로 보인다

 

그럼 해결 방법은?

쿼리 개선에서처럼 서브쿼리를 제거하고 Flat하게 가거나(불확실한 판단을 제거),
(Mysql인 경우)Straight join 등을 사용하여 조인 순서를 강제 하는 등의 방법을 사용할 수 있을거 같음