36장. 실행계획 리뷰 케이스
문제 query는 고객의 최근 주문 20건을 읽는다. 평소 8ms였는데 특정 대형 고객에서 1.8초가 되었다. 먼저 SQL 문자열만 보고 index를 추가하지 말고 bind 값, table/index size, statistics 시각, plan, buffer, concurrency를 함께 고정한다.
explain (analyze,buffers,wal,settings,summary)
select id,status,gross_amount,created_at
from orders
where customer_id=1842
order by created_at desc,id desc
limit 20;
가장 먼저 estimated rows와 actual rows의 배율을 본다. 12를 예상했는데 210,000이면 executor 보다 통계·data skew의 문제일 수 있다. ANALYZE로 해결되는지, 특정 고객의 분포가 평균에 숨었는지, 관련 column 통계가 필요한지 구분한다. estimated cost는 ms가 아니다.
actual time은 node의 start..end, rows는 loop당 값이므로 loops를 곱해 읽는다. Nested Loop 안쪽 node가 1ms여도 20만 번 돌면 주요 비용이다. Sort Method의 external merge와 Disk가 보이면 work_mem을 전역으로 키우기 전 concurrent sort 수와 query 구조를 본다.
Buffers의 shared hit는 물리 disk read 0을 뜻하지는 않지만 cache에서 찾았음을 뜻한다. read가 많으면 cold/warm 조건을 구분한다. index-only scan에 heap fetch가 많으면 visibility map·vacuum 상태를 본다. 선택한 index가 작은 limit에 유리한 순서로 row를 주는지 확인한다.
수정 후에는 성공 query만 재지 않는다. 새 index 생성 시간·lock, disk, insert/update p95, WAL bytes, vacuum, replica replay lag을 같이 비교한다. 새 plan을 강제하는 hint 대신 왜 planner가 선택했는지 설명한다. 검증 fixture는 평균 고객과 skew 고객을 둘 다 포함한다.
리뷰 결과는 가설, before/after plan, 변경 DDL, 생성·취소 런북, 추가 저장공간, rollback 기준으로 남긴다. plan image 한 장이 아니라 재현 가능한 SQL과 실제 row 분포가 증거다.