pg_hint_plan (PostgreSQL 実行計画制御ツール)

更新日:2026年5月27日

1. pg_hint_planとは

pg_hint_plan は PostgreSQL のクエリの実行計画を制御するためのモジュールです。本モジュールにより、他のリレーショナルデータベースで一般的な機能であるヒント句を PostgreSQL で利用することが可能になります。ヒント句とは実行されるクエリの実行計画の決定をプランナに任せきりにせず、プランナに実行計画を直接指示することを可能とする機能です。

PostgreSQL ではクエリの実行計画はプランナが統計情報を用いて決定します。プランナのバグや使用する統計情報が正確でないことで不適切な実行計画が作成されたり、
刻々と変化する統計情報に依存する実行計画によってクエリの実行性能が不安定になる場合があります。pg_hint_plan を用いることで、プランナの不適切な実行計画を改善したり、統計情報に依存する不安定な実行計画を安定化することが可能になります。

2026年 6月時点では、PostgreSQL 18.x に対応した 1.8.0 がリリースされています。そのほかも PostgreSQL バージョン 9.1 以降のそれぞれの PostgreSQL バージョンに対応するpg_hint_plan バージョンがリリースされています。

Windows むけのバイナリ配布物は公式サイトからリリースされていませんが、Windowsむけのビルドは可能です。SRA OSS の PostgreSQLサポートサービスでは pg_hint_plan の Windowsバイナリ提供を行っています。

PostgreSQL 9.0.x 以前のバージョンに対応していないのは PostgreSQL 側にヒント句を追加できる枠組みが備わっていないためです。

2. 本文書の環境

本文書は、PostgreSQL 18.3 と pg_hint_plan 1.8.0 を使って、pg_hint_plan の Linux 上での使い方を解説します。

3. 導入方法

本節では Rocky Linux 10.1で pg_hint_plan 1.8.0 を導入する例を示します。PostgreSQL 18 が RPM でインストールされているものとします。

◆ インストール

pg_hint_plan のダウンロードページから pg_hint_plan18-1.8.0-1.pg18.rhel10.x86_64.rpm をダウンロードします。ダウンロード後はローカルファイルに対してdnf コマンドでインストールを実行します。

# dnf install pg_hint_plan18-1.8.0-1.pg18.rhel10.x86_64.rpm

◆ セッションへのロード

セッションにおいて pg_hint_plan を利用するには予めロードが必要になります。特定のセッションでのみ利用する場合は LOAD コマンドを実行してください。

db1=# LOAD ‘pg_hint_plan’;
LOAD

全セッションで利用する場合は PostgreSQL の 設定ファイル postgresql.conf の shared_preload_libraries パラメータに ‘pg_hint_plan’ を追加してください。

shared_preload_libraries = 'pg_hint_plan'

◆ データベースへの登録

pg_hint_plan では、クエリにコメントでヒントを指定する方法に加え、hint テーブルにヒントを登録して利用することもできます。hint テーブルを利用する場合は、対象データベースに拡張機能を登録し、

$ psql -d db1 -c "CREATE EXTENSION pg_hint_plan"

postgresql.conf の pg_hint_plan.enable_hint_table を on に設定してください。

pg_hint_plan.enable_hint_table = on

なお、pg_hint_plan 1.7.0 以降では Query Identifier (query_id) を利用して対象クエリを識別するため、以下設定も必要です。

compute_query_id = on

データベースの登録が済むと次のオブジェクトが作成されます。

  • hint_plan スキーマ
  • hint_plan.hints テーブル
  • hint_plan.hints_pkey インデックス
  • hint_plan.hints_id_and_app インデックス(1.7.0より前は hint_plan.hints_norm_and_app)
  • hint_plan.hints_id_seq シーケンス

4. ヒントの使用

ヒントはクエリにコメントの形で与えます。ヒント用のコメントは通常のコメントと区別するために /*+ で始めるようにして、「/*+ ヒント */ クエリ」という書式で記述します。複数のヒントを指定する場合には空白文字で区切って並べます。

以下表に示すカテゴリのヒントが利用可能です。使用できるヒント一覧はマニュアルに記載があります。

<利用可能なヒントのカテゴリ>

