TestForge | Aidevops | 📊 Plogger ✍️ Blog 📚 Docs
plogger

AI DevOps Korea

AI 서비스 개발, 운영, 성능개선을 하나의 루프로 연결합니다

aidevops.kr에서 LLMOps, RAG, AI Agent, 관측성, 평가, 비용-성능 최적화를 실전 운영 관점으로 정리합니다.

SQL 성능 최적화 실전 가이드

· 수정 4월 21일
SQL 성능 최적화 실전 가이드
SQL 성능 최적화 실전 가이드 다이어그램
이 글에서 다루는 핵심 흐름, 아키텍처 구조, 주요 판단 포인트를 한눈에 이해할 수 있도록 정리한 그림입니다.
SQL 성능 문제는 거의 언제나 "쿼리가 느리다"로 보고되지만, 실제 원인은 대개 쿼리 문장 자체보다 workload와 access path, 데이터 양, 인덱스 설계의 불일치에 있습니다. 그래서 SQL 텍스트만 보고 튜닝하면 자주 실패합니다. 문자열은 표면일 뿐이고, 비용은 execution plan에 숨어 있기 때문입니다.

좋은 SQL 튜닝은 결국 더 영리한 문장을 쓰는 일이 아니라, DB가 해야 할 일을 얼마나 줄이느냐의 문제입니다.

시작점은 쿼리 영웅담이 아니라 workload 이해다

튜닝에서 흔한 실수는 쿼리 하나만 떼어 놓고 아래를 보지 않는 것입니다.

  • 얼마나 자주 실행되는가
  • p95에서도 느린가, 특정 burst에서만 느린가
  • 병목이 CPU인가, I/O인가, lock contention인가, network인가
  • 어떤 비즈니스 경로를 망가뜨리는가

이 맥락이 없으면 팀은 가장 눈에 띄는 느린 쿼리를 고치고, 실제로 제일 비싼 쿼리는 놓치게 됩니다.

좋은 첫 질문은 “이 SQL을 어떻게 예쁘게 만들까?”가 아니라, 우리가 어떤 workload 비용을 치르고 있는가입니다.

EXPLAIN 은 문법이 아니라 실행 계획을 보는 도구다

EXPLAINEXPLAIN 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에 맞아야 한다

인덱스는 자주 만능 해결책처럼 취급되지만, 실제로는 접근 패턴과 맞을 때만 의미가 있습니다.

좋은 인덱스 설계는 최소한 아래를 함께 봅니다.

  • WHERE selectivity
  • JOIN 경로 지원
  • 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

다음으로 읽기 좋은 글

다음 탐색

이 주제를 시스템 관점으로 더 이어서 보기