API 하나만 유독 느려졌을 때, 많은 팀이 가장 먼저 하는 일은 인덱스를 하나 더 붙여 보는 것입니다. 하지만 실무에서는 그 순서가 자주 틀립니다. 느린 MySQL 쿼리는 “인덱스가 없다” 하나로 끝나는 문제가 아니라, 어떤 경로로 읽는지, 얼마나 많이 읽는지, 어디서 정렬하는지, 조인 입력이 얼마나 큰지, 심지어 잠금 대기까지 같이 얽혀 있기 때문입니다.
이 글은 “느린 쿼리를 봤다, 이제 무엇부터 봐야 하지?”라는 상황을 위한 체크리스트입니다.
- 진짜 느린 쿼리를 어떻게 좁혀야 하는지
EXPLAIN에서 무엇을 먼저 읽어야 하는지- 인덱스, row 수, 정렬, 조인, 잠금 중 어디가 병목인지 어떻게 가르는지
짧게 말하면 느린 쿼리는 SQL 문장만 보고 고치기보다, 접근 경로, 읽는 양, 정렬 비용, 결과 크기, 잠금 영향 순서로 좁혀가야 훨씬 빨리 풀립니다.
Quick answer
MySQL query optimization checklist를 한 줄로 요약하면 아래 순서입니다.
- 진짜 느린 쿼리와 느린 구간을 먼저 특정한다
EXPLAIN으로 접근 경로와 예상 row 수를 본다- 인덱스가 “있는지”보다 “이 쿼리 패턴에 맞는지”를 확인한다
- 불필요하게 너무 많은 row를 읽고 있지 않은지 본다
SELECT *, 큰 결과 집합, 큰 OFFSET 같은 낭비를 줄인다ORDER BY,GROUP BY,DISTINCT, 임시 작업 비용을 본다- JOIN 입력 크기와 조인 순서를 점검한다
- 쓰기 쿼리라면 잠금 대기와 긴 트랜잭션을 같이 본다
- 운영과 비슷한 데이터 규모에서 전후 비교를 남긴다
이 순서를 따르면 “감으로 튜닝하다가 다시 원점으로 돌아오는 일”이 크게 줄어듭니다.
1. 먼저 진짜 느린 쿼리부터 특정하기
쿼리 최적화는 SQL 문장을 예쁘게 바꾸는 작업이 아니라, 실제 병목을 잡는 작업입니다. 그래서 첫 질문은 “이 쿼리를 더 짧게 쓸 수 있나?”가 아니라 “정말 이 쿼리가 병목이 맞나?”여야 합니다.
먼저 아래를 분리해서 보세요.
- 항상 느린가, 특정 사용자나 특정 시간대에만 느린가
- 최근 배포 이후 느려졌는가, 데이터가 쌓이면서 점점 느려졌는가
- DB 서버 시간이 느린가, 아니면 애플리케이션에서 쿼리를 너무 많이 호출하는가
예를 들어 응답 시간이 2초인 API가 있다고 해도, 실제로는:
- SQL 한 건이 2초인 경우
- 50ms짜리 SQL을 40번 호출하는 경우
- DB는 200ms인데 애플리케이션에서 후처리가 긴 경우
가 서로 완전히 다릅니다.
즉, 체크리스트의 첫 단계는 “느린 SQL 하나를 정확히 찍는 것”입니다.
2. SQL을 고치기 전에 EXPLAIN부터 보기
느린 쿼리를 찾았다면 그 다음은 감으로 문장을 뜯어고치는 것이 아니라 EXPLAIN을 보는 것입니다.