スキャン方式 テーブルのスキャン方式を指定できます
結合順序 複数あるテーブルをどの順で結合するか指定できます
結合方式 テーブル結合の方式を指定できます
見積件数補正 テーブル結合結果の件数見積もりを補正する指定ができます
パラレル実行 パラレル実行を強制あるいは禁止する指定ができます
設定パラメータ SETコマンドで指定できるパラメータ設定をヒントから与えます
インデックス無効化 指定したインデックスをクエリ計画時に無効化します
結合動作制御 結合時に内部表の結果をメモ化するか制御できます

ヒント句一覧
https://github.com/ossc-db/pg_hint_plan/blob/master/docs/hint_list.md

◆ 実行例

ここから実際にヒントを与えて実行計画を変更する例を示します。この例ではヒントを指定することでプランナの計画した不適切な実行計画よりも適切な実行計画を指定しています。

本検証例のサンプルデータは以下の通り pgbench コマンドで作成しています。

$ pgbench -U postgres -i -s 10 db1
creating tables...
...
$ psql db1
db1=# VACUUM ANALYZE;
VACUUM

pgnemch コマンドで作成された pgbench_accounts テーブル、pgbench_branches テーブルに対して結合とソート操作を行うクエリの実行計画を検証します。

<ヒントを指定しない場合>

db1=# EXPLAIN ANALYZE
        SELECT * FROM pgbench_branches b
          JOIN pgbench_accounts a ON b.bid = a.bid ORDER BY a.aid;
                                                                              QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=0.57..66151.81 rows=1000000 width=461) (actual time=0.093..340.277 rows=1000000.00 loops=1)
   Buffers: shared hit=20 read=19129
   ->  Index Scan using pgbench_accounts_pkey on pgbench_accounts a  (cost=0.42..42377.43 rows=1000000 width=97) (actual time=0.083..128.160 rows=1000000.00 loops=1)
         Index Searches: 1
         Buffers: shared read=19129
   ->  Memoize  (cost=0.15..0.16 rows=1 width=364) (actual time=0.000..0.000 rows=1.00 loops=1000000)
         Cache Key: a.bid
         Cache Mode: logical
         Hits: 999990  Misses: 10  Evictions: 0  Overflows: 0  Memory Usage: 2kB
         Buffers: shared hit=20
         ->  Index Scan using pgbench_branches_pkey on pgbench_branches b  (cost=0.14..0.15 rows=1 width=364) (actual time=0.007..0.007 rows=1.00 loops=10)
               Index Cond: (bid = a.bid)
               Index Searches: 10
               Buffers: shared hit=20
 Planning:
   Buffers: shared hit=4
 Planning Time: 0.145 ms
 Execution Time: 363.231 ms
(18 rows)

ヒントを指定しない場合は Index Scan, Nested Loop, Memoize を利用した実行計画が計画されました。
これに対して Seq Scan, Hash Join を計画するようにヒント指定してみます。SeqScan(a) はテーブル a をシーケンシャルスキャンすること、HashJoin(a b) はテーブル a , b をハッシュジョインすることを意味します。ヒント指定においては、クエリ内でテーブル名に別名が付けられている場合、ヒント指定では別名を使う点に注意してください。

<ヒントを指定した場合>

