에디블로그
Engineer's Field Notes

AI 자동화로 매일
한 편씩 쓰는
엔지니어 운영 노트

Claude Code · 자동화 파이프라인 · 사고 회고까지. 잘 굴러간 기록 + 깨진 흔적도 같이 남깁니다.

사람이 할 수 있는 일은,
AI도 할 수 있어야 합니다.
매일 한 편 쓰면서 검증 중.
— 이번 주 가장 많이 읽힌 글 TOP 3
백엔드/데이터 & DB

[MySQL] 느린 쿼리 트러블슈팅 가이드: slow query log, 락 대기, 커넥션 고갈 증상별 대응

반응형
[MySQL] 느린 쿼리 트러블슈팅 가이드: slow query log, 락 대기, 커넥션 고갈 증상별 대응

[MySQL] 느린 쿼리 트러블슈팅 가이드: slow query log, 락 대기, 커넥션 고갈 증상별 대응

"DB가 느려요"는 사실 네 가지 다른 문제의 같은 증상이에요 — 쿼리가 느리거나, 락에 막혔거나, 리소스가 포화됐거나, 커넥션이 말랐거나. 어느 쪽인지 가르지 않고 인덱스부터 추가하면 절반은 헛수고예요. 이 글은 그 분기를 잡아주는 MySQL 시리즈의 진단 허브예요.

증상별로 보는 지표와 대응을 정리하고, 깊은 내용은 시리즈의 해당 글로 연결해요.

01. 진단 분기, 증상이 원인을 가리켜요

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

"항상이냐 가끔이냐", "특정이냐 전체냐" 두 질문만 던져도 사분면이 갈려요.

02. 증상: 특정 쿼리만 항상 느려요

느린 쿼리 개선 루프. slow query log로 1초 이상 쿼리를 수집하고 EXPLAIN으로 원인을 분석해 인덱스와 쿼리를 수정한 뒤 EXPLAIN ANALYZE로 실측 검증한다

왕도는 루프예요. 수집 → 분석 → 수정 → 검증.

-- 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로 도는 쿼리를 보고, innodb_lock_waits로 락 대기 관계를 확인하고, innodb_trx로 오래 열린 트랜잭션을 찾는다
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의 함정들로 넘어가요.

출처: MySQL — The Slow Query Log · MySQL — InnoDB Buffer Pool

반응형

📚 같이 보면 좋은

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."