SQL 성능 최적화 실전 가이드
좋은 SQL 튜닝은 결국 더 영리한 문장을 쓰는 일이 아니라, DB가 해야 할 일을 얼마나 줄이느냐의 문제입니다.
시작점은 쿼리 영웅담이 아니라 workload 이해다
튜닝에서 흔한 실수는 쿼리 하나만 떼어 놓고 아래를 보지 않는 것입니다.
- 얼마나 자주 실행되는가
- p95에서도 느린가, 특정 burst에서만 느린가
- 병목이 CPU인가, I/O인가, lock contention인가, network인가
- 어떤 비즈니스 경로를 망가뜨리는가
이 맥락이 없으면 팀은 가장 눈에 띄는 느린 쿼리를 고치고, 실제로 제일 비싼 쿼리는 놓치게 됩니다.
좋은 첫 질문은 “이 SQL을 어떻게 예쁘게 만들까?”가 아니라, 우리가 어떤 workload 비용을 치르고 있는가입니다.
EXPLAIN 은 문법이 아니라 실행 계획을 보는 도구다
EXPLAIN 과 EXPLAIN ANALYZE 가 중요한 이유는 DB가 의도를 실행하는 것이 아니라, 계획을 실행하기 때문입니다.
핵심으로 볼 것은:
- 어떤 테이블부터 접근하는가
- index를 타는가 아니면 broad scan인가
- estimated rows와 actual rows가 크게 어긋나는가
- sort, temporary structure, hash 단계가 비용을 지배하는가
- join이 selective condition부터 시작되는가
즉 SQL 튜닝은 대개 syntax problem보다 planner problem에 먼저 가깝습니다.
논리적으로 맞는 쿼리도 운영적으로는 틀릴 수 있다
느린 쿼리의 상당수는 SQL이 틀린 게 아니라, workload와 비용 구조가 맞지 않는 경우입니다.
대표적인 예:
- 리스트 API가 필요 이상으로 많은 컬럼을 읽는 경우
- 관리자 화면이 exact count를 너무 자주 요구하는 경우
- 분석형 join이 request path에 올라와 있는 경우
- 대용량 데이터셋에서 offset pagination을 깊게 쓰는 경우
쿼리 자체는 멀쩡해 보여도 workload cost는 전혀 건강하지 않을 수 있습니다.
인덱스는 희망사항이 아니라 access pattern에 맞아야 한다
인덱스는 자주 만능 해결책처럼 취급되지만, 실제로는 접근 패턴과 맞을 때만 의미가 있습니다.
좋은 인덱스 설계는 최소한 아래를 함께 봅니다.
WHEREselectivityJOIN경로 지원ORDER BY,GROUP BY비용- write amplification과 maintenance cost
그래서 plan을 읽기 전에 감으로 인덱스를 추가하는 습관은 가장 나쁜 습관 중 하나입니다.
특히 composite index는 filtering과 ordering이 같이 붙는 workload에서 강하지만, generic checklist가 아니라 실제 많이 호출되는 query pattern 근거가 있어야 합니다.
covering index는 강력하지만 공짜가 아니다
covering index는 table body lookup을 줄여 읽기 지연 시간을 크게 낮출 수 있습니다. 특히 아래에 효과적입니다.
- list API
- dashboard summary
- projection shape가 안정적인 hot read path
하지만 모든 중요한 쿼리를 covering index로 해결하려 하면:
- index 크기가 커지고
- write가 느려지며
- storage와 maintenance 비용이 커집니다
즉 covering index는 “좋은 최적화”가 아니라, 정말 중요한 반복 access path에만 쓰는 비싼 최적화에 가깝습니다.
OFFSET pagination은 자주 숨어 있는 세금이다
SELECT *
FROM posts
WHERE id < :lastId
ORDER BY id DESC
LIMIT 20;
OFFSET 기반 페이지네이션은 단순해 보이지만, 뒤 페이지로 갈수록 앞 row를 읽고 버려야 해서 비용이 커집니다.
cursor 기반 전략이 특히 더 나은 경우는:
- infinite scroll
- 큰 관리자 테이블
- high-volume timeline
- 동시 쓰기 중에도 안정적인 정렬이 필요한 경우
즉 pagination은 프런트엔드 concern만이 아니라, API에서 가장 중요한 DB 성능 결정 중 하나입니다.
aggregation 비용은 읽기 비용 + 정렬 비용인 경우가 많다
GROUP BY, DISTINCT, aggregate 쿼리가 비싸지는 이유는 단순 CPU가 아닙니다.
- 너무 많은 데이터를 읽고
- 큰 집합을 정렬하고
- temporary structure를 만들고
- transactional traffic과 경쟁하기 때문입니다
그래서 정답이 꼭 “쿼리만 다듬기”는 아닙니다. 종종 더 나은 해법은:
- aggregation scope 줄이기
- summary precompute 하기
- analytical workload 분리하기
- request path 밖으로 빼기
SQL 튜닝은 statement shape뿐 아니라, workload placement 문제이기도 합니다.
COUNT(*) 는 종종 product question이다
정확한 count 최적화에 집착하기 전에, 그 exactness가 진짜 필요한지 먼저 물어야 합니다.
많은 경우 아래만 알아도 충분합니다.
- 다음 페이지가 있는가
- 특정 threshold를 넘었는가
- approximate count로 충분한가
가장 빠른 count는 종종 product가 더 이상 실시간 exact count를 요구하지 않는 count입니다.
ORM은 실제 비용 구조를 숨기기도 한다
ORM이 나쁜 것은 아니지만, 아래를 가리기 쉽습니다.
- 반복 query
- 지나치게 넓은 entity load
- accidental N+1
- 불안정한 sort/filter pattern
그래서 좋은 SQL 튜닝은 DB 레이어 위도 같이 봐야 합니다.
- API shape
- 화면 동작
- repository design
- serialization 요구사항
잘못된 read path는 SQL이 문법적으로는 맞아도, 아키텍처적으로는 낭비일 수 있습니다.
좋은 튜닝 루프
measure -> explain -> change one variable -> verify -> monitor regression
이 루프가 중요한 이유는 production 튜닝이 반복적이기 때문입니다. 데이터 분포, 동시성, cache warmth, traffic shape가 함께 변합니다.
그래서 안전한 개선은 늘 작고 측정 가능한 변화에서 나옵니다.
자주 보는 안티패턴
- 눈에 띄는 느린 쿼리만 고치고 total cost가 큰 쿼리는 놓치는 경우
- plan을 읽기 전에 인덱스를 추가하는 경우
- 실행 시간만 보고 lock contention은 무시하는 경우
- 데이터 접근량은 그대로 둔 채 SQL 문장만 더 세련되게 바꾸는 경우
- 분석형 질의를 latency-sensitive request path에 그대로 두는 경우
이런 실수는 SQL 텍스트는 잘 보이지만 workload behavior는 덜 보이기 때문에 자주 반복됩니다.
리뷰 체크리스트
- 이 쿼리는 실제 중요한 workload에서 비싼가
- estimated rows와 actual rows가 같은 이야기를 하는가
- 인덱스가 실제 access path를 돕는가, 아니면 옆에 존재만 하는가
- pagination, counting, aggregation이 숨어 있는 비용을 만들고 있지 않은가
- SQL 문법보다 workload shape를 바꾸는 편이 더 낫지 않은가
마무리 판단
SQL 성능 최적화의 핵심은 문법 트릭이 아닙니다. 데이터 접근량과 planner 비용, 운영 낭비를 줄이는 규율에 가깝습니다. 좋은 튜닝은 대개 쿼리를 더 똑똑하게 보이게 만드는 것이 아니라, 워크로드 자체를 더 작게 만드는 쪽에서 나옵니다.
Continue Reading
다음으로 읽기 좋은 글
MySQL 인덱스 최적화 전략 — EXPLAIN으로 쿼리 분석하기
MySQL 인덱스의 동작 원리와 최적화 전략을 EXPLAIN 분석과 함께 정리합니다. 복합 인덱스, 커버링 인덱스, 인덱스 힌트까지 실무 예제로 알아봅니다.
🗄️ DatabaseOracle vs MySQL 차이점 정리 — 문법, 함수, 시퀀스
Oracle에서 MySQL로, 또는 MySQL에서 Oracle로 마이그레이션할 때 알아야 할 주요 문법과 기능 차이를 실전 쿼리로 정리합니다.
🖥️ FrontendCore Web Vitals 최적화 — LCP, CLS, INP 실전 가이드
Core Web Vitals를 체크리스트 수준이 아니라 사용자 체감 성능과 렌더링 구조의 관점에서 정리합니다. LCP, CLS, INP가 왜 나빠지는지, 무엇부터 측정하고 어떤 순서로 최적화해야 하는지 실무 예제로 설명합니다.
📈 최신 동향PostgreSQL 18 최신 동향: 실무에서 진짜 중요한 변화
PostgreSQL 18은 단순 업그레이드 뉴스가 아닙니다. AIO, skip scan, 업그레이드 후 성능 회복, OAuth, generated columns까지 운영팀과 개발팀 모두에게 영향이 큰 변화가 들어왔습니다.
다음 탐색