PostgreSQLにおけるすべてのインデックスは二次的なインデックス、つまり各インデックスはテーブルの主要なデータ領域(PostgreSQLの用語ではテーブルのヒープと呼ばれます)とは別に格納されています。
このことは、通常のインデックススキャンにおいて、各行の検索にはインデックスとヒープの両方からデータを取得する必要があることを意味します。
さらに、指定のインデックス可能なWHERE
条件に適合するインデックスのエントリは、通常、インデックス内の近い位置にあるのに対し、そこから参照されるテーブルの行はヒープ内のあらゆるところにあるかもしれません。
このため、インデックススキャンにおけるヒープアクセスの部分では、ヒープに対する多くのランダムアクセスがありますが、これは遅い可能性があり、特に伝統的な回転型メディアでは遅くなります。
(11.5. 複数のインデックスの組み合わせで説明したように、ビットマップインデックスはヒープアクセスをソートした順で行うことでこのコストを緩和しようとするものですが、それはある程度までしかできません。)
このパフォーマンス問題を解決するため、PostgreSQLはインデックスオンリースキャンをサポートします。 これは、問い合わせに対してヒープアクセスをせずにインデックスのみで回答できるものです。 基本的な考え方は、関連するヒープのエントリを参照せずに、各インデクスエントリから直接に値を返すというものです。 この方法が使用できるためには2つの基本的な制限があります。
インデックスの種類がインデックスオンリースキャンをサポートする必要があります。 Bツリーインデックスはいつでもインデックスオンリースキャンをサポートします。 GiSTとSP-GiSTは一部の演算子クラスでインデックスオンリースキャンをサポートしますが、サポートしない演算子クラスもあります。 他のインデックスの種類はインデックスオンリースキャンをサポートしません。 根本的な必要条件は、インデックスが各インデックスのエントリに対応する元のデータ値を物理的に格納していなければならない、あるいはそれを再構築できる必要がある、ということです。 その反例として、GINインデックスでは、各インデックスエントリが通常は元のデータ値の一部しか保持していないため、インデックスオンリースキャンをサポートすることができません。
問い合わせはインデックスに格納されている列だけを参照しなければなりません。
例えばテーブルの列x
とy
にインデックスがあり、そのテーブルにはさらに列z
がある場合、次の問い合わせはインデックスオンリースキャンを使用できます。
SELECT x, y FROM tab WHERE x = 'key'; SELECT x FROM tab WHERE x = 'key' AND y < 42;
しかし、以下の問い合わせはインデックスオンリースキャンを使用できません。
SELECT x, z FROM tab WHERE x = 'key'; SELECT x FROM tab WHERE x = 'key' AND z < 42;
(以下で説明するように、式によるインデックスや部分インデックスは、この規則を複雑にします。)
この2つの基本的な要件が満たされるなら、問い合わせで要求されるすべてのデータ値はインデックスから利用できるので、インデックスオンリースキャンが物理的に可能になります。 しかし、PostgreSQLのすべてのテーブルスキャンにおいて、さらなる必要条件があります。 それは、13章同時実行制御で説明するように、検索された各行が問い合わせのMVCCスナップショットに対して「可視」であることを確認しなければならない、ということです。 可視性の情報はインデックスのエントリには格納されず、ヒープのエントリにのみあります。 そのため、一見すると、すべての行検索はいずれにせよヒープアクセスが必要なように思われます。 そして、テーブルの行が最近に更新された場合は、まさにその通りなのです。 しかし、あまり更新されないデータについてはこの問題を回避する方法があります。 PostgreSQLではテーブルのヒープの各ページについて、そのページに格納されているすべての行が、十分に古く、すべての現在および将来のトランザクションに対して可視であるかどうかを追跡しています。 この情報はテーブルの可視性マップのビットに格納されます。 インデックスオンリースキャンでは、候補となるインデックスのエントリを見つけた後、対応するヒープページの可視性マップのビットを検査します。 それがセットされていれば、行が可視であることがわかるので、それ以上の作業をすることなく、データを返すことができます。 セットされていない場合は、それが可視かどうかを調べるためにヒープエントリにアクセスする必要があり、そのため標準的なインデックススキャンに対するパフォーマンス上の利点はありません。 うまくいく場合であっても、この方法はヒープアクセスと引き換えに可視性マップにアクセスします。 しかし、可視性マップはヒープに比べ、4桁の規模で小さいため、アクセスに必要な物理的I/Oははるかに少ないです。 ほとんどの状況では、可視性マップは常にメモリ内にキャッシュされて残っています。
要するに、2つの基本的条件が満たされていればインデックスオンリースキャンが可能ですが、テーブルのヒープページのかなりの部分に対し、その全可視のビットがセットされている場合にのみ、性能が向上します。 しかし大部分の行が変化しないテーブルは一般的であり、現実にはこの種のスキャンは非常に有効です。
インデックスオンリースキャンの機能を有効に利用するため、先頭の列のみがWHERE
句に適合し、残りの列は問い合わせによって返される「ペイロード」データとなっているインデックスを作成することを選択するかもしれません。
例えば、次のような問い合わせをよく実行する場合を考えます。
SELECT y FROM tab WHERE x = 'key';
このような問い合わせを高速化する伝統的な手法は、x
のみにインデックスを作成することです。
しかし(x, y)
にインデックスを作ることで、この問い合わせをインデックスオンリースキャンとして実現できる可能性が出てきます。
前に説明した通り、そのようなインデックスは大きくなり、x
だけのインデックスよりも高価になるため、このテーブルがほぼ静的であることがわかっている場合にのみ魅力的です。
インデックスを(y, x)
ではなく(x, y)
と宣言するのが重要であることに注意してください。
ほとんどの種類のインデックス(特にBツリー)において、先頭のインデックスによる制約がない検索はあまり効率的ではないからです。
原則として、インデックスオンリースキャンは式によるインデックスでも使うことができます。
例えば、x
がテーブルの列で、f(x)
上にインデックスがある場合、次の問い合わせをインデックスオンリースキャンとして実行できるはずです。
SELECT f(x) FROM tab WHERE f(x) < 1;
そして、関数f()
の計算が高価なら、この方法は非常に魅力的です。
しかしPostgreSQLのプランナは現在のところ、このような場合についてあまり賢くありません。
プランナは、問い合わせで必要となるすべての列がインデックスから利用可能な場合にのみ、その問い合わせが潜在的にインデックスオンリースキャンで実行可能と考えます。
この例では、f(x)
という文脈でしかx
は必要になりませんが、プランナはそのことに気付かないため、インデックスオンリースキャンは不可能であると結論します。
インデックスオンリースキャンは十分に価値があると思われるなら、これはインデックスを(f(x), x)
上に宣言することで回避できます。
ここで2番目の列は現実には使用されないでしょうが、プランナにインデックスオンリースキャンが可能であると確信させるためだけに存在しています。
目的がf(x)
の再計算を避けることの場合、さらなる注意として、プランナはインデックス可能なWHERE
句にないf(x)
の使用を必ずしもインデックス列とマッチしないという事があります。
上記のような単純な問い合わせの場合は通常は正しく処理できるでしょうが、結合を含む問い合わせでは駄目でしょう。
これらの欠点はPostgreSQLの将来のバージョンで解決されるかもしれません。
部分インデックスもインデックスオンリースキャンとの間に興味深い関係があります。 例11.3「一意な部分インデックスの作成」に示す部分インデックスを考えます。
CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target) WHERE success;
原則として、次のような問い合わせに対して、このインデックスを使ったインデックスオンリースキャンが可能です。
SELECT target FROM tests WHERE subject = 'some-subject' AND success;
しかし、WHERE
句で参照されるsuccess
がインデックスの結果列として利用できないという問題があります。
それにも関わらず、インデックスオンリースキャンが可能です。
なぜなら、このプランではWHERE
句のその部分を実行時に再検査する必要がない、つまりインデックス内にあるすべてのエントリは必ずsuccess = true
なので、プラン内でこれを明示的に検査する必要がないからです。
PostgreSQLのバージョン9.6およびそれ以降ではこのような場合を認識し、インデックスオンリースキャンを生成可能ですが、それより古いバージョンではできません。