몇 백/천 줄 되어가는 쿼리의 탑을 보고 있자니 답답하기 그지없다.
어느 부분을 어떻게 건드려야 할지 감이 오지 않을 때도 있고,
건드렸다가 다른 곳이 무너질 것 같은 불안감도 있다.
그럴 때마다 드는 생각이 있다.
"어디서부터 봐야 하지."
---
실행 계획부터 본다.
쿼리가 길어도 DB 입장에서 실제로 무거운 구간은 따로 있다.
전체를 다 이해하려 할 게 아니라, 비용이 가장 큰 구간을 먼저 찾는 게 맞다.
EXPLAIN을 보면 각 단계의 예상 비용이 나온다.
그 중 rows가 튀거나 type이 ALL인 구간이 병목이다.
긴 쿼리도 결국 여러 단계의 조합이다.
병목 구간 하나를 찾으면 나머지는 잠깐 눈을 감아도 된다.
---
그 다음엔 왜 그 구간이 비싼지를 따진다.
크게 세 가지 중 하나다.
첫째, 너무 많은 데이터를 읽고 있다.
인덱스가 없거나, 있어도 타지 못하는 상황이다.
둘째, 읽은 데이터를 정렬하거나 집계하느라 시간이 걸린다.
filesort나 Using temporary가 여기에 해당한다.
셋째, 조인이 잘못 붙어 있다.
드라이빙 테이블이 뒤바뀌거나, 조인 조건에 인덱스가 없으면 중간 결과가 폭발한다.
원인이 셋 중 어디에 해당하는지가 먼저다.
원인 없이 수정부터 하면 운 좋게 빨라질 수는 있지만, 왜 빨라졌는지 모른다.
---
인덱스를 추가하면 해결되는 경우도 있지만, 그게 전부는 아니다.
쿼리를 다시 쓰는 게 더 효과적일 때가 많다.
서브쿼리가 메인 쿼리보다 먼저 실행되길 바랐는데,
옵티마이저가 순서를 바꿔버리면 의도와 다르게 동작한다.
이럴 때 인덱스를 추가해봤자 근본적인 문제가 해결되지 않는다.
WHERE 조건의 순서를 바꾸거나, 서브쿼리를 WITH 절로 올리거나, JOIN 순서를 명시하는 게 더 직접적인 해결책이 된다.
---
그러면 옵티마이저를 믿어야 할까, 의심해야 할까.
기본적으로는 믿는다.
통계 기반으로 수천 번의 계산을 거쳐 실행 계획을 짠다.
"이게 더 빠를 것 같은데"라는 개발자의 감보다 대부분 낫다.
다만 통계가 오래됐거나, 데이터 분포가 극단적으로 쏠려 있을 때는 빗나간다.
그 순간만큼은 힌트나 FORCE INDEX로 개입이 필요할 수 있다.
하지만 그건 최후의 수단이다.
힌트를 남용하면 데이터가 바뀌었을 때 오히려 더 느려지는 상황이 생긴다.
옵티마이저가 알아서 적응할 기회를 막아버리는 것이다.
---
그런데 여기서 한 가지 유혹이 있다.
"일단 캐시를 붙이면 되지 않나."
틀린 말은 아니다.
쿼리가 느려도 결과가 캐시에 있으면 사용자는 빠르다고 느낀다.
하지만 캐시는 느린 쿼리를 고치는 게 아니라 덮는 것이다.
캐시가 없는 순간 — 처음 요청이 들어올 때, TTL이 만료됐을 때, evict가 일어났을 때 —
느린 쿼리는 그대로 DB로 간다.
트래픽이 몰리는 시점에 캐시가 한꺼번에 만료되면,
수십 개의 느린 쿼리가 동시에 DB로 쏟아진다.
이걸 Cache Stampede라고 한다.
캐시를 붙였는데 오히려 더 위험한 상황을 만들게 된다.
---
그래서 캐시와 쿼리 튜닝은 역할이 다르다.
쿼리 튜닝은 느린 것을 빠르게 만든다.
캐시는 빠른 것을 더 빠르게, DB 부하를 줄이는 용도다.
순서가 있다.
쿼리가 충분히 빠르지 않은 상태에서 캐시를 먼저 붙이면,
캐시가 없는 순간의 리스크가 너무 커진다.
---
덮어두기 전에, 고쳐야 할 것과 덮어도 되는 것을 구분하는 게 먼저다.
자주 조회되고 잘 바뀌지 않는 데이터라면 캐시가 맞다.
반면 실시간성이 중요하거나 쓰기가 잦은 데이터는 캐시가 오히려 복잡도를 높인다.
느린 쿼리에 캐시를 붙이는 건, 탑에 판자를 덧대는 것과 비슷하다.
당장은 버텨도 구조 자체가 약하면 언젠가는 무너진다.
'Loopers > WIL' 카테고리의 다른 글
| 머물러 있던 자리를 벗어난 10주 (0) | 2026.07.23 |
|---|---|
| Week 6. 이론과 수치 (0) | 2026.06.26 |
| Week 4. 동시성 제어 (0) | 2026.06.12 |
| Week 3. 도메인 주도 설계 (0) | 2026.05.29 |
| Week 2. 설계는 선택의 연속이다. (0) | 2026.05.22 |