운영 중인 백엔드에서 성능 사고가 반복되는 가장 흔한 패턴 중 하나는 코드를 거의 안 바꿨는데 쿼리 실행계획이 갑자기 바뀌는 경우입니다. 전날까지 80ms 나오던 API가 오늘 1.8초를 찍고, 애플리케이션 레벨에서는 타임아웃과 재시도가 늘어나며, 결국 DB CPU와 커넥션 풀이 같이 무너집니다. 팀 입장에서는 “분명 같은 SQL인데 왜 갑자기 느려졌지?“라는 질문이 생기고, 원인을 찾는 동안 사용자 체감은 빠르게 악화됩니다.

이 문제의 핵심은 SQL 문장 자체보다 실행계획(Plan) 안정성입니다. 옵티마이저는 통계와 데이터 분포를 보고 매번 최적이라고 판단한 경로를 선택하지만, 실제 운영 데이터는 균일하지 않습니다. 테넌트 편차, 특정 시간대 트래픽 쏠림, 통계 갱신 타이밍 변화가 겹치면 “평소엔 잘 맞던 계획"이 순식간에 병목으로 바뀔 수 있습니다.

그래서 실무에서는 쿼리 튜닝을 1회성 작업으로 끝내지 않고, **Plan Regression Guardrail(실행계획 회귀 방지 장치)**을 운영 체계로 붙입니다. 이 글은 그 체계를 만들 때 필요한 기준을 숫자·조건·우선순위 중심으로 정리합니다.

이 글에서 얻는 것

  • 실행계획 회귀가 왜 반복되는지(통계 드리프트, 데이터 스큐, 파라미터 편차)를 운영 관점에서 이해할 수 있습니다.
  • 배포 전/후에 어떤 지표를 어떻게 비교해야 “위험한 계획 변경"을 조기에 잡는지 기준을 가져갈 수 있습니다.
  • 장애 시 30분 내 완화를 목표로 한 대응 순서(탐지 → 격리 → 완화 → 원인 고정)를 팀 런북에 반영할 수 있습니다.

핵심 개념/이슈

1) 실행계획 회귀는 “DB 문제"가 아니라 “확률적으로 반복되는 운영 이벤트"다

실행계획은 정적 파일이 아니라, 매 순간의 통계와 조건에 따라 바뀌는 선택 결과입니다. 즉 회귀는 예외가 아니라 반복 가능한 현상입니다. 대표 원인은 아래 네 가지입니다.

  1. 통계 드리프트
    • 대량 적재/삭제 이후 히스토그램이 실제 분포를 못 따라가면 옵티마이저가 잘못된 카디널리티를 추정합니다.
  2. 데이터 스큐(편향)
    • 평균적으론 좋은 인덱스가 특정 테넌트/특정 키에서는 최악이 될 수 있습니다.
  3. 파라미터 편차
    • 같은 Prepared Statement라도 입력 값 범위가 달라지면 최적 경로가 바뀝니다.
  4. 조인 순서 민감도
    • 테이블 크기나 조건 선택도 변화로 조인 순서가 뒤집히면 비용이 급증합니다.

이런 이유로 “문제 쿼리를 고쳤다"보다 중요한 건, 회귀가 재발해도 피해를 제한하는 운영 장치를 먼저 깔아두는 것입니다. 기본 성능 관측은 MySQL 인덱스/실행계획 심화, DB Lock Contention 플레이북과 함께 봐야 맥락이 잡힙니다.

2) Plan Regression은 지연시간만 보면 늦다: “계획 변화 + 비용 신호"를 같이 본다

대부분 팀이 latency_p95만 보다가 늦게 대응합니다. 회귀 탐지는 최소 아래 5개를 묶어야 합니다.

  • plan_hash 또는 queryid + normalized_sql 기준의 계획 식별값
  • rows_examined / rows_returned 비율
  • buffer_hit_ratio 혹은 디스크 읽기 증가율
  • temp table / filesort 발생 비율
  • API 레벨 p95/p99와 DB wait event

