콘텐츠로 이동

plan_cache_mode 정리

PostgreSQL 준비된 문장(Prepared Statement)과 드라이버 문장 캐시

  • 준비된 문장: SQL 모양을 서버에 등록해 두고 값만 바꿔 실행하는 방식
  • 실행 계획: PostgreSQL이 쿼리를 실행하기 전에 정하는 읽기 방법. 예상 비용이 가장 낮은 계획을 고름
  • 준비된 문장의 실행 계획은 두 종류
flowchart TD
    subgraph G["범용 계획(generic plan)"]
        G1[값 없이 $1, $2로<br/>한 번만 계획] --> G2[계획 시간 절약<br/>대신 모든 값에 같은 계획]
    end
    subgraph C["맞춤 계획(custom plan)"]
        C1[실제 값을 보고<br/>실행마다 새로 계획] --> C2[값에 딱 맞는 계획<br/>대신 매번 계획 시간이 듦]
    end

plan_cache_mode는 둘 중 무엇을 쓸지 정하는 설정

값 동작
auto(기본) 아래 규칙으로 자동 선택
force_custom_plan 항상 맞춤 계획
force_generic_plan 항상 범용 계획

auto 규칙

flowchart TD
    A[같은 준비된 문장 실행] --> B{몇 번째 실행인가}
    B -->|1~5번째| C[맞춤 계획]
    B -->|6번째| D[범용 계획을 만들어<br/>예상 비용을<br/>맞춤 계획 평균과 비교]
    D -->|범용이 훨씬 비쌈| C
    D -->|범용이 크게 비싸지 않음| E[이후 범용 계획 재사용]
  • 비교 기준은 예상 비용뿐. 실제 실행 시간은 보지 않음
  • 범용 계획이 실제로 느려도 자동으로 되돌아가지 않음
  • 파라미터가 없는 문장은 처음부터 범용 계획

앱 코드에 PREPARE가 없어도 드라이버가 준비된 문장을 자동으로 씀

flowchart TD
    A[앱 코드<br/>같은 SQL을 반복 호출] --> B[SQLAlchemy, asyncpg<br/>준비된 문장으로 등록<br/>문장 캐시에 보관]
    B --> C[PostgreSQL<br/>연결마다 실행 횟수를 셈]
    C --> D[같은 연결에서 6번째부터<br/>범용 계획으로 바뀔 수 있음]
드라이버 문장 캐시 설정 기본값
asyncpg statement_cache_size 100
SQLAlchemy asyncpg 방언 prepared_statement_cache_size 100

범용 계획은 값에 따라 읽을 양이 크게 다른 쿼리에서 느려짐. 값을 모르는 범위 조건(기간 등)을 무조건 0.5%로 어림하기 때문

예: 120만 행 테이블에서 작은 그룹을 1년 기간으로 조회

flowchart TD
    subgraph G["범용 계획"]
        G0[그룹 $1, 기간 $2~$3<br/>값을 모름] --> G1[기간을 1년의 0.5%로 가정<br/>예상 30행]
        G1 --> G2[기간 인덱스로<br/>120만 행 훑음]
    end
    subgraph C["맞춤 계획"]
        C0[그룹 200, 1년<br/>값을 앎] --> C1[그룹 인덱스로<br/>그룹 200의 행만 읽음]
        C1 --> C2[1,952행만 읽음]
    end
  • 예상 비용은 범용 계획이 26배 싸 보이지만 실제로는 30배 안팎 느림
  • 기간이 짧으면 범용 계획도 괜찮고 길수록 손해가 커짐

확인은 같은 연결에서

EXPLAIN EXECUTE pivot(200, '2025-01-01', '2025-12-31');
-- 계획에 $1, $2가 보이면 범용 계획

SELECT name, generic_plans, custom_plans FROM pg_prepared_statements;
-- generic_plans가 늘고 있으면 범용 계획 사용 중
  • EXPLAIN EXECUTE도 실행 횟수에 포함
  • EXPLAIN (GENERIC_PLAN)으로 범용 계획을 실행 없이 미리 볼 수 있음

해결은 적용 범위가 좁을수록 부작용이 적음

flowchart TD
    A[SET LOCAL<br/>그 트랜잭션의 쿼리만] -->|범위 넓어짐| B[연결 설정 server_settings<br/>그 연결의 모든 쿼리]
    B -->|범위 넓어짐| C[드라이버 문장 캐시 끄기<br/>모든 쿼리<br/>서버 왕복 증가]
    C -->|범위 넓어짐| D[서버, DB, 역할 단위 설정<br/>모든 클라이언트<br/>계획 CPU 증가]

권장은 값에 민감한 쿼리의 트랜잭션에만 SET LOCAL

async with engine.begin() as conn:
    await conn.execute(text("SET LOCAL plan_cache_mode = force_custom_plan"))
    result = await conn.execute(query, params)
  • 다른 쿼리는 범용 계획의 이점을 그대로 누림
  • SET LOCAL은 트랜잭션 안에서만 동작. 밖에서 쓰면 오류 없이 무시됨
  • 이미 준비된 문장에도 바로 적용됨
기능 필요 버전
plan_cache_mode PostgreSQL 12
pg_prepared_statements의 generic_plans, custom_plans PostgreSQL 14
EXPLAIN (GENERIC_PLAN) PostgreSQL 16