EXPLAIN
SELECT id, created_at, total_amount
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 20;
입문 단계에서는 아래 네 칸만 먼저 봐도 충분히 많은 힌트를 얻을 수 있습니다.
typekeyrowsExtra
특히 아래 신호는 거의 항상 다시 볼 가치가 있습니다.
type = ALL: 풀스캔 가능성key = NULL: 인덱스 미사용 가능성rows가 지나치게 큼: 읽는 양이 너무 많음Extra에Using filesort,Using temporary: 정렬이나 임시 작업 비용 존재 가능성
EXPLAIN이 중요한 이유는 “이 SQL이 예뻐 보이느냐”가 아니라 “MySQL이 실제로 이 SQL을 어떻게 읽으려 하느냐”를 보여주기 때문입니다.
실행 계획 읽기 자체를 더 자세히 보고 싶다면 MySQL EXPLAIN 가이드를 같이 보면 좋습니다.
3. 인덱스가 있는지보다 쿼리 패턴에 맞는지 보기
실무에서 가장 흔한 착각 중 하나는 “인덱스가 하나 있으니 괜찮겠지”입니다. 하지만 인덱스는 존재 여부보다 쿼리 패턴과의 정합성이 더 중요합니다.
체크할 질문은 보통 이렇습니다.
- WHERE 조건 컬럼이 인덱스 선두와 맞는가
- JOIN 키 양쪽에 적절한 인덱스가 있는가
ORDER BY까지 함께 도와주는가- 함수, 형변환, 계산 때문에 인덱스를 못 타고 있지 않은가
예를 들어 아래처럼 컬럼에 함수를 적용하면 인덱스 이점이 약해질 수 있습니다.
-- 인덱스 활용이 어려워질 수 있는 형태
WHERE DATE(created_at) = '2026-04-13'
-- 범위 조건으로 바꿔 인덱스 활용 가능성을 높이는 형태
WHERE created_at >= '2026-04-13'
AND created_at < '2026-04-14'
또한 WHERE user_id = ? ORDER BY created_at DESC 패턴이라면, 필터만 보는 인덱스와 필터 뒤 정렬까지 같이 고려한 인덱스는 결과가 다를 수 있습니다.
핵심은 인덱스는 DB에 “있다”로 끝나는 자산이 아니라, 특정 쿼리 형태를 빠르게 만들기 위해 설계된 구조라는 점입니다.
4. 정말 필요한 row만 읽고 있는지 확인하기
쿼리가 느릴 때는 CPU보다 “읽는 양”이 더 큰 문제인 경우가 많습니다. 즉, 최적화의 핵심은 작은 비용을 미세 조정하는 것이 아니라, 애초에 훨씬 적게 읽게 만드는 데 있습니다.
이 단계에서 볼 질문은 아래와 같습니다.
- 필요한 row만 읽는가
- 필터가 충분히 좁은가
LIMIT이 필요한 화면인데 빠져 있지 않은가- 너무 큰 범위를 한 번에 긁고 있지 않은가
예를 들어 운영 화면이 최근 20건만 보여주는데 쿼리는 최근 2만 건을 읽고 있다면, 그 병목은 SQL 문법보다 접근 범위에 있습니다.
특히 큰 OFFSET 페이지네이션은 자주 비용을 키웁니다.
LIMIT 20 OFFSET 10000은 뒤쪽 페이지로 갈수록 점점 더 무거워질 수 있습니다- 탐색형 화면이라면 keyset pagination이 더 낫기도 합니다
즉, “읽는 row 수를 줄일 수 있는가?”는 거의 항상 최우선 질문입니다.
5. 결과 집합과 컬럼 수를 줄이기
느린 쿼리라고 하면 WHERE 조건과 인덱스에만 시선이 가지만, 실제로는 가져오는 결과가 너무 커서 느린 경우도 많습니다.
아래는 꼭 체크해 볼 만한 질문입니다.
SELECT *를 습관적으로 쓰고 있지 않은가- 실제 화면이나 API가 쓰는 컬럼만 가져오는가
- 큰 텍스트 컬럼이나 JSON 컬럼까지 불필요하게 읽고 있지 않은가
예를 들어 목록 화면이라면 보통 아래 정도면 충분합니다.
SELECT id, title, created_at
FROM posts
WHERE status = 'published'
ORDER BY created_at DESC
LIMIT 20;
그런데 여기에 본문 전체, 메타데이터 JSON, 큰 설명 컬럼까지 함께 읽으면 I/O와 네트워크 전송량이 같이 커집니다.
또한 필요한 컬럼만 읽는 습관은 커버링 인덱스 가능성까지 넓혀 줄 수 있습니다. 즉, 결과 크기 축소는 종종 “작은 최적화”가 아니라 체감 성능을 크게 바꾸는 단계입니다.
6. 정렬, 그룹핑, 임시 작업이 병목인지 보기
WHERE 조건은 괜찮아 보여도 쿼리가 느린 이유가 정렬이나 집계인 경우가 많습니다. 특히 ORDER BY, GROUP BY, DISTINCT는 읽은 뒤 추가 작업을 크게 만들 수 있습니다.
이 단계에서는 아래를 확인하세요.
Extra에Using filesort가 보이는가Using temporary가 보이는가- 필터는 좁은데 정렬 기준이 따로라서 비용이 커지는가
- 집계 전에 입력 row 수를 충분히 줄이고 있는가
예를 들어 “조건으로는 user_id로 좁히는데 정렬은 created_at으로 한다” 같은 패턴은 인덱스 설계에 따라 성능 차이가 크게 날 수 있습니다.
또한 DISTINCT는 중복 제거가 쉬워 보여도, 실제로는 upstream에서 row 폭증이 일어나고 있다는 신호일 수 있습니다. 즉, DISTINCT 자체를 고치기보다 “왜 중복이 이렇게 많이 생겼나”를 보는 편이 더 중요할 때가 많습니다.
7. JOIN은 입력 크기와 순서가 핵심이다
JOIN 쿼리는 SQL 문장 길이보다 “얼마나 큰 집합끼리 붙이고 있느냐”가 더 중요합니다.