실무 임계치 예시:

  • 동일 SQL에서 plan_hash 변경 + rows_examined_ratio 3배 이상 상승
  • 10분 이동창 기준 query_duration_p95 2배 이상 상승
  • CPU는 유사한데 io_wait 30% 이상 증가

위 조건 2개 이상이면 “일시적 변동"이 아니라 회귀로 분류하고 자동 알림을 보내는 쪽이 안전합니다. 관측 알람 설계는 Observability 기준의 사용자 영향 우선 원칙을 그대로 가져오면 됩니다.

3) 배포 전 방어선: “SQL 자체"보다 “예상 계획 변화"를 검증한다

코드 리뷰에서 SQL 텍스트만 보는 팀은 회귀를 놓치기 쉽습니다. 배포 전 최소 방어선은 다음 순서가 효과적입니다.

  1. 정규화 SQL 단위 스냅샷
    • 리터럴 제거한 SQL fingerprint를 기준으로 최근 7일 대표 파라미터를 샘플링
  2. 카나리 EXPLAIN 비교
    • 배포 전후 환경에서 주요 SQL Top N의 실행계획/예상 비용 비교
  3. 위험 쿼리 차단 게이트
    • rows_estimate 혹은 cost가 기준 대비 2배 이상 튀면 배포 보류
  4. 통계 갱신 창 관리
    • 대규모 배치 직후 통계 재수집과 배포가 동시에 일어나지 않게 분리

여기서 중요한 의사결정 우선순위는 기능 릴리스 속도 < 계획 안정성입니다. 성능 SLO를 못 지키는 배포는 기능이 맞아도 결과적으로 장애입니다. 배포 운영은 배포 런북과 연결해서 게이트를 문서화해두는 게 좋습니다.

4) 사고 시 대응 우선순위: “완벽한 원인 분석"보다 “피해 반경 축소”

실행계획 회귀가 발생하면 1시간짜리 원인 분석보다 10분짜리 완화가 먼저입니다.

  • 1순위: 고위험 API 트래픽 제한(저우선순위 요청 shed)
  • 2순위: 문제 쿼리 경로 우회(캐시 TTL 상향, 읽기 경로 단순화)
  • 3순위: 계획 고정/힌트/롤백으로 즉시 안정화
  • 4순위: 통계 재수집 및 장기 수정

즉시 조치 기준 예시:

  • db_cpu > 85% 5분 지속 + query_p99 3배 상승
  • active_connections가 풀 상한의 90% 초과
  • 핵심 엔드포인트 에러율 1% 초과

이때 재시도를 무작정 늘리면 오히려 DB 부하를 증폭합니다. 서비스 보호는 Timeout/Retry/BackoffLoad Shedding/Bulkhead 기준을 함께 적용해야 합니다.

실무 적용

1) 4주 도입 플랜

1주차: 관측 표준화

  • 상위 20개 SQL fingerprint 선정(호출량 x 지연 x 비즈니스 중요도)
  • plan_hash, rows_examined, duration_p95 수집 파이프라인 추가
  • 회귀 알림 규칙 초안 생성

2주차: 배포 게이트 도입

  • 카나리 EXPLAIN 자동 비교 작업 생성
  • 위험 기준(예: cost 2배↑, rows estimate 3배↑) 초과 시 수동 승인 강제
  • 배포 체크리스트에 “Top SQL 계획 변화” 항목 추가

3주차: 완화 런북 고정

  • 회귀 발생 시 30분 대응 플로우 문서화
  • 온콜이 바로 실행할 수 있는 액션(트래픽 제한, 캐시 모드, 읽기 우회) 스크립트화
  • 게임데이로 회귀 시나리오 1회 리허설

4주차: 정책 상수화

  • 서비스별 임계치 튜닝(도메인 특성 반영)
  • 배치/통계 갱신 일정과 릴리스 일정 분리
  • 월간 리뷰: 오탐률·탐지리드타임·MTTR 점검

