
PostgreSQL18連載の6本目の記事です。
PostgreSQL 18がリリースされました。リリースされた機能のうち私は「B-treeインデックスのスキップスキャン」機能が気になったので、機能の特徴を深堀りしつつ、実際の挙動を確認してみます。
B-treeインデックスのスキップスキャンとは
複合インデックス(複数の列で構成されるインデックス)の利用効率を劇的に向上させる新しいスキャン方法です。
従来の課題
PostgreSQLでは、例えば(列A, 列B)
という順番で複合インデックスを作成した場合、これまではWHERE句に先頭の「列A」の条件がないと、インデックスを効率的に使えませんでした。
例えば、WHERE 列B = 'hoge'
というクエリでは、せっかくの (列A, 列B)
インデックスをうまく使えず、結果としてテーブル全体をスキャン(シーケンシャルスキャン)してしまう、あるいは、インデックスを使えたとしても「列A」の条件を指定していない分だけパフォーマンスが低下する原因となっていました。
これにより「列B」だけのインデックスを別途作成するケースもあり、ストレージの無駄やデータ更新時のコスト増につながっていました。
新機能による効果
スキップスキャン機能では、インデックスの先頭列(列A)がWHERE句になくてもPostgreSQLがインデックスの内部を「スキップ」しながら、2番目以降の列(列B)の条件に合うデータをより効率的なアルゴリズムで探し出してくれます。
機能の仕組み
- まず、インデックスの先頭列(列A)にどのような値の種類があるかを把握
- 次に、列Aの各値の「先頭」にジャンプ
- そこから、2番目の列(列B)が条件に合致するかどうかをチェック
これを列Aの値の種類ぶんだけ繰り返すことで、インデックス全体を舐めるよりもはるかに効率的にデータを見つけ出すことができます。
公式ドキュメントでは、上記2,3における挙動が詳細に説明されています。
スキップスキャンは、インデックス列のすべての可能な値に一致する動的な等価制約を内部的に生成することによって機能します
つまり上記の例であれば、列Bに対してのみ条件が指定されている場合でも列Aに対する条件を内部的に生成して動作することを意味します。
例えば、(x, y)に対するインデックスがあり、クエリ条件がWHERE y = 7700である場合、B-treeインデックススキャンはスキップスキャン最適化を適用できる可能性があります。これは一般的に、クエリプランナが、テーブルで利用可能なインデックスを考慮した上で、Nのすべての可能な値(またはインデックスに実際に格納されているすべてのxの値)に対してWHERE x = N AND y = 7700という検索を繰り返すことが最も高速なアプローチであると予測する場合に発生します。
この仕組みは、インデックスの先頭列の値の種類が少ない(カーディナリティが低い)場合に特に高い効果を発揮します。 例えば、「性別」「注文ステータス」「都道府県」のように、値のバリエーションが限られている列が先頭にある複合インデックスで非常に有効だと言えます。
https://www.postgresql.org/docs/current/indexes-multicolumn.html
実際に検証してみる
さて、PostgreSQL 17と18を比較してどのようにクエリ応答性能が変化しているか比べてみます。
検証環境
- Windows11 Home
- WSL2(ubuntu)
PostgreSQL 18 の場合
下準備
データベースの準備
まずはPostgreSQL18が動く環境を準備します。(今回はDockerを利用)
docker run --name pg18-handson -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres:18 |
コンテナの起動を確認。
$ docker ps |
psql
コマンドで入ってみると、、しっかりと動いてることが確認できたので次に進みます。
$ docker exec -it pg18-handson psql -U postgres |
テーブルの作成と検証用データの投入
orders
テーブルを作成し、100万件のデータを投入します。
CREATE TABLE orders ( |
複合インデックスの作成
スキップスキャンの効果を検証するため、カーディナリティの低い order_status
を先頭にした複合インデックスを作成します。
CREATE INDEX idx_orders_status_customer ON orders (order_status, customer_id); |
統計情報の更新
クエリオプティマイザが正しい判断を下せるように、テーブルの統計情報を最新の状態にします。
ANALYZE orders; |
スキップスキャン機能の検証
では、ここから実際に機能の検証を行ってみます。
まずは複合インデックスの2番目の列であるcustomer_id
のみをwhere句に指定して検索してみます。
postgres=*# EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123; |
Index Scanが行われており、 0.276ms
で応答しました。高速ですね。ただし、実行計画にはスキップスキャンを示す表記が登場しないため、厳密にはスキップスキャンを行ったか否かを判断できないのが悩ましいところです。今回のクエリは複合インデックスの2列目に対してのみ等価条件を指定しており、1列目データ群はカーディナリティが低いため、”おそらく”スキップスキャンが行われているだろうと考えられます。
次に、Seq Scanが採択された場合にどのような結果となるかも試してみます。
postgres=*# SET LOCAL enable_indexscan = off; |
18.625ms
で応答しており、Index Scanよりも大幅に遅い結果になりました。
PostgreSQL 17 の場合
次は同様の検証をPostgreSQL 17にて実施してみます。
下準備
以下コマンドでコンテナを立ち上げた後は18の時と同じ手順でデータを投入していきます。
docker run --name pg17-handson -e POSTGRES_PASSWORD=mysecretpassword -p 5433:5432 -d postgres:17 |
データが揃ったところで実際に検索してみます。
最も大きな変更点として、PostgreSQL 18からEXPLAIN ANALYZEを実行すると、バッファ使用量が自動的に表示されるようになりました。これまではBUFFERSオプションを明示的に指定する必要がありましたが、18からは標準で出力されます。
先日の山本さんの記事にて触れられてましたが、PostgreSQL 17時点ではEXPLAIN ANALYZE
のみではバッファ使用量が出力されないので、Buffersオプションを付けて実行します。
postgres=# EXPLAIN (ANALYZE,BUFFERS) SELECT * FROM orders WHERE customer_id = 123; |
2.220ms
で応答しました。
次に、Seq Scan時の応答性能を確認しておきます。
postgres=*# SET LOCAL enable_indexscan = off; |
結果は18.132ms
でした。試行回数は少ないですが、バージョン18と大きな乖離があるわけではないと考えられます。
結果の考察
改めてバージョン18および17で実施した検証結果をまとめると以下の通りです。
Type | ver.18 | ver.17 |
---|---|---|
Index Scan | 0.276 ms | 2.220 ms |
Seq Scan | 18.625 ms | 18.132 ms |
Index Scan性能は約8倍の差がありますね。18単体の結果を見た時点ではスキップスキャンによって高速化してるのかが正直分かりづらかったですが、こうして17と比較するとスキップスキャンが機能していることが確認できます。
まとめ
PostgreSQL 18で追加されたスキップスキャン機構により、Index Scan時のクエリ応答性能が向上しました。また、複合インデックスを有効活用できるシーンが増えたことにより、不要なインデックスを削除でき、ディスク容量を節約するとともに登録・更新時の性能の向上も期待できます。
この新機能の特徴をしっかりとおさえた上で、インデックス設計および性能検証を行っていきましょう。