느린 쿼리보다 총 시간 — pg_stat_statements 로 튜닝 순서 정하기
느린 쿼리 1등을 3초에서 0.5초로 줄였는데 서비스 체감은 그대로예요. 하루 500만 번 도는 50ms 쿼리가 더 큰 짐이었거든요. 튜닝할 쿼리를 고르는 기준을 바꾸는 법을 알 수 있어요.
느린 쿼리 로그를 켜고 상위 목록을 뽑았어요. 3초짜리 리포트 쿼리가 1등이라 그걸 튜닝했어요. 3초를 0.5초로 줄였고요.
그런데 서비스 체감은 그대로예요. 응답 시간 지표도 안 움직여요.
알고 보니 그 리포트는 하루에 스무 번 도는 쿼리였어요. 총 시간으로 치면 하루 1분이에요. 반면 50ms 짜리 조회 하나가 하루 500만 번 돌고 있었고, 그게 총 69시간이었어요.
튜닝의 대상은 느린 쿼리가 아니라 총 시간이 큰 쿼리예요. 실행 시간에 호출 횟수를 곱한 값이요.
이 기준을 안 바꾸면 아무리 열심히 튜닝해도 지표가 안 움직여요. 그리고 노력한 만큼 성과가 안 나오니 튜닝 자체가 소용없다고 결론 내리게 됩니다.
왜 그런가
한 쿼리가 시스템에 주는 부담은 평균 실행 시간이 아니라 총 시간이에요. 실행 시간에 호출 횟수를 곱한 값이요.
느린 쿼리 로그는 임계값을 넘는 개별 실행만 기록해요. 그래서 자주 도는 짧은 쿼리는 아예 안 보여요. 임계값을 1초로 잡았다면 50ms 쿼리는 500만 번을 돌아도 로그에 한 줄도 안 남아요.
그런데 실제로 DB 를 붙잡고 있는 건 그 쿼리예요. 그리고 이런 쿼리는 대개 사용자 경로 위에 있어요. 리포트는 하루 스무 번이지만 목록 조회는 모든 사용자가 매번 부르니까요.
여기에 또 하나가 있어요. 총 시간이 큰 쿼리는 개선의 지렛대도 커요. 50ms 를 5ms 로 줄이면 하루 69시간이 7시간이 돼요. 62시간의 DB 시간이 비는 거예요. 3초짜리를 0.5초로 줄여 봐야 하루 50초를 아낍니다.
그리고 총 시간이 큰 쿼리는 다른 쿼리에도 영향을 줘요. 커넥션과 버퍼를 계속 차지하니, 그게 줄면 전체가 같이 빨라져요.
그래서 봐야 할 지표가 바뀌어요. 최댓값이 아니라 합계, 개별 느림이 아니라 누적 부담입니다.
코드로 보면
합계로 정렬한다:
느린 쿼리 로그 상위 = 개별 실행이 느린 것
자주 도는 짧은 쿼리는 여기 안 나온다
· 총 시간으로 정렬 — 이게 튜닝 우선순위다
SELECT round(total_exec_time) AS total_ms,
calls,
round(mean_exec_time, 2) AS mean_ms,
round(100 * total_exec_time /
sum(total_exec_time) OVER (), 1) AS pct,
left(query, 60)
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 20;
· 상위 5개가 전체의 몇 퍼센트인지 본다
· 대개 소수가 대부분을 차지한다✕ 문제 지점 · ✓ 해법 · 그대로 돌려 보기 전에 자기 환경에 맞게 고쳐 주세요.
실전에서는
총 시간으로 보면 우선순위가 완전히 바뀌어요. 대개 상위 다섯 개가 전체 DB 시간의 대부분을 차지합니다.
그리고 그 목록에서 자주 발견되는 게 앞서 본 문제들이에요. N+1 로 수백만 번 도는 단순 조회, 목록마다 도는 개수 세기, 캐시 없이 반복되는 설정 조회 같은 것들이요. 개별로는 빠르지만 합치면 시스템의 대부분입니다.
다만 총 시간만 보는 것도 완전하지 않아요. 몇 가지를 같이 봐야 해요.
최댓값도 봐야 해요. 평균은 50ms 인데 최댓값이 30초인 쿼리가 있다면, 그건 앞서 본 계획 전환이나 파라미터 문제일 수 있어요. 사용자 일부가 아주 나쁜 경험을 하고 있는 거고요.
호출 횟수 자체도 신호예요. 비정상적으로 큰 호출 수는 N+1 이나 캐시 부재를 뜻해요. 이건 쿼리 튜닝이 아니라 호출 구조를 고쳐야 하는 문제예요.
그리고 사용자 경로 위에 있는지도 중요해요. 총 시간이 커도 야간 배치라면 급하지 않을 수 있어요. 반대로 총 시간이 중간이어도 로그인 경로에 있다면 우선순위가 높습니다.
측정 자체에도 주의가 필요해요. 통계는 마지막 초기화 이후 누적이라, 언제부터 쌓였는지 확인해야 해요. 그리고 정규화된 형태로 묶이니 파라미터가 다른 실행이 하나로 합쳐집니다.
정리하면 우선순위는 총 시간을 기본으로 하되, 최댓값과 호출 횟수와 경로를 함께 보고 정하는 거예요.
처방
- 느린 쿼리 로그가 아니라 총 시간 상위 목록으로 우선순위를 정하세요. 이 기준 하나만 바꿔도 튜닝 효율이 달라집니다.
- 상위 5개가 전체의 몇 퍼센트인지 보세요. 대개 소수가 대부분이라, 거기만 고쳐도 큰 폭으로 개선됩니다.
- 호출 횟수가 비정상적으로 큰 쿼리를 따로 보세요. 그건 쿼리가 아니라 호출 구조 문제예요.
- 최댓값이 평균의 수백 배인 쿼리를 보세요. 계획이 값에 따라 갈리고 있다는 신호입니다.
- 통계가 언제부터 쌓였는지 확인하세요. 방금 초기화됐다면 목록이 대표성이 없어요.
- 사용자 경로 위에 있는지 함께 보세요. 같은 총 시간이면 사용자가 기다리는 쪽이 먼저입니다.
- 개선 뒤에 다시 재세요. 상위 항목을 고치면 순위가 바뀌고, 그다음 항목이 새 병목이 됩니다.
- 튜닝 전에 총 시간을 기록해 두세요. 개선 효과를 숫자로 말할 수 있어야 다음 작업의 근거가 됩니다.
지금 확인해 보세요
무엇부터 고칠지 정하는 한 쿼리:
SELECT round(total_exec_time) AS total_ms,
calls,
round(mean_exec_time, 2) AS mean_ms,
round(max_exec_time) AS max_ms,
round(100 * total_exec_time /
sum(total_exec_time) OVER (), 1) AS pct,
left(query, 60)
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 20;
pct 상위 5개의 합이 대개 절반을 넘는다
mean 은 작은데 calls 가 수백만 → N+1 이나 캐시 부재
max 가 mean 의 수백 배 → 계획이 값에 따라 갈린다
· 통계가 언제부터 쌓였나 — 대표성 확인
SELECT stats_reset FROM pg_stat_statements_info;
· 고치고 나서 다시 잰다
SELECT pg_stat_statements_reset();핵심 한 줄느린 쿼리가 아니라 총 시간이 큰 쿼리를 고쳐라.
자주 묻는 것
느린 쿼리 튜닝은 무엇부터 해야 하나요?
실행 시간에 호출 횟수를 곱한 총 시간이 큰 쿼리부터예요. pg_stat_statements 를 total_exec_time 순으로 정렬하면 우선순위가 나와요. 느린 쿼리 로그는 임계값을 넘는 개별 실행만 기록해서 자주 도는 짧은 쿼리를 놓쳐요.