db1=# /*+ HashJoin(a b) SeqScan(a) */
      EXPLAIN ANALYZE
        SELECT * FROM pgbench_branches b
          JOIN pgbench_accounts a ON b.bid = a.bid ORDER BY a.aid;
                                                                  QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=546787.57..549287.57 rows=1000000 width=461) (actual time=937.656..1018.152 rows=1000000.00 loops=1)
   Sort Key: a.aid
   Sort Method: external merge  Disk: 112576kB
   Buffers: shared hit=13754 read=2641, temp read=28139 written=28176
   ->  Hash Join  (cost=1.23..30132.72 rows=1000000 width=461) (actual time=2.499..528.706 rows=1000000.00 loops=1)
         Hash Cond: (a.bid = b.bid)
         Buffers: shared hit=13754 read=2641
         ->  Seq Scan on pgbench_accounts a  (cost=0.00..26394.00 rows=1000000 width=97) (actual time=2.192..232.446 rows=1000000.00 loops=1)
               Buffers: shared hit=13753 read=2641
         ->  Hash  (cost=1.10..1.10 rows=10 width=364) (actual time=0.139..0.183 rows=10.00 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 9kB
               Buffers: shared hit=1
               ->  Seq Scan on pgbench_branches b  (cost=0.00..1.10 rows=10 width=364) (actual time=0.061..0.062 rows=10.00 loops=1)
                     Buffers: shared hit=1
 Planning Time: 2.741 ms
 Execution Time: 1071.856 ms
(16 rows)

ヒントにより、Hash Join および Seq Scan を利用する実行計画へ変更されていることが確認できます。

◆ ヒントを予め登録する

ヒントの与え方はクエリにコメントとして直接指定する方式の他に、ヒント用のテーブル hint_plan.hints に実行計画を制御したいクエリとヒントを登録しておく方式があります。

先ほどのクエリにテーブルでヒントを指定してみます。

まず、対象クエリの クエリIDを確認します。compute_query_id = on であれば、EXPLAIN VERBOSE を実行すると Query Identifier からクエリIDを確認できます。

db1=# EXPLAIN (VERBOSE)
        SELECT * FROM pgbench_branches b
          JOIN pgbench_accounts a ON b.bid = a.bid ORDER BY a.aid;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=0.57..66151.81 rows=1000000 width=461)
   Output: b.bid, b.bbalance, b.filler, a.aid, a.bid, a.abalance, a.filler
   Inner Unique: true
   ->  Index Scan using pgbench_accounts_pkey on public.pgbench_accounts a  (cost=0.42..42377.43 rows=1000000 width=97)
         Output: a.aid, a.bid, a.abalance, a.filler
   ->  Memoize  (cost=0.15..0.16 rows=1 width=364)
         Output: b.bid, b.bbalance, b.filler
         Cache Key: a.bid
         Cache Mode: logical
         ->  Index Scan using pgbench_branches_pkey on public.pgbench_branches b  (cost=0.14..0.15 rows=1 width=364)
               Output: b.bid, b.bbalance, b.filler
               Index Cond: (b.bid = a.bid)
 Query Identifier: 3522461857817933824
(13 rows)

次に hint_plan.hints テーブルに指示対象のクエリID、アプリケーション名、ヒントを登録します。このとき、アプリケーション名に ’’(空文字列)を登録することで、どの名称のアプリケーションにも適用されます。

db1=# INSERT INTO hint_plan.hints (query_id, application_name, hints)
          VALUES ('3522461857817933824','','HashJoin(a b) SeqScan(a)');
INSERT 0 1

以下の通り、コメントでヒントを指定せずともヒントが適用されていることが確認できます。

db1=# EXPLAIN ANALYZE
        SELECT * FROM pgbench_branches b
          JOIN pgbench_accounts a ON b.bid = a.bid ORDER BY a.aid;
                                                                 QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=546787.57..549287.57 rows=1000000 width=461) (actual time=475.282..549.065 rows=1000000.00 loops=1)
   Sort Key: a.aid
   Sort Method: external merge  Disk: 112576kB
   Buffers: shared hit=13702 read=2693, temp read=28139 written=28176
   ->  Hash Join  (cost=1.23..30132.72 rows=1000000 width=461) (actual time=0.457..166.269 rows=1000000.00 loops=1)
         Hash Cond: (a.bid = b.bid)
         Buffers: shared hit=13702 read=2693
         ->  Seq Scan on pgbench_accounts a  (cost=0.00..26394.00 rows=1000000 width=97) (actual time=0.427..43.786 rows=1000000.00 loops=1)
               Buffers: shared hit=13701 read=2693
         ->  Hash  (cost=1.10..1.10 rows=10 width=364) (actual time=0.018..0.020 rows=10.00 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 9kB
               Buffers: shared hit=1
               ->  Seq Scan on pgbench_branches b  (cost=0.00..1.10 rows=10 width=364) (actual time=0.011..0.012 rows=10.00 loops=1)
                     Buffers: shared hit=1
 Planning:
   Buffers: shared hit=1
 Planning Time: 0.278 ms
 Execution Time: 584.014 ms
(18 rows)

テーブルでヒントを指定するときは以下の点に注意してください。

  • 「コメントでの指定」と「テーブルでの指定」の両方で指定した場合、「テーブルでの指定」が優先される
  • query_id は定数値を正規化して生成されるため、定数値ごとに異なるヒントを設定することはできない。例えば、SELECT * FROM emp WHERE emp_id = 100; と SELECT * FROM emp WHERE emp_id = 200; は同一のクエリIDになる

◆ Parallelヒント

もう一つ例を示します。pg_hint_plan 1.2.0 から PostgreSQL 9.6 新機能のパラレルクエリに対応しています。Parallel (テーブル名 ワーカ数 [強制度]) というヒントを指定することでクエリをパラレルクエリとして実行させることが可能です。

引き続き、pgbench で生成したテーブルを使って例を示します。

<ヒントを指定しない場合>

db1=# EXPLAIN ANALYZE
        SELECT bid FROM pgbench_accounts
          UNION ALL SELECT bid FROM pgbench_branches;
                                                             QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..31395.15 rows=1000010 width=4) (actual time=0.307..128.488 rows=1000010.00 loops=1)
   Buffers: shared hit=13588 read=2807
   ->  Seq Scan on pgbench_accounts  (cost=0.00..26394.00 rows=1000000 width=4) (actual time=0.306..82.152 rows=1000000.00 loops=1)
         Buffers: shared hit=13587 read=2807
   ->  Seq Scan on pgbench_branches  (cost=0.00..1.10 rows=10 width=4) (actual time=0.022..0.023 rows=10.00 loops=1)
         Buffers: shared hit=1
 Planning Time: 0.147 ms
 Execution Time: 152.405 ms