2) 의사결정 기준(숫자·조건·우선순위)

운영 기준은 “정확성 100%“보다 “피해 축소 속도"를 우선합니다.

  • Sev2 즉시 대응 트리거
    • 동일 SQL plan_hash 변경 + duration_p95 2배 이상 (10분)
    • rows_examined / rows_returned 비율 5배 이상 상승
    • 결제/주문/인증 경로 중 1개라도 SLO 위반
  • 배포 보류 트리거
    • 카나리 EXPLAIN에서 고위험 SQL 3개 이상 기준 초과
    • 통계 재수집 직후 2시간 이내 대규모 스키마 변경 배포
  • 우선순위
    1. 핵심 사용자 경로 지연 악화 차단
    2. DB 포화 전이 차단
    3. 루트 원인 정리 및 영구 대책

3) 팀 운영 포인트

  • SQL 소유권 명시: 핵심 SQL마다 owner를 지정하지 않으면 사고 때 판단이 지연됩니다.
  • 파라미터 샘플 저장: “평균 값"만 테스트하면 스큐를 놓칩니다. 상·중·하위 분포 값을 같이 보관하세요.
  • 배치와 배포 분리: 대량 적재 직후 통계 갱신 타이밍과 애플리케이션 배포를 겹치지 않게 운영 캘린더를 분리해야 합니다.
  • 회귀 회고 템플릿 고정: 어떤 plan이 왜 선택됐는지, 탐지는 왜 늦었는지, 자동화로 어디까지 막을 수 있는지까지 남겨야 다음 사고가 짧아집니다.

트레이드오프/주의점

  1. 계획 고정(힌트/베이스라인)은 단기 안정성은 높이지만 장기 최적화를 막을 수 있다

    • 데이터 분포가 크게 바뀐 뒤에도 과거 계획을 강제하면 새 병목이 생길 수 있습니다. 분기별 재검토가 필요합니다.
  2. 알람 기준을 너무 빡빡하게 잡으면 오탐이 늘고 온콜 피로가 누적된다

    • 초기엔 오탐률 20~30%를 허용하되, 2주 단위로 임계치를 다듬는 방식이 현실적입니다.
  3. 카나리 검증은 비용이 든다

    • EXPLAIN/샘플 실행 파이프라인을 돌리면 CI 시간과 DB 리소스가 늘어납니다. 핵심 SQL Top N부터 단계 적용해야 합니다.
  4. 앱팀-DBA 책임 경계가 모호하면 실행이 멈춘다

    • “누가 승인하고 누가 롤백하는지"를 사고 전에 정하지 않으면, 기술적으로 맞는 답이 있어도 대응이 늦어집니다.

체크리스트 또는 연습

체크리스트

  • 상위 SQL fingerprint별 plan_hash 추적이 운영 대시보드에 있다.
  • 회귀 판정 임계치(지연, rows examined, plan 변경)가 문서화되어 있다.
  • 배포 전 카나리 EXPLAIN 비교가 자동화되어 있다.
  • 온콜이 30분 내 실행 가능한 완화 액션이 런북/스크립트로 준비되어 있다.
  • 회귀 사고 후 포스트모템에 “탐지 지연 원인"과 “자동화 전환 항목"이 포함된다.

연습 과제

  1. 최근 2주간 DB 슬로우 쿼리 로그에서 상위 10개 SQL을 뽑아 fingerprint 기준으로 묶고, rows_examined_ratio 분포를 계산해보세요.
  2. 핵심 API 1개를 선택해 배포 전/후 EXPLAIN 결과를 비교하고, “배포 보류” 기준을 팀 규칙으로 문서화해보세요.
  3. 회귀 사고를 가정해 30분 대응 런북(트래픽 제한, 캐시 우회, 계획 고정, 후속 분석)을 표 형태로 작성해보세요.

관련 글