- Published on
Postgres 19의 pg_plan_advice — 힌트를 거절해 온 프로젝트가 내놓은 타협안
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며 — "우리는 힌트를 넣지 않는다"
- 힌트가 아니라 "조언"이라고 부르는 이유
- 조언은 실제로 어떻게 생겼나
- 조언이 먹혔는지 알려 주는 피드백
- pg_stash_advice — 애플리케이션을 안 고치고 계획 고정하기
- 위키의 반대 6개, 채점표
- 정직한 한계
- 설계 뒷이야기 — mechanism, not policy
- 그래서, 언제 쓰고 언제 쓰지 말아야 하나
- 마치며
- 참고 자료
들어가며 — "우리는 힌트를 넣지 않는다"
PostgreSQL 커뮤니티에 오래 걸려 있던 문장이 있습니다. 프로젝트 위키의 Optimizer hints discussion 페이지는 이렇게 시작합니다 — 우리는 다른 데이터베이스에서 흔히 구현된 그 방식대로 힌트를 구현할 생각이 없다.
그리고 그 아래에 이유 여섯 개가 붙어 있습니다. 요약하면 이렇습니다.
- 애플리케이션 코드 유지보수성 악화 — 쿼리에 힌트를 박으면 나중에 대규모 리팩터링이 필요해진다.
- 업그레이드 방해 — 오늘 도움이 되는 힌트가 업그레이드 후엔 성능을 깎아먹는다.
- 나쁜 DBA 습관 조장 — 진짜 원인을 찾는 대신 힌트를 붙이고 만다.
- 데이터 크기에 따라 안 맞음 — 테이블이 작을 때 맞던 힌트는 커지면 틀린다.
- 실제로 성능이 안 좋아짐 — 대개는 옵티마이저가 맞다.
- 플래너 개선 방해 — 힌트를 쓰는 사람은 문제를 프로젝트에 보고하지 않는다.
이 목록은 그냥 고집이 아닙니다. 오라클과 MySQL에서 힌트가 실제로 만들어 낸 문제들을 정리한 것에 가깝습니다. 그런데 같은 페이지가 이렇게 덧붙입니다 — 다른 힌트 시스템에서 관찰된 문제들을 피하는 제안이라면 관심이 있다.
2026년 6월 4일 나온 PostgreSQL 19 Beta 1에 그 제안이 들어갔습니다. Robert Haas가 만든 pg_plan_advice와 pg_stash_advice, 두 개의 contrib 모듈입니다. 릴리스 노트의 표현은 "사용자가 플래너 결정을 안정화하고 통제할 수 있게 해 준다"입니다.
이 글은 그게 정확히 무엇이고, 위 여섯 개 중 무엇을 지웠고 무엇을 못 지웠는지를 봅니다. 참고로 19의 최종 릴리스는 2026년 9~10월경으로 예고돼 있고, 지금은 베타입니다.
힌트가 아니라 "조언"이라고 부르는 이유
공식 문서 F.30. pg_plan_advice는 이 모듈을 이렇게 설명합니다 — 플래너의 주요 결정을 "plan advice"라는 전용 미니 언어로 기술하고, 재현하고, 바꿀 수 있게 한다. 의도는 두 가지입니다. 사용자가 좋다고 판단한 계획을 고정하는 것, 그리고 플래너가 최적이 아니라고 믿는 계획을 실험해 보는 것.
이름이 hint가 아니라 advice인 데는 설계상의 이유가 있습니다. 세 가지가 기존 힌트와 다릅니다.
첫째, 조언은 SQL 밖에 삽니다. 오라클 힌트나 pg_hint_plan은 쿼리 텍스트 안(주석)에 힌트를 넣습니다. pg_plan_advice는 pg_plan_advice.advice라는 GUC로 주거나, pg_stash_advice를 통해 쿼리 ID를 키로 별도 저장소에 넣습니다. 애플리케이션의 SQL 문자열은 손대지 않습니다. 위키 반대 1번을 정면으로 겨냥한 설계입니다.
둘째, 플래너를 대체하지 않고 탐색 공간을 좁힙니다. 이게 가장 중요한 차이입니다. 문서의 문장을 그대로 옮기면 — 어떤 조언을 주든, 해당 쿼리에 대해 코어 플래너가 고려했을 계획만 나온다. 즉 조언은 "이 계획을 써라"라는 명령이 아니라 "이 후보들은 빼고 골라라"에 가깝습니다. 플래너가 의미상 틀린 결과를 낸다고 믿는 계획은 조언이 뭐라 하든 안 나옵니다.
셋째, 부분만 고정할 수 있습니다. 조언 문자열을 통째로 박아 계획 전체를 못 박을 수도 있지만, 필요한 한 조각만 지정하고 나머지는 플래너에게 맡길 수도 있습니다. Christophe Pettus는 Hints, Part 3: Advice, Not Orders(2026-04-21)에서 이 절제된 사용법이 위키 반대 2번(업그레이드 방해)과 4번(데이터 크기)을 완화할 여지라고 짚습니다. 다만 그는 곧바로 단서를 답니다 — 이 도구는 절제를 허용할 뿐, 강제하지는 않는다.
조언은 실제로 어떻게 생겼나
추상적으로는 감이 안 오니 실물을 봅시다. 모듈을 로드하면 EXPLAIN에 PLAN_ADVICE 옵션이 생깁니다. 지금 플래너가 고른 계획을 조언 문자열로 되받아 적어 주는 기능입니다.
EXPLAIN (COSTS OFF, PLAN_ADVICE)
SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id;
문서에 실린 출력은 이렇습니다.
Generated Plan Advice:
JOIN_ORDER(f d)
HASH_JOIN(d)
SEQ_SCAN(f d)
NO_GATHER(f d)
읽는 법은 직관적입니다 — f와 d를 이 순서로 조인하고, d를 안쪽에 두고 해시 조인하고, 둘 다 순차 스캔하고, 병렬 Gather는 쓰지 않았다. 이 문자열을 그대로 다시 먹이면 같은 계획이 재현됩니다.
SET pg_plan_advice.advice = 'JOIN_ORDER(f d)';
조언 언어의 어휘는 의도적으로 좁습니다. 문서가 정의하는 태그는 대략 네 부류입니다.
- 스캔 방식:
SEQ_SCAN(target),TID_SCAN(target),INDEX_SCAN(target index_name),INDEX_ONLY_SCAN(target index_name),BITMAP_HEAP_SCAN(target),DO_NOT_SCAN(target) - 조인 순서:
JOIN_ORDER(...)— 괄호로 묶어 결합 순서를 강제하거나 느슨하게 둘 수 있습니다 - 조인 방식:
HASH_JOIN(...),MERGE_JOIN_PLAIN(...),MERGE_JOIN_MATERIALIZE(...),NESTED_LOOP_PLAIN(...),NESTED_LOOP_MEMOIZE(...),NESTED_LOOP_MATERIALIZE(...) - 병렬화:
GATHER(...),GATHER_MERGE(...),NO_GATHER(target)
여기에 파티션와이즈 조인(PARTITIONWISE)과 세미조인 유일성(SEMIJOIN_UNIQUE / SEMIJOIN_NON_UNIQUE) 통제가 붙습니다. 대상 지정은 별칭 기준이고, 같은 별칭이 여러 번 나오면 f#2처럼 발생 순번을 붙이며, 파티션이나 서브플랜까지 짚는 확장 문법도 있습니다.
Daniel Westermann의 실습 기록(2026-03-13)이 이 흐름을 잘 보여 줍니다. 100만 행 t1과 200만 행 t2를 조인하면 플래너는 이런 조언을 뱉습니다.
JOIN_ORDER(t2 t1) NESTED_LOOP_MEMOIZE(t1) SEQ_SCAN(t2) INDEX_SCAN(t1 public.t1_pkey) NO_GATHER(t1 t2)
그대로 되먹이면 전부 matched로 찍히고 계획은 그대로입니다. 반대로 조인 순서만 뒤집어 JOIN_ORDER(t1 t2)를 주면 계획이 Merge Join + Sort + Materialize로 바뀝니다. 조언이 실제로 먹는다는 걸 확인하는 가장 짧은 실험입니다.
조언이 먹혔는지 알려 주는 피드백
이 부분이 개인적으로 이 설계에서 가장 마음에 듭니다. 힌트 시스템의 고질병은 "힌트를 썼는데 무시당한 것 같은데 확인할 방법이 없다"입니다. pg_plan_advice는 조언 한 줄마다 주석으로 판정을 붙여 줍니다.
INDEX_SCAN(f no_such_index) /* matched, inapplicable, failed */
HASH_JOIN(f) /* matched */
HASH_JOIN(g) /* not matched */
JOIN_ORDER(f g) /* partially matched */
판정 어휘는 문서 기준으로 여섯 가지입니다 — matched(대상을 플래닝 중 봤고 적용 가능했다), partially matched(일부만 봤거나 함께 보지 못했다), not matched(대상을 아예 못 봤다), inapplicable(존재하지 않는 인덱스처럼 태그를 적용할 수 없었다), conflicting(두 조언이 서로 모순된다), failed(matched된 조언을 최종 계획이 따르지 않았다).
pg_plan_advice.feedback_warnings를 켜면 조언이 강제되지 않았을 때 경고를 띄웁니다. 기본값은 off입니다. 이게 왜 중요하냐면 — 조언은 시간이 지나면 썩는데(스키마가 바뀌고 인덱스가 사라지고 데이터 분포가 변하니까), 썩은 조언이 조용히 무시되는 것과 시끄럽게 경고를 뱉는 것은 운영상 완전히 다른 이야기이기 때문입니다.
pg_stash_advice — 애플리케이션을 안 고치고 계획 고정하기
pg_plan_advice는 세션 GUC로 조언을 줍니다. 실험에는 좋지만 프로덕션에서 문제 쿼리 하나를 고정하려고 세션마다 SET을 걸 수는 없습니다. 그래서 F.33. pg_stash_advice가 있습니다.
동작은 단순합니다. 쿼리 ID에서 조언 문자열로 가는 매핑(문서 용어로 advice stash)을 동적 공유 메모리에 두고, 플래닝하는 쿼리의 ID가 스태시에 있으면 해당 조언을 자동으로 적용합니다. Shaun Thomas가 pgEdge 글(2026-06-05)에서 보여 주는 사용법은 이렇습니다.
SELECT pg_create_advice_stash('production_tuning');
SELECT pg_set_stashed_advice('production_tuning', 5424487836266966148,
'INDEX_SCAN(f idx_fact_dim_id) NESTED_LOOP_PLAIN(f)');
SET pg_stash_advice.stash_name = 'production_tuning';
pg_stash_advice.stash_name은 ALTER DATABASE ... SET이나 ALTER ROLE ... SET으로도 걸 수 있어서, 사실상 애플리케이션 코드를 한 줄도 안 고치고 특정 쿼리의 계획을 못 박을 수 있습니다. SQL Server의 plan guide나 DB2의 계획 고정과 같은 계열의 운영 패턴입니다.
전제 조건이 몇 개 있습니다. shared_preload_libraries에 넣고 재시작해야 하고, compute_query_id가 켜져 있어야 합니다(auto면 모듈 로드 시 자동으로 켜집니다). 기본적으로 스태시는 데이터 디렉터리의 pg_stash_advice.tsv로 30초 간격 영속화되며, 이 주기는 pg_stash_advice.persist_interval로 조정합니다.
위키의 반대 6개, 채점표
이제 재미있는 부분입니다. 위 설계가 프로젝트가 스스로 적어 둔 반대 이유들을 얼마나 지웠는지 하나씩 봅시다.
1. 애플리케이션 유지보수성 — 지웠습니다. 조언이 SQL 밖에 있으니 쿼리 텍스트가 오염되지 않습니다. 이건 명확한 개선입니다.
5. 대개 옵티마이저가 맞다 — 구조적으로 지웠습니다. 조언은 코어 플래너가 고려할 계획 집합 안에서만 고릅니다. 플래너가 "이건 틀린 결과를 낸다"고 판단한 계획은 조언으로도 못 끌어냅니다. 즉 최악의 경우가 "느린 계획"이지 "틀린 계획"이 아닙니다. 이게 오라클식 힌트와의 근본적 차이입니다.
6. 플래너 개선 방해 — 부분적으로 지웠습니다. 피드백 주석과 EXPLAIN (PLAN_ADVICE) 덕분에 "플래너가 뭘 골랐고 내가 뭘 강제했는지"가 기계가 읽을 수 있는 형태로 남습니다. 버그 리포트에 붙이기 좋은 형식입니다. 다만 여전히 조언을 붙이고 조용히 넘어가는 사람을 막지는 못합니다.
2. 업그레이드 방해 / 4. 데이터 크기 — 부분만 지웠습니다. 조언을 최소한만 남기면 나머지는 플래너가 계속 적응합니다. 그런데 계획 전체를 통째로 고정하면 이 두 문제는 그대로 돌아옵니다. Pettus의 지적이 정확히 이겁니다 — 완전한 계획 고정은 업그레이드 간섭과 스케일링 문제를 다시 불러온다. 도구가 절제를 허용할 뿐 강제하지 않으니까요. 문서도 같은 말을 합니다 — 데이터 분포가 바뀔 때 플래너가 계획을 바꿀 수 있는 건 일반적으로 기능이지 버그가 아니다.
3. 나쁜 DBA 습관 — 못 지웠습니다. Pettus의 문장이 가장 정직합니다 — 통계 문제를 고치는 대신 pg_plan_advice를 집어드는 DBA는 똑같이 나쁜 결과를 얻는다. 그리고 그는 이걸 "기술 문제가 아니라 문화 문제"로 분류합니다. 도구로 풀 수 없는 종류의 반대입니다.
정리하면 6개 중 2개는 확실히, 1개는 상당 부분, 2개는 사용자가 절제할 때만, 1개는 전혀 못 지웠습니다. 나쁘지 않은 성적이지만 완승은 아닙니다.
정직한 한계
문서가 스스로 인정하는 것들이 꽤 됩니다. 이 부분을 건너뛰면 안 됩니다.
통제 못 하는 영역이 넓습니다. 집계를 정렬로 할지 해시로 할지 정할 수 없습니다. eager aggregation이나 partitionwise aggregation 같은 전략도 못 건드립니다. UNION, INTERSECT 같은 집합 연산의 동작도 통제 대상이 아닙니다. depesz가 정리한 커밋 메시지대로, 이 기능은 조인 방식·스캔 종류 같은 결과를 겨냥하지 비용 추정이나 선택도(selectivity) 자체를 건드리지 않습니다.
계획을 안 바꿔도 비용이 붙습니다. pg_stash_advice 문서의 표현이 명확합니다 — 계획 조언을 적용하는 것은 계획을 바꾸지 않더라도 눈에 띄는(noticeable) 성능 비용이 있다. 필요할 때만 쓰라고 못 박습니다. pgEdge 글도 조언이 플래너 루프를 중간에 끊기 때문에 작은 페널티가 불가피하다고 설명합니다. 구체적 수치는 어느 쪽도 제시하지 않으니, 여기서 숫자를 지어내진 않겠습니다.
보안 모델이 아직 미성숙합니다. 문서가 "unsophisticated"라는 단어를 씁니다. 스태시 생성·수정은 슈퍼유저나 권한자만 가능하지만, 아무 사용자나 자기 세션에 pg_stash_advice.stash_name을 걸 수 있어서 스태시 내용이 비권한 사용자에게 노출될 수 있습니다. 조언 문자열엔 인덱스명과 테이블 별칭이 들어가니 스키마 정보 유출 경로가 됩니다.
메모리를 먹습니다. 스태시는 동적 공유 메모리에 삽니다. 얼마나 많은 조언을 넣을지 정할 때 이 점을 고려하라는 게 문서의 조언입니다.
그리고 이건 1.0입니다. Haas 본인이 자기 블로그에서 (아직 커밋 전이던 시점에) 이렇게 썼습니다 — 이건 아주 명백히 버전 1.0 기술이다. 한계가 많고, 여전히 꽤 투박한 것들이 여럿 있다. 커밋은 2026년 3월 12일에 됐고 베타에 실렸지만, 그 자평이 틀렸다고 볼 이유는 없습니다.
마지막으로 depesz의 한 줄이 이 도구의 성격을 가장 잘 요약합니다 — 강력하고, 동시에 풋건이다. 요구사항이 변해도 프로덕션에 남아 있는 조언이 해를 끼칠 수 있다는 뜻입니다.
설계 뒷이야기 — mechanism, not policy
한 가지 더 알아 둘 만한 게 있습니다. Haas는 이 작업을 의도적으로 최소한으로 만들었습니다. 그의 표현을 옮기면 — 나는 의도적으로 메커니즘과 정책을 분리하려 했다. pg_plan_advice는 조언을 생성하는 가장 단순한 방법(즉 EXPLAIN)과 조언을 제공하는 가장 단순한 방법(즉 pg_plan_advice.advice 설정)만 제공하는 코어 기술이지만, 플러그 가능하다.
이 분리가 왜 중요한가 하면, 그 아래에 깔린 인프라가 pg_plan_advice 자체보다 오래 갈 수 있기 때문입니다. pganalyze가 정리한 바에 따르면, Haas는 RelOptInfo에 pgs_mask라는 비트마스크를 넣어 확장이 "이 테이블에는 순차 스캔을 쓰지 마라" 같은 제약을 플래너의 가장 이른 단계에서 걸 수 있게 했습니다. 기존 pg_hint_plan은 이런 게 없어서 코어 소스 2,500줄 이상을 복사하고 전역 설정을 조작해야 했는데, 이 인프라 위에서라면 그 복사본 대부분을 지울 수 있다는 게 pganalyze의 관측입니다. 즉 pg_plan_advice가 마음에 안 들어도, 서드파티 힌트 확장들이 훨씬 덜 위험한 방식으로 재구현될 길이 열린 셈입니다.
Haas가 처음 제안했던 세 모듈 중 pg_collect_advice는 19 문서의 contrib 목록에 없습니다. 실제로 들어간 건 pg_plan_advice(F.30)와 pg_stash_advice(F.33) 둘입니다.
그래서, 언제 쓰고 언제 쓰지 말아야 하나
문서의 경고부터 옮깁니다 — 플래너는 대개 좋은 결정을 내리기 때문에, 그 판단을 뒤집는 것은 쉽게 역효과를 낼 수 있다. 계획 조언은 플래너의 선택을 제약하는 위험이 이득보다 작을 때만 쓰는 게 중요하다.
손대기 전에 먼저 할 것들이 있습니다.
ANALYZE가 제대로 돌고 있는지, 통계 타깃이 충분한지 확인- 상관된 컬럼이면 확장 통계(
CREATE STATISTICS)를 먼저 시도 random_page_cost,effective_cache_size같은 비용 파라미터가 실제 하드웨어와 맞는지 점검- 인덱스가 없어서 생긴 문제를 조인 방식 강제로 덮으려는 건 아닌지 자문
이 넷을 다 해 보고도 플래너가 반복적으로 틀리는, 그리고 그 틀림이 비즈니스에 아픈 특정 쿼리 — 거기서부터가 pg_plan_advice의 자리입니다. Pettus의 표현대로 통계 튜닝의 대체재가 아니라, 피할 수 없는 플래너 오판에 대한 최후의 수단입니다.
쓰지 말아야 할 때
- 아직 19가 GA가 아닙니다. 지금은 베타이고, 최종 릴리스는 2026년 9~10월경 예정입니다. 프로덕션 도입을 지금 계획할 일이 아닙니다.
- 플래너 문제가 아니라 통계 문제일 때. 대부분이 여기 해당합니다.
- 쿼리 몇 개가 아니라 수십 개를 고정하고 싶어질 때. 그 시점엔 도구가 아니라 스키마나 통계나 하드웨어에 진짜 문제가 있습니다.
- 데이터 분포가 계속 변하는 테이블. 오늘 맞는 조언이 다음 달에 틀립니다 — 위키 반대 4번은 이 설계로도 안 지워졌습니다.
- 조언을 누가 언제 왜 걸었는지 기록할 문화가 없는 팀. 주인 없는 조언은 몇 달 뒤 아무도 못 건드리는 지뢰가 됩니다.
쓴다면 이렇게
조언은 최소한만 쓰십시오. 문서가 직접 권하는 방식이 이겁니다 — 조언 문자열을 다듬어 꼭 필요한 부분만 통제하고, 플래너 오판 위험이 큰 곳에만 적용하라. 계획 전체를 복사해 붙이는 순간 위키 반대 2번과 4번이 그대로 돌아옵니다. 그리고 feedback_warnings를 켜서 썩은 조언이 조용히 죽지 않게 하십시오.
마치며
pg_plan_advice를 "Postgres가 드디어 힌트를 받아들였다"로 요약하면 두 군데가 틀립니다. 첫째, 이건 명령이 아니라 제약입니다 — 플래너가 애초에 고려했을 계획 중에서만 고르니, 잘못 써도 최악이 느린 계획이지 틀린 계획이 아닙니다. 둘째, 이건 정책이 아니라 메커니즘입니다 — Haas는 최소한의 토대만 놓고 나머지는 확장에 맡겼습니다.
프로젝트가 오래 버텨 온 반대 목록에 이 설계가 답한 건 절반 남짓입니다. SQL 오염과 "옵티마이저가 대개 맞다"는 구조적으로 해결됐고, 업그레이드·스케일링 문제는 사용자가 절제할 때만 완화되며, 나쁜 DBA 습관은 도구가 풀 수 있는 문제가 아닙니다. 그 절반이 진짜 개선이라는 것과, 나머지 절반이 여전히 당신 책임이라는 것 — 둘 다 사실입니다.
19가 GA되면, 통계와 비용 파라미터를 다 손보고도 안 되는 그 쿼리 하나에, 딱 필요한 만큼만 쓰십시오. 그게 이 도구가 스스로 요구하는 사용법입니다.
참고 자료
- PostgreSQL 19 Beta 1 Released! (2026-06-04)
- PostgreSQL 19 문서 — F.30. pg_plan_advice
- PostgreSQL 19 문서 — F.33. pg_stash_advice
- PostgreSQL Wiki — Optimizer hints discussion (힌트 반대 6개 항목의 출처)
- Robert Haas — pg_plan_advice: Plan Stability and User Planner Control for PostgreSQL? (2026-03)
- Christophe Pettus — Hints, Part 3: Advice, Not Orders (2026-04-21)
- depesz — Waiting for PostgreSQL 19: Add pg_plan_advice contrib module (2026-03-22)
- Daniel Westermann (dbi services) — PostgreSQL 19: pg_plan_advice (2026-03-13)
- Shaun Thomas (pgEdge) — Looking Forward to Postgres 19: Query Hints (2026-06-05)
- pganalyze — Waiting for Postgres 19: Better Planner Hints with Path Generation Strategies