(8 rows)

以下の Parallel ヒントは、pgbench_accounts テーブルをワーカ数8で実行させるように指定していますが、実際はワーカ数3で実行されています。これは Parallel ヒントの強制度はデフォルトで soft 指定であり、max_parallel_workers_per_gather のみを変更してあとはプランナに任せるので、必ずしもワーカ数は指定した値で実行されるとは限りません。

<強制度 soft で Parallel ヒント指定する場合>

db1=# /*+ Parallel(pgbench_accounts 8) */
      EXPLAIN ANALYZE
        SELECT bid FROM pgbench_accounts
          UNION ALL SELECT bid FROM pgbench_branches;
                                                                 QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.00..103085.41 rows=1000010 width=4) (actual time=1.230..71.277 rows=1000010.00 loops=1)
   Workers Planned: 3
   Workers Launched: 3
   Buffers: shared hit=13682 read=2713
   ->  Parallel Append  (cost=0.00..2084.41 rows=416671 width=4) (actual time=0.175..44.610 rows=250002.50 loops=4)
         Buffers: shared hit=13682 read=2713
         ->  Parallel Seq Scan on pgbench_branches  (cost=0.00..1.06 rows=6 width=4) (actual time=0.024..0.026 rows=10.00 loops=1)
               Buffers: shared hit=1
         ->  Parallel Seq Scan on pgbench_accounts  (cost=0.00..0.00 rows=322581 width=4) (actual time=0.175..29.598 rows=250000.00 loops=4)
               Buffers: shared hit=13681 read=2713
 Planning Time: 0.157 ms
 Execution Time: 106.721 ms
(12 rows)

ワーカ数を強制するには強制度を hard に指定します。以下では、実際に指定したワーカ数 8 で実行されていることがわかります。

<強制度 hard で Parallel ヒント指定する場合>

db1=# /*+ Parallel(pgbench_accounts 8 hard) */
      EXPLAIN ANALYZE
        SELECT bid FROM pgbench_accounts
          UNION ALL SELECT bid FROM pgbench_branches;
                                                                  QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.00..103085.41 rows=1000010 width=4) (actual time=1.201..179.885 rows=1000010.00 loops=1)
   Workers Planned: 8
   Workers Launched: 8
   Buffers: shared read=16395
   ->  Parallel Append  (cost=0.00..2084.41 rows=416671 width=4) (actual time=1.456..142.900 rows=111112.22 loops=9)
         Buffers: shared read=16395
         ->  Parallel Seq Scan on pgbench_branches  (cost=0.00..1.06 rows=6 width=4) (actual time=0.472..0.473 rows=5.00 loops=2)
               Buffers: shared read=1
         ->  Parallel Seq Scan on pgbench_accounts  (cost=0.00..0.00 rows=125000 width=4) (actual time=1.454..135.136 rows=111111.11 loops=9)
               Buffers: shared read=16394
 Planning:
   Buffers: shared hit=53 read=16
 Planning Time: 4.714 ms
 Execution Time: 215.078 ms
