DB 슬로우 쿼리 진단 & 튜닝 — 성능 저하 원인 찾기
응답 시간이 느린 API의 80% 이상은 DB 쿼리가 원인입니다. 슬로우 쿼리를 찾아내고 개선하는 것이 성능 최적화의 첫 번째 단계입니다.
슬로우 쿼리 로그 활성화
섹션 제목: “슬로우 쿼리 로그 활성화”PostgreSQL
섹션 제목: “PostgreSQL”-- postgresql.conf 설정log_min_duration_statement = 1000 -- 1초 이상 걸리는 쿼리 기록log_statement = 'none' -- 전체 로그는 비활성 (성능 영향)log_line_prefix = '%t [%p]: '# 실시간 슬로우 쿼리 확인tail -f /var/log/postgresql/postgresql.log | grep "duration:"MySQL / MariaDB
섹션 제목: “MySQL / MariaDB”-- 슬로우 쿼리 로그 활성화SET GLOBAL slow_query_log = ON;SET GLOBAL long_query_time = 1; -- 1초 이상SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';# mysqldumpslow으로 요약mysqldumpslow -s t -t 20 /var/log/mysql/slow.log# -s t: 실행 시간 기준 정렬 / -t 20: 상위 20개EXPLAIN으로 실행 계획 분석
섹션 제목: “EXPLAIN으로 실행 계획 분석”쿼리가 왜 느린지 확인하는 핵심 도구입니다.
-- EXPLAIN ANALYZE: 실제 실행 + 시간 측정EXPLAIN ANALYZESELECT o.id, o.total, u.nameFROM orders oJOIN users u ON u.id = o.user_idWHERE o.status = 'pending'ORDER BY o.created_at DESCLIMIT 50;결과 해석 포인트
섹션 제목: “결과 해석 포인트”Seq Scan on orders (cost=0.00..45231.00 rows=12345 ...) ↑ 풀 테이블 스캔 — 인덱스 없음, 위험 신호
Index Scan using idx_orders_status on orders ... ↑ 인덱스 사용 — 정상| 실행 계획 키워드 | 의미 | 조치 |
|---|---|---|
Seq Scan | 풀 테이블 스캔 | 인덱스 추가 검토 |
Hash Join | 조인 시 해시 사용 | 대부분 정상, rows 수 확인 |
Nested Loop | 중첩 루프 조인 | rows가 많으면 N+1 의심 |
Sort | 정렬 연산 | 정렬 기준 컬럼 인덱스 확인 |
인덱스 설계 핵심 원칙
섹션 제목: “인덱스 설계 핵심 원칙”조건절 컬럼에 인덱스 추가
섹션 제목: “조건절 컬럼에 인덱스 추가”-- WHERE, JOIN ON, ORDER BY에 쓰이는 컬럼이 대상CREATE INDEX idx_orders_status ON orders(status);CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);복합 인덱스 컬럼 순서
섹션 제목: “복합 인덱스 컬럼 순서”복합 인덱스 (a, b, c)는:- WHERE a = ? → 사용- WHERE a = ? AND b = ? → 사용- WHERE b = ? → ❌ 사용 안 됨 (선두 컬럼 없음)N+1 쿼리 탐지 & 해결
섹션 제목: “N+1 쿼리 탐지 & 해결”N+1은 DB 슬로우 쿼리 중 가장 흔하고 치명적인 패턴입니다.
# N+1 발생 코드 (Django ORM)orders = Order.objects.filter(status='pending')for order in orders: print(order.user.name) # 매 반복마다 SELECT users WHERE id=?# 주문 100건이면 DB 쿼리 101번
# 해결: select_related (JOIN)orders = Order.objects.filter(status='pending').select_related('user')# 쿼리 1번으로 해결// N+1 발생 코드 (JPA)List<Order> orders = orderRepository.findByStatus("pending");orders.forEach(o -> System.out.println(o.getUser().getName()));
// 해결: JPQL fetch join@Query("SELECT o FROM Order o JOIN FETCH o.user WHERE o.status = :status")List<Order> findByStatusWithUser(@Param("status") String status);pg_stat_statements로 Top 슬로우 쿼리 찾기
섹션 제목: “pg_stat_statements로 Top 슬로우 쿼리 찾기”-- pg_stat_statements 확장 활성화 (postgresql.conf)-- shared_preload_libraries = 'pg_stat_statements'
-- 평균 실행 시간 기준 상위 10개 쿼리SELECT query, calls, round(mean_exec_time::numeric, 2) AS avg_ms, round(total_exec_time::numeric, 2) AS total_msFROM pg_stat_statementsORDER BY mean_exec_time DESCLIMIT 10;다음 단계
섹션 제목: “다음 단계”이 가이드를 내 서비스에 직접 적용해 보세요.
TestForge 무료 스캔 시작 →