느린 쿼리를 찾는 순서 — 감이 아니라 로그에서 시작한다
어디가 느린지 추측하지 않고 측정으로 좁히는 절차. slow query log부터 실행 계획까지.
감으로 시작하면 엉뚱한 곳을 고친다
"이 화면이 느리다"에서 곧바로 특정 쿼리를 의심하는 것은 대개 빗나간다. 순서를 지키면 훨씬 빠르다.
1단계 — 슬로우 쿼리 로그
DB가 제공하는 기능부터 켠다.
```
MySQL
slow_query_log = ON
long_query_time = 1
PostgreSQL
log_min_duration_statement = 1000 # ms
```
기준값은 1초에서 시작해 상황에 따라 낮춘다. 처음부터 100ms로 잡으면 로그가 폭증한다.
2단계 — 총 소요 시간으로 정렬
가장 흔한 실수가 가장 느린 한 건을 먼저 고치는 것이다. 실제로 중요한 것은 총합이다.
```
쿼리 A: 5초 × 하루 10회 = 50초
쿼리 B: 0.1초 × 하루 50,000회 = 5,000초
```
B가 100배 더 큰 문제인데, 로그를 시간 순으로만 보면 A가 눈에 띈다.
pg_stat_statements(PostgreSQL)나 performance_schema(MySQL) 같은 집계 뷰를 쓰면 이 정렬을 바로 볼 수 있다.
3단계 — 실행 계획
대상을 정했으면 계획을 본다.
```sql
EXPLAIN ANALYZE SELECT ...;
```
확인할 것:
- 예상 행 수 vs 실제 행 수 — 크게 다르면 통계가 낡음
- Seq Scan / Full Table Scan — 큰 테이블에서 나오면 인덱스 검토
- 가장 시간이 많이 걸린 노드 — 위에서 아래로 훑으며 비중 확인
- Nested Loop의 반복 횟수 — 안쪽이 여러 번 실행되는 경우
4단계 — 원인별 대응
| 관찰 | 대응 |
|---|---|
| 통계 부정확 | 통계 갱신 (ANALYZE) |
| 인덱스 미사용 | 조건절 형태 확인 (함수·타입) |
| 인덱스 없음 | 추가 검토 (쓰기 비용 고려) |
| 조인 순서 문제 | 조건 재작성, 필요시 힌트 |
| 결과 자체가 큼 | 페이지네이션·필터 추가 |
| 정렬이 비쌈 | 정렬 컬럼 인덱스 검토 |
마지막 두 행이 자주 간과된다. 쿼리가 느린 게 아니라 가져오는 데이터가 너무 많은 경우, 인덱스로는 해결되지 않는다.
인덱스를 추가하기 전에
인덱스는 공짜가 아니다.
- 쓰기(INSERT/UPDATE/DELETE)마다 갱신 비용
- 저장 공간
- 옵티마이저의 선택지가 늘어 계획이 바뀔 수 있음
추가 전 확인:
- 기존 인덱스로 커버되지 않는가 (복합 인덱스의 선행 컬럼)
- 이 쿼리가 실제로 자주 실행되는가
- 조건의 선택도가 충분한가
5단계 — 재측정
고친 뒤 같은 방식으로 다시 잰다. 계획만 보고 "좋아졌겠지"로 끝내면 안 된다.
EXPLAIN ANALYZE로 실제 시간 비교- 슬로우 쿼리 로그에서 사라졌는지 확인
- 다른 쿼리가 느려지지 않았는지 확인 (인덱스 추가의 부작용)
애플리케이션 쪽 원인
DB 쪽에서 아무 문제가 없는데 화면이 느린 경우도 흔하다.
- N+1 쿼리 (개별 쿼리는 빠른데 횟수가 많음)
- 연결 풀 고갈로 대기
- 결과를 애플리케이션에서 가공하는 비용
- 직렬화·네트워크 전송량
슬로우 쿼리 로그가 비어 있다면 이쪽을 본다. APM 도구로 요청 전체의 시간 분포를 보면 어디서 시간이 가는지 드러난다.
최종 수정 2026-08-28