(14 rows)

なお、parallel_setup_cost や min_parallel_table_scan_size などの並列実行に関するパラメータ設定の影響により、単一リレーションでは期待通りに並列化が有効にならない場合があります。例えば以下のケースでは、hard を指定していてもシーケンシャルスキャンが選択されています。

db1=# /*+ Parallel(pgbench_accounts 8 hard) */
      EXPLAIN ANALYZE  SELECT * FROM pgbench_accounts;
                                                          QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------
 Seq Scan on pgbench_accounts  (cost=0.00..26394.00 rows=1000000 width=97) (actual time=0.173..42.259 rows=1000000.00 loops=1)
   Buffers: shared hit=7533 read=8861
 Planning Time: 0.083 ms
 Execution Time: 65.553 ms
(4 rows)

本ケースでは、parallel_tuple_cost をデフォルトの 0.1 から 0 にすると、並列実行が選択されるようになります。

db1=# SET parallel_tuple_cost to 0;
SET
db1=# /*+ Parallel(pgbench_accounts 8 hard) */
      EXPLAIN ANALYZE  SELECT * FROM pgbench_accounts;
                                                              QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
 Gather  (cost=1000.00..1000.00 rows=1000000 width=97) (actual time=1.041..74.213 rows=1000000.00 loops=1)
   Workers Planned: 8
   Workers Launched: 8
   Buffers: shared hit=7626 read=8768
   ->  Parallel Seq Scan on pgbench_accounts  (cost=0.00..0.00 rows=125000 width=97) (actual time=0.176..9.210 rows=111111.11 loops=9)
         Buffers: shared hit=7626 read=8768
 Planning Time: 0.083 ms
 Execution Time: 103.684 ms
(8 rows)

◆ ログメッセージとデバッグ出力

クエリ実行時にヒントを解釈できなかった場合、クエリ自体はヒントが与えられなかったものとして通常通り実行されます。ヒントが適正でなかったことはログに出力されます。

以下に、存在しないヒントを入力したときの例を示します。

db1=# /*+ error_hint */
      EXPLAIN ANALYZE
        SELECT * FROM pgbench_branches b
          JOIN pgbench_accounts a ON b.bid = a.bid ORDER BY a.aid;
INFO:  pg_hint_plan: hint syntax error at or near " error_hint "
DETAIL:  Unrecognized hint keyword "error_hint".

このときのメッセージレベルは pg_hint_plan.parse_messages(デフォルト info)パラメータで指定できます。

また、pg_hint_plan.debug_print = on (デフォルト off)とすることで指定したヒントの実行計画への影響をログに出力させることが可能です。指定したヒントが以下 4 つのカテゴリに分類されて報告されます。

used hint 利用されたヒント
not used hint 利用されなかったヒント
duplication hint 矛盾するヒントが重複して利用されなかったヒント
error hint エラーが発生して利用されなかったヒント

pg_hint_plan.debug_print によるログ出力の例を示します。

LOG:  pg_hint_plan:
        used hint:
        SeqScan(a)
        HashJoin(a b)
        not used hint:
        duplication hint:
        error hint:

STATEMENT:  /*+ HashJoin(a b) SeqScan(a) */
EXPLAIN ANALYZE SELECT * FROM pgbench_branches b
  JOIN pgbench_accounts a ON b.bid = a.bid ORDER BY a.aid;

5. まとめ

本文書では pg_hint_plan について基本的な情報を取りまとめました。PostgreSQL では制御の難しい実行計画を容易に制御可能な pg_hint_plan は強力なツールといえます。
しかしながら、ヒントを利用するかどうかは慎重な検討してください。PostgreSQL のプランナは基本的には賢くかつ網羅的であり、コスト計算が適切であれば様々な実行計画の中から一番適切な実行計画が選択されるはずです。ヒントを指定しても正しい保証は無く、ある状況で適切に思えても他の多くの状況で不適切な計画を実行してしまう危険もはらんでいます。

◆ 関連サイトリンク