[MySQL] 느린 쿼리 트러블슈팅 가이드: slow query log, 락 대기, 커넥션 고갈 증상별 대응
![[MySQL] 느린 쿼리 트러블슈팅 가이드: slow query log, 락 대기, 커넥션 고갈 증상별 대응](https://blog.kakaocdn.net/dna/p94Vx/dJMcacQSePL/AAAAAAAAAAAAAAAAAAAAACw5_sRiWFUjFJMtEpYCKwb63lzm5zxOmoRlGcUfZb6k/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1788188399&allow_ip=&allow_referer=&signature=iNX6MwZWarhen4T2t1XgRT0aLm4%3D)
[MySQL] 느린 쿼리 트러블슈팅 가이드: slow query log, 락 대기, 커넥션 고갈 증상별 대응
"DB가 느려요"는 사실 네 가지 다른 문제의 같은 증상이에요 — 쿼리가 느리거나, 락에 막혔거나, 리소스가 포화됐거나, 커넥션이 말랐거나. 어느 쪽인지 가르지 않고 인덱스부터 추가하면 절반은 헛수고예요. 이 글은 그 분기를 잡아주는 MySQL 시리즈의 진단 허브예요.
증상별로 보는 지표와 대응을 정리하고, 깊은 내용은 시리즈의 해당 글로 연결해요.
01. 진단 분기, 증상이 원인을 가리켜요

- 특정 쿼리만 항상 느리다 → 인덱스·쿼리 문제. EXPLAIN의 영역이에요.
- 평소 빠른 쿼리가 가끔 멈춘다 → 락 대기. 누가 잡고 있는 거예요.
- 전반적으로 다 느려졌다 → 리소스 포화(버퍼 풀·디스크 IO) 또는 무거운 쿼리의 연쇄 영향.
- DB는 한가한데 앱이 타임아웃 → 커넥션 풀 고갈. DB가 아니라 풀의 문제예요.
"항상이냐 가끔이냐", "특정이냐 전체냐" 두 질문만 던져도 사분면이 갈려요.
02. 증상: 특정 쿼리만 항상 느려요

왕도는 루프예요. 수집 → 분석 → 수정 → 검증.
-- 1초 넘는 쿼리를 로그로 수집
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
GLOBAL 변경은 새 세션부터 적용돼요. HikariCP처럼 커넥션을 오래 쥐는 환경에선 기존 커넥션이 옛 값을 계속 써서 "설정했는데 안 잡히는" 일이 생겨요(풀 재시작 또는 maxLifetime 회전 후 적용).
slow query log에 잡힌 쿼리를 EXPLAIN으로 분석해요 — type이 ALL인지, rows가 큰지, filesort가 뜨는지. 원인이 보이면 인덱스 설계 원칙(왼쪽 접두·동등 앞 범위 뒤·커버링)이나 쿼리 수정(컬럼 가공 제거·형변환 정리)으로 고치고, EXPLAIN ANALYZE로 실측 검증까지 해요. 감으로 인덱스를 추가하면 "고쳤는데 그대로"가 반복돼요.
ORM을 쓴다면 "쿼리 자체는 빠른데 개수가 많은" 패턴도 의심해요 — 그건 MySQL이 아니라 JPA N+1의 영역이에요. slow log엔 안 잡히는데 화면은 느린 경우가 그래요.
03. 증상: 평소 빠른 쿼리가 가끔 멈춰요
쿼리 자체는 문제없는데 간헐적으로 수 초~수십 초 걸린다면, 거의 항상 락 대기예요.

SHOW PROCESSLIST; -- 지금 뭐가 돌고 있나
SELECT * FROM sys.innodb_lock_waits; -- 누가 누구를 기다리나
SELECT * FROM information_schema.innodb_trx
ORDER BY trx_started; -- 오래 열린 트랜잭션
블로커는 대부분 장수 트랜잭션이에요 — 커밋 안 하고 열려 있는 배치, 수동 세션, 트랜잭션 안에서 외부 API를 기다리는 코드요. 장애 중이면 블로커 KILL이 가장 빠른 복구일 때가 많고, 재발 방지는 락 동작 이해(특히 인덱스 없는 UPDATE의 전체 잠금)와 데드락 예방 수칙(순서 통일·짧은 트랜잭션)이에요.
04. 증상: 전반적으로 다 느려졌어요
특정 쿼리가 아니라 전체가 가라앉았다면 리소스를 봐요. 두 가지가 단골이에요.
첫째, 버퍼 풀 부족이에요. InnoDB는 데이터·인덱스를 메모리(버퍼 풀)에 올려두고 일하는데, 데이터가 커져 버퍼 풀 적중률이 떨어지면 디스크 읽기가 늘며 전체가 느려져요. innodb_buffer_pool_size(전용 서버면 메모리의 50~75%)와 적중률 지표를 확인해요.
둘째, 무거운 쿼리 하나의 연쇄 효과예요. 풀 스캔 쿼리가 버퍼 풀을 다 밀어내거나 디스크 IO를 독점하면, 멀쩡한 쿼리들까지 같이 느려져요. 이때 processlist에 그 무거운 쿼리가 보여요 — "전체가 느려진 시점에 뭐가 돌고 있었나"가 질문이에요.
05. 증상: DB는 한가한데 앱이 타임아웃 나요
DB CPU도 낮고 slow log도 깨끗한데 앱에서 "connection timeout"이면, DB가 아니라 커넥션 풀이에요. active/pending 지표를 보고, "오래 점유"의 3대 원인 — 느린 쿼리·트랜잭션 안 외부 호출·OSIV — 과 누수를 점검해요. 전부 HikariCP 편에서 다뤘어요. 풀 사이즈를 늘리는 건 원인 분석 후의 마지막 카드예요.
06. 평소에 깔아두면 좋은 것
- slow query log 상시 ON — long_query_time 1초. 평소에 쌓여야 "언제부터 느려졌나"를 알 수 있어요. SET GLOBAL은 서버 재시작에 휘발되니, 상시로 둘 거면
SET PERSIST나 my.cnf에 박아둬요. - 핵심 지표 대시보드 — QPS, 슬로우 쿼리 수, 버퍼 풀 적중률, 락 대기 수, 커넥션 active/pending. 다섯 개면 위 사분면이 다 커버돼요.
- 릴리즈와 지표를 같이 보기 — "느려진 시점 = 배포 시점"인 경우가 많아요. 새 쿼리가 인덱스를 안 타는 거죠. 배포 후 슬로우 쿼리 수 확인을 루틴으로 만들어요.
정리
"DB가 느려요"는 특정/전체 × 항상/가끔으로 가르면 네 갈래예요. 특정+항상은 인덱스(EXPLAIN 루프), 특정+가끔은 락(블로커 추적), 전체는 리소스(버퍼 풀·무거운 쿼리), DB 한가+앱 타임아웃은 커넥션 풀이에요. 측정 없이 인덱스부터 만지지 않기 — 이게 이 시리즈 전체를 한 줄로 줄인 말이에요.
그 네 갈래를 따라 인덱스 동작 원리 · 복합 인덱스 설계 · EXPLAIN · 격리수준과 MVCC · InnoDB 락 · 데드락 · HikariCP까지 내려왔어요. 같은 이야기를 ORM 위층에서 다시 보고 싶으면 JPA 트러블슈팅과 한 쌍으로 읽으면 돼요. 다음 시리즈는 프레임워크 차례예요. @Transactional이 안 먹는 이유부터, Spring의 함정들로 넘어가요.
'백엔드 > 데이터 & DB' 카테고리의 다른 글
| [DB] 깊은 페이징 성능 문제: OFFSET이 느려지는 이유와 커서 기반 페이징(no-offset) (1) | 2026.08.17 |
|---|---|
| [MySQL] HikariCP 커넥션 풀 설정: 풀 사이즈 공식, 타임아웃, 커넥션 누수 잡기 (0) | 2026.07.27 |
| [MySQL] 데드락 원인과 해결: SHOW ENGINE INNODB STATUS 읽는 법, 재시도 전략 (0) | 2026.07.24 |
| [MySQL] InnoDB 락 정리: 레코드 락, 갭 락, 넥스트키 락, SELECT FOR UPDATE (0) | 2026.07.23 |
| [MySQL] 트랜잭션 격리수준과 MVCC: REPEATABLE READ, 언두 로그, 팬텀 리드 (0) | 2026.07.22 |
📚 같이 보면 좋은
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."