PostgreSQLでインデックスを作成するとき、通常はテーブル内のすべての行がインデックスの対象になります。しかし、実際の業務では「検索対象になるのは全体のごく一部だけ」というケースも珍しくありません。
そのようなときに有効なのがPartial Index(部分インデックス)です。Partial Indexは、WHERE条件を満たす行だけをインデックスへ格納できるため、通常のインデックスより小さく保ちやすく、検索性能や更新コストの面で有利になる場合があります。
特に、未処理データ、削除されていないデータ、特定ステータスのデータ、NULLではないデータなど、テーブル全体の一部だけを頻繁に検索するケースで効果を発揮します。この記事では、PostgreSQLのPartial Indexがどのような場合に有効なのか、通常のインデックスとの違い、実際のSQL例、注意点まで整理して解説します。
- PostgreSQLのPartial Indexとは
- 通常のインデックスとの違い
- 特に有効なケース1:未処理・未完了データだけを頻繁に検索する
- 特に有効なケース2:論理削除されていないデータだけを検索する
- 特に有効なケース3:NULLではないデータだけを検索する
- 特に有効なケース4:特定の少数カテゴリだけ頻繁に使う
- 特に有効なケース5:特定期間のデータだけ頻繁に扱う
- Partial Indexが小さいと何がうれしいのか
- Partial Indexは更新負荷の軽減にもつながる
- Partial Indexを使うにはクエリ条件が述語と一致する必要がある
- パラメータ化クエリでは注意が必要
- Partial UNIQUE Indexという便利な使い方もある
- 通常のインデックスのほうが適しているケース
- 対象行が多すぎるPartial Indexはメリットが小さい
- Partial Indexを乱立させるのは避ける
- 実際に利用されているかEXPLAINで確認する
- ANALYZEと統計情報も重要
- Partial Indexの有効性を判断するチェックポイント
- 通常インデックスとPartial Indexの使い分け例
- まとめ:Partial Indexは「よく使う少数の行」だけを索引化したいときに強い
PostgreSQLのPartial Indexとは
Partial Indexとは、テーブルの全行ではなく、指定した条件を満たす行だけを対象として作成するインデックスです。
PostgreSQLでは、CREATE INDEX文の末尾にWHERE句を付けることで作成できます。
CREATE INDEX idx_orders_pending
ON orders (created_at)
WHERE status = 'pending';
この例では、ordersテーブルのすべての行ではなく、status = 'pending'の行だけがインデックスへ登録されます。
PostgreSQL公式ドキュメントでも、Partial Indexはテーブルの一部分だけを対象とするインデックスとして説明されており、特定の値を除外したり、一部の行だけを高速に検索したりする用途が示されています。[参照] PostgreSQL公式ドキュメント「Partial Indexes」
通常のインデックスとの違い
通常のインデックスでは、基本的に対象列についてテーブル全体の行がインデックスへ登録されます。
たとえば次のインデックスでは、ordersテーブル内の全行についてcreated_atがインデックス対象になります。
CREATE INDEX idx_orders_created_at
ON orders (created_at);
一方、Partial Indexなら次のように対象を限定できます。
CREATE INDEX idx_orders_pending_created_at
ON orders (created_at)
WHERE status = 'pending';
違いをまとめると次のようになります。
| 項目 | 通常のインデックス | Partial Index |
|---|---|---|
| 対象行 | 原則として全行 | WHERE条件を満たす行のみ |
| インデックスサイズ | 大きくなりやすい | 対象が少なければ小さくできる |
| 更新コスト | 対象列の全行で発生し得る | 条件に該当する行を中心に発生 |
| 利用できる検索 | 幅広い | 述語条件と整合する検索 |
Partial Indexは万能な上位互換ではなく、検索対象が明確に偏っている場合に強い特殊なインデックスと考えると分かりやすくなります。
特に有効なケース1:未処理・未完了データだけを頻繁に検索する
Partial Indexが非常に使いやすい代表例が、ステータス管理された業務データです。
たとえば1000万件の注文データがあり、そのうち処理待ちの注文が5万件だけだとします。業務画面では次のようなSQLが頻繁に実行されるとします。
SELECT id, created_at, customer_id
FROM orders
WHERE status = 'pending'
ORDER BY created_at;
このとき、テーブル全体を対象としたインデックスを作るより、処理待ちだけを対象にしたPartial Indexが候補になります。
CREATE INDEX idx_orders_pending
ON orders (created_at)
WHERE status = 'pending';
1000万件すべてではなく5万件程度だけを保持するインデックスになれば、インデックスサイズを大きく削減できる可能性があります。
特に「完了済みデータは大量に蓄積されるが、普段見るのは未完了だけ」というシステムでは効果が期待できます。
特に有効なケース2:論理削除されていないデータだけを検索する
業務システムでは、行を物理的にDELETEせず、deleted_atやis_deletedによって論理削除する設計がよくあります。
たとえば通常の検索がほぼ必ず次の条件を持つとします。
SELECT *
FROM users
WHERE deleted_at IS NULL
AND email = 'user@example.com';
この場合は次のようなPartial Indexを作れます。
CREATE INDEX idx_users_active_email
ON users (email)
WHERE deleted_at IS NULL;
削除済みユーザーが大量に残っていても、通常検索で必要な有効ユーザーだけをインデックスへ含められます。
特に長期間運用して削除済みデータが増えていくシステムでは、全行対象のインデックスより効率的になる場合があります。
特に有効なケース3:NULLではないデータだけを検索する
ある列の大半がNULLで、値が入っている一部の行だけを検索する場合もPartial Indexが有効です。
たとえばprocessed_at列の大部分がNULLで、処理済みデータだけを検索するとします。
SELECT id, processed_at
FROM jobs
WHERE processed_at IS NOT NULL;
この場合は次のようなPartial Indexを作成できます。
CREATE INDEX idx_jobs_processed_at
ON jobs (processed_at)
WHERE processed_at IS NOT NULL;
反対に、未処理データだけを頻繁に扱うなら、次のようにNULL側だけを対象にできます。
CREATE INDEX idx_jobs_unprocessed
ON jobs (id)
WHERE processed_at IS NULL;
どちらを選ぶべきかは、「実際によく検索する側がどちらか」で決まります。
特に有効なケース4:特定の少数カテゴリだけ頻繁に使う
データのカテゴリに強い偏りがある場合にもPartial Indexは使えます。
たとえばログテーブルにINFO、WARNING、ERRORがあり、99%がINFO、ERRORは1%未満だとします。
運用担当者が主にERRORログだけを検索するのであれば、次のようなインデックスを作れます。
CREATE INDEX idx_logs_error_time
ON logs (created_at)
WHERE level = 'ERROR';
全ログをインデックスに含めるより、エラー行だけを小さなインデックスに保持できます。
このように、全体では少数派だが業務上よく検索されるデータがある場合はPartial Indexの有力候補です。
特に有効なケース5:特定期間のデータだけ頻繁に扱う
状況によっては、最近のデータだけを頻繁に扱うシステムもあります。
たとえば古い履歴はほとんど参照されず、新しいデータだけを日常的に検索する場合です。
概念的には次のようなインデックスを考えたくなるかもしれません。
CREATE INDEX idx_recent_orders
ON orders (created_at)
WHERE created_at >= DATE '2026-01-01';
ただし、この方法は固定日付なら利用できますが、「常に直近30日」のような動的条件をPartial Indexの述語へそのまま入れる設計には注意が必要です。インデックス述語では、インデックス定義時に安定して判定できる式が求められます。
時間とともに対象範囲が移動する大量データでは、Partial Indexだけでなくテーブルパーティショニングも比較検討するとよいでしょう。
Partial Indexが小さいと何がうれしいのか
インデックスサイズが小さくなることには、単なるディスク容量削減以上の意味があります。
まず、インデックスページが少なくなれば、検索時に読み込むページ数を減らせる可能性があります。また、頻繁に使うインデックスが共有バッファなどのメモリ上へ収まりやすくなれば、ディスクI/Oを減らせる場合があります。
さらにINSERTやUPDATEなどの際、Partial Indexの条件に該当しない行であれば、そのインデックスへ新しいエントリを追加する必要がありません。
たとえば完了済みデータが大量に追加されるシステムで、未完了データだけをPartial Indexに含めているなら、全行を対象とするインデックスよりメンテナンス対象を減らせる可能性があります。
Partial Indexは更新負荷の軽減にもつながる
通常のインデックスは、対象列を持つ行のINSERTやUPDATEに伴って維持する必要があります。
Partial Indexでは、述語に該当しない行はインデックスへ含まれません。そのため、条件によってはインデックス更新量を削減できます。
たとえば次のインデックスがあるとします。
CREATE INDEX idx_tasks_open
ON tasks (priority)
WHERE completed = false;
完了済みタスクが大量に存在していても、それらはこのインデックスに入りません。
一方、未完了だった行を完了済みに更新すると、その行はPartial Indexから除外されることになります。したがって、条件列自体を頻繁に変更する場合には、その更新コストも考慮する必要があります。
Partial Indexを使うにはクエリ条件が述語と一致する必要がある
Partial Indexで非常に重要なのが、クエリのWHERE条件がインデックスの述語を満たすとPostgreSQLが判断できなければ利用されないという点です。
たとえば次のPartial Indexがあります。
CREATE INDEX idx_orders_pending
ON orders (created_at)
WHERE status = 'pending';
次のクエリは条件が明確に一致しています。
SELECT *
FROM orders
WHERE status = 'pending'
AND created_at >= DATE '2026-08-01';
一方、クエリ側の条件からPartial Indexの述語が成立するとPostgreSQLが認識できなければ、そのインデックスは候補になりません。
PostgreSQL公式ドキュメントでも、クエリのWHERE条件がインデックスの述語を数学的に一般証明する高度な定理証明器を備えているわけではなく、単純な一致や認識可能な不等式などによって利用可否を判断すると説明されています。[参照] PostgreSQL公式ドキュメント「Partial Indexes」
パラメータ化クエリでは注意が必要
Partial Indexを使用するときに見落としやすいのが、Prepared Statementやパラメータ化されたSQLとの関係です。
たとえばPartial Indexの条件が次のようになっているとします。
WHERE status = 'pending'
SQL側もリテラルでstatus = 'pending'なら関係が明確です。しかし一般化されたパラメータを利用している場合、プラン作成時にその値が必ず’pending’になるとは判断できないケースがあります。
PostgreSQL公式ドキュメントでも、パラメータ化されたクエリ句は、すべての可能なパラメータ値についてPartial Indexの述語を含意するとは判断できないため、Partial Indexとの組み合わせで問題になることがあるとされています。
ORMやアプリケーションフレームワークから発行されるSQLでPartial Indexを設計するときは、実際の実行計画を確認することが重要です。
Partial UNIQUE Indexという便利な使い方もある
Partial Indexは性能改善だけでなく、条件付きの一意性を表現する用途にも使えます。
たとえば論理削除を採用しているusersテーブルで、「削除されていないユーザー同士ではメールアドレスを重複させたくないが、削除済み行との重複は許可したい」とします。
通常のUNIQUE制約だけでは、この要件を表現しにくいことがあります。その場合は次のようなPartial UNIQUE Indexが利用できます。
CREATE UNIQUE INDEX ux_users_active_email
ON users (email)
WHERE deleted_at IS NULL;
これにより、deleted_at IS NULLの有効ユーザーについてだけemailの重複を禁止できます。
条件付きで一意性を保証したい場合には、Partial Indexならではの非常に実用的な利用方法です。
通常のインデックスのほうが適しているケース
Partial Indexが有効なのは、検索対象に明確な偏りがある場合です。すべてのケースで通常のインデックスより優れているわけではありません。
たとえばstatusがpending、processing、completedの3種類あり、どの状態もほぼ同じ頻度で検索されるなら、特定ステータスだけのPartial Indexを作るメリットは小さくなります。
また検索条件が毎回大きく異なる場合、Partial Indexでは利用できるクエリが限定されすぎる可能性があります。
このような場合は、全体をカバーする通常のB-treeインデックスのほうが汎用性があります。
対象行が多すぎるPartial Indexはメリットが小さい
Partial Indexを作っていても、条件に該当する行がテーブル全体の90%や95%を占めるなら、通常のインデックスとのサイズ差はそれほど大きくなりません。
たとえば全100万行のうち95万行がactive = trueであり、次のPartial Indexを作ったとしても、多くの行がインデックスに入ります。
CREATE INDEX idx_users_active
ON users (email)
WHERE active = true;
この場合は、Partial Indexによる容量や更新コスト削減の効果が限定的になる可能性があります。
Partial Indexは一般に、インデックスへ含めたい行が全体の一部に偏っているほど効果を出しやすいと考えるとよいでしょう。ただし明確な万能の割合があるわけではなく、実際のデータ分布とクエリによって判断します。
Partial Indexを乱立させるのは避ける
たとえばcategory列に多数の値があるからといって、カテゴリごとに大量のPartial Indexを作成するのは、必ずしもよい設計ではありません。
多数のPartial Indexを作ると、ストレージ使用量だけでなく、INSERT・UPDATE・DELETE時のインデックス管理、統計や運用管理の複雑さも増します。
複数の値を幅広く検索するのであれば、categoryをキーとする通常のインデックスのほうがシンプルで効率的な場合があります。
Partial Indexは「インデックスを細かく分割するための仕組み」というより、明確に不要な行をインデックスから除外する仕組みとして考えたほうが設計しやすくなります。
実際に利用されているかEXPLAINで確認する
Partial Indexを作成したら、想像だけで効果を判断せずEXPLAINやEXPLAIN ANALYZEで実行計画を確認します。
たとえば次のように確認できます。
EXPLAIN ANALYZE
SELECT id, created_at
FROM orders
WHERE status = 'pending'
ORDER BY created_at;
実行計画に作成したインデックスを利用するIndex ScanやIndex Only Scanなどが表示されるかを確認します。
ただし、インデックスが存在してもPostgreSQLがSeq Scanのほうが安いと判断すれば利用されないことがあります。これは異常ではありません。
データ件数が少ない、条件に該当する割合が高い、統計情報の状況などによっては、シーケンシャルスキャンのほうが効率的な場合があります。
ANALYZEと統計情報も重要
PostgreSQLのクエリプランナーは、統計情報を利用して各アクセス方法のコストを推定します。
インデックスを追加した直後や大量のデータ変更後に期待した実行計画にならない場合は、統計情報が適切か確認することも重要です。
ANALYZE orders;
ただし、「Partial Indexが使われないからとりあえずANALYZEすれば直る」というものではありません。クエリ条件が述語を満たしているか、対象行率が低いか、インデックスのキー列が検索内容に適しているかも確認する必要があります。
Partial Indexの有効性を判断するチェックポイント
Partial Indexを検討するときは、次のような点を確認すると判断しやすくなります。
- 検索対象になる行がテーブル全体の一部に偏っているか
- 同じWHERE条件が頻繁に使われているか
- 大量の不要な行を通常インデックスから除外できるか
- 対象条件が長期間安定しているか
- 条件列の変更頻度が高すぎないか
- 実際のクエリ条件がPartial Indexの述語を満たすか
- EXPLAIN ANALYZEで効果を確認できるか
特に「一部の行だけ」という点と、「その一部を頻繁に検索する」という2条件がそろうとPartial Indexのメリットが出やすくなります。
通常インデックスとPartial Indexの使い分け例
| 状況 | 向いている候補 |
|---|---|
| 全データをさまざまな条件で検索する | 通常のインデックス |
| 未処理データだけ頻繁に検索する | Partial Index |
| 論理削除されていない行だけ検索する | Partial Index |
| 少数のERRORログだけ頻繁に検索する | Partial Index |
| NULLではない少数行だけ検索する | Partial Index |
| どの値も均等に検索する | 通常のインデックスを検討 |
| 条件付きで一意性を保証したい | Partial UNIQUE Index |
このように、Partial Indexは特定のアクセスパターンが明確なシステムほど使いやすい機能です。
まとめ:Partial Indexは「よく使う少数の行」だけを索引化したいときに強い
PostgreSQLのPartial Indexは、テーブル全体ではなく特定条件を満たす行だけをインデックスへ格納できる機能です。
通常のインデックスと比べて特に有効なのは、未処理データ、未削除データ、ERRORログ、NULLまたはNOT NULLの一部行など、テーブル全体の中では少数だが頻繁に検索する行が明確に決まっているケースです。
対象行を限定することでインデックスサイズを小さくでき、I/Oやキャッシュ効率の改善、INSERT・UPDATE時のインデックス維持コスト削減につながる可能性があります。またPartial UNIQUE Indexを利用すれば、「有効な行だけメールアドレスを一意にする」といった条件付き制約も実現できます。
一方で、Partial Indexを利用するにはクエリ条件がインデックスのWHERE述語を満たすとPostgreSQLが判断できる必要があります。対象行がテーブルの大部分を占める場合や、さまざまな条件で検索する場合には、通常のインデックスのほうが適していることもあります。
したがって、「全体の一部だけを何度も検索しているか」を最初の判断基準にし、実際のデータ分布とEXPLAIN ANALYZEの結果を確認して採用するのが、PostgreSQLでPartial Indexを効果的に使う基本です。


コメント