아래 질문으로 점검해 보세요.
- 조인 전에 한쪽 입력을 충분히 좁히고 있는가
- JOIN 키에 인덱스가 있는가
- one-to-many 조인 때문에 row 수가 폭증하고 있지 않은가
- WHERE 조건이 너무 늦게 적용되고 있지 않은가
예를 들어 주문 목록을 조회할 때, 먼저 최근 주문 20건을 좁힌 뒤 상세를 붙이는 것과, 거대한 주문 테이블과 상세 테이블을 먼저 크게 조인한 뒤 마지막에 줄이는 것은 비용이 전혀 다릅니다.
즉, JOIN 최적화의 핵심은 “더 빨리 붙이는 것”보다 **“붙이기 전에 얼마나 좁혔는가”**에 가깝습니다.
8. 쓰기 쿼리라면 잠금 대기까지 같이 보기
느린 쿼리라고 해서 항상 읽기 최적화 문제는 아닙니다. UPDATE, DELETE, 대량 INSERT는 실행 계획보다 잠금 대기나 긴 트랜잭션 때문에 체감이 느린 경우도 많습니다.
특히 아래 상황은 같이 봐야 합니다.
- 긴 트랜잭션 안에서 실행되는가
- 같은 row나 같은 인덱스 범위를 자주 건드리는가
lock wait timeout이나 deadlock 로그가 같이 보이는가- 애플리케이션이 불필요하게 트랜잭션을 오래 잡고 있지 않은가
이 경우 SQL 튜닝만으로 해결이 안 되고:
- 트랜잭션 범위 축소
- 갱신 순서 통일
- 배치 크기 조정
- hotspot row 분산
같은 운영 관점 개선이 더 중요할 수 있습니다.
9. 운영과 비슷한 데이터에서 전후 비교 남기기
쿼리 최적화는 “좀 빨라진 것 같다”로 끝내면 다시 반복됩니다. 그래서 전후 비교 기준을 남기는 것이 중요합니다.
최소한 아래 정도는 기록해 두는 편이 좋습니다.
- 수정 전
EXPLAIN과 수정 후EXPLAIN - 응답 시간 또는 실행 시간 변화
- 읽는 row 수 변화
- 정렬/임시 작업 여부 변화
테스트 환경이 너무 작으면 운영과 전혀 다른 결론이 나올 수 있습니다. 예를 들어 로컬 데이터 1만 건에서는 괜찮던 쿼리가 운영 1천만 건에서는 정렬 비용 때문에 급격히 무거워질 수 있습니다.
그래서 체크리스트의 마지막 질문은 항상 이것입니다.
“이 개선이 운영과 비슷한 데이터 규모에서도 여전히 유효한가?”
자주 하는 실수
1. 인덱스 하나 추가하고 끝낸다
실제 병목은 대량 정렬, 큰 결과 집합, 조인 폭증일 수도 있습니다.
2. EXPLAIN 없이 SQL 문장만 보고 고친다
이 경우 대개 “왠지 빨라 보이는 방향”으로만 바뀌고, 실제 원인은 그대로 남습니다.
3. SELECT *와 큰 OFFSET을 당연하게 쓴다
초기에는 괜찮아 보여도 데이터가 쌓이면 빠르게 병목이 됩니다.
4. 테스트 환경에서만 확인하고 끝낸다
운영 데이터 분포와 트래픽 조건이 다르면 결론도 달라질 수 있습니다.
FAQ
Q. 느린 쿼리를 보면 무조건 인덱스부터 추가해야 하나요?
아닙니다. 먼저 접근 경로, 읽는 row 수, 정렬 비용, 결과 크기, 잠금 여부를 나눠 봐야 합니다. 인덱스는 중요한 도구지만 항상 첫 해답은 아닙니다.
Q. EXPLAIN에서 가장 먼저 볼 신호는 무엇인가요?
입문자 기준으로는 type, key, rows, Extra 네 가지가 가장 실용적입니다. 특히 ALL, NULL, 큰 rows, Using filesort, Using temporary는 우선 확인할 가치가 큽니다.
Q. 쿼리 자체를 다시 짜야 할지, 인덱스를 손봐야 할지 어떻게 구분하나요?
필터는 괜찮은데 읽는 양이 너무 크거나 정렬이 무겁다면 쿼리 구조를 먼저 의심하고, 필터/조인/정렬 패턴과 맞는 인덱스가 비어 있다면 인덱스 설계를 같이 봐야 합니다. 둘은 경쟁 관계가 아니라 보통 함께 봐야 하는 항목입니다.
Read Next
- 실행 계획을 좀 더 차분히 읽고 싶다면 MySQL EXPLAIN 가이드를 먼저 보면 좋습니다.
- 실제 운영 흐름에서 slow query를 어떻게 좁히는지는 MySQL slow query 가이드와 자연스럽게 이어집니다.
- 잠금 대기와 트랜잭션 병목이 의심된다면 애플리케이션의 트랜잭션 범위와 Lock 획득 순서를 우선 점검하는 편이 좋습니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis, RabbitMQ, Kafka 중 어디부터 볼까 Redis, RabbitMQ, Kafka가 함께 있는 시스템에서 지금 보이는 장애가 어느 계층에 더 가까운지, 첫 10분 안에 무엇을 확인하고 어떤 글로 들어가야 하는지 정리한 실전 허브 가이드입니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Astro 기술 블로그 SEO 체크리스트: 트래픽 기다리기 전에 먼저 고칠 것 Astro 기술 블로그를 위한 실전 SEO 체크리스트입니다. 배포 호스트 확인, robots.txt, sitemap, canonical, hreflang, 구조화 데이터, 페이지별 메타데이터, noindex 판단, 검증 명령까지 우선순위대로 정리합니다.
- 다국어 블로그 canonical과 hreflang 설정 가이드: 무엇을 확인하고 어디서 깨질까 다국어 블로그에서 canonical과 hreflang을 어떻게 설정해야 하는지 실전 기준으로 정리합니다. self-canonical, 상호 연결되는 hreflang 묶음, x-default, 카테고리 페이지, 최종 렌더 HTML 점검, 한 언어 버전이 다른 언어 버전을 눌러버리는 실수까지 다룹니다.
- OpenAI Codex CLI 설치 가이드: 설치, 인증, 첫 작업까지 OpenAI Codex CLI를 실전 기준으로 설치하는 방법을 정리했다. 설치, 로그인, 첫 실행, Windows 주의점, 첫 작업을 어떻게 시작하면 좋은지까지 다룬다.