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건 |
문제점
- dsst 테이블을 먼저 읽음
- 213,234건을 모두 메모리에 로드
- filtered = 100% → 필터링 없이 전체 데이터 처리
- 비효율적인 조인 순서
mjc(1건) → rfc(1,236건) → mnc(12건) → dsst(213,234건) → store_mnc
- 큰 테이블(dsst)을 먼저 처리 후 작은 테이블과 조인
- 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건만 처리 |
개선 포인트
- store_mnc를 먼저 필터링
- 1,400건에서 매칭되는 store 목록 먼저 추출
- 이후 dsst에서 해당 store_cd_id만 조회
- 효율적인 조인 순서
mjc(1건) → store_mnc(1,400건) → rfc(14건) → mnc(14건) → dsst(21,323건)
- 작은 테이블들로 먼저 필터링
- dsst는 필터링된 store_cd_id만 조회
- 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% 감소 |
| 최종 조인 | 전체 데이터 조인 후 필터링 | 필터링 후 필요한 것만 조인 | - |
왜 빨라졌는가?
- 조인 순서 변경
- 작은 테이블(store_mnc) 먼저 → 필터링된 목록 생성
- 큰 테이블(dsst)은 마지막에 필터링된 조건으로만 조회
- 메모리 사용량 감소
- Hash Join에 사용되는 메모리: 213,234건 → 21,323건
- 약 90% 메모리 절약
- 디스크 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 등을 사용하여 조인 순서를 강제 하는 등의 방법을 사용할 수 있을거 같음
'개발 고민' 카테고리의 다른 글
| [개발 고민] webSocket, SSE(Server Sent Event) 중 뭐가 좋을까? (0) | 2026.01.18 |
|---|---|
| [개발일지_safeHome] 12. 등기부등본 분석 작업 관련 DB 설계 및 서비스 개발(임시) (0) | 2026.01.08 |
| [Java, Spring] Lombok @Data 쓰지 말아야 하는 이유 (0) | 2025.11.01 |
| 멀티테넌시 아키텍처 (0) | 2025.04.02 |
| SaaS와 멀티테넌시 (0) | 2025.04.02 |