PostgreSQLには、クラッシュから完全に回復可能なディスク上の永続的なハッシュインデックスの実装が含まれています。 明確な線形順序付けを持たないものも含め、すべてのデータ型がハッシュインデックスでインデックス可能です。 ハッシュインデックスは、インデックスされる値のハッシュ値のみを保存するので、インデックス対象の列のサイズは制限となりません。
ハッシュインデックスは単一列のインデックスのみをサポートし、唯一性のチェックはできません。
ハッシュインデックスは=
演算子のみをサポートしており、範囲演算を指定するWHERE句はハッシュインデックスの恩恵をこうむることができません。
各インデックスタプルは単なる4バイトのハッシュ値で、実際の列の値ではありません。 そのため、UUIDやURLのような大きなデータをインデックスすると、ハッシュインデックスはB-treeよりもずっと小さくなるかも知れません。 また、列値が含まれていないため、すべてのハッシュスキャンは損失がある(lossy)ものになります。 ハッシュインデックスはビットマップインデックススキャンおよび後方スキャンに参加できます。
ハッシュインデックスは、大きなテーブルに対して同値スキャンを使用するSELECTとUPDATEを多用するワークロードに対して最適です。 B-treeインデックスでは、スキャンはリーフページが見つかるまで木を降下しなければなりません。 何百万行のテーブルではこの降下スキャンによりデータをアクセスする時間がかかることがあります。 ハッシュインデックスにおけるリーフページに相当するものはバケットページと呼ばれます。 対照的に、ハッシュインデックスはバケットページを直接アクセスすることが可能で、大きなテーブルでのインデックスアクセスの時間を短縮できる可能性があります。 「論理的なI/O」における時間短縮は、共有バッファ/RAMよりもインデックス/データが大きな時にはより顕著になります。
ハッシュインデックスはハッシュ値の均等ではない分布を想定して設計されています。 バケットページへのアクセスはハッシュ値が均一に分布している時にうまく働きます。 挿入によりバケットページが満杯になると、追加のオーバーフローページが特定のバケットページに連結され、そのハッシュ値に適合するインデックスタプル用の領域を局所的に拡張します。 問い合わせ中にハッシュバケットをスキャンする際は、すべてのオーバーフローページをスキャンする必要があります。 そのため、不均等なハッシュインデックスは、いくつかのデータに対してアクセスが必要なブロックの数の点で、Bツリーよりも悪いかも知れません。
オーバーフローが発生する場合を考慮すると、ハッシュインデックスは一意か、ほぼ一意に近いデータあるいは、それぞれのハッシュバケットに少数の行があるデータがもっとも適していると言えます。 問題を回避できる方法の1つは部分インデックス条件を使って極端に一意ではない値を排除することですが、多くの場合にこれが適しているとは言えないかも知れません。
Bツリーのように、ハッシュインデックスは単純なインデックスタプルの削除を行います。 これは削除しても安全であると分かる(アイテム識別子のLP_DEADビットがすでにセットされている)インデックスタプルを削除する遅延メンテナンス操作です。 挿入の際にページに領域が確保できない場合は、不要インデックスタプルを削除することによって、新しいオーバーフローページの作成を回避しようとします。 その時点でそのページにピンがある場合は削除することはできません。 不要インデックスポインタの削除もVACUUM中に発生します。
可能ならば、VACUUMはインデックスタプルをできるだけ少ないオーバーフローページに押し込むことも試み、オーバーフローの連結を最小限に抑えます。 あるオーバーフローページが空になったらそのオーバーフローページは再利用できますが、オペレーティングシステムに返却することはありません。 今の所、REINDEXで再構築する以外にハッシュインデックスを縮小するようにする予定はありません。 バケット数を少なくする予定もありません。
ハッシュインデックスはインデックスされた行数が増えるとバケットページ数も拡張します。 ハッシュキーからバケット番号へのマッピングは、インデックスが徐々に拡張できるように選択されます。 新しいバケットがインデックスに追加されることになったら、存在しているバケットの厳密に一つが「分割」される必要があります。 更新されたハッシュキーからバケット番号へのマッピングにしたがって、タプルが新しいバケットに転送されます。
その拡張はフォアグラウンドで行われるので、ユーザが挿入を実行するのにかかる時間を増加させるでしょう。 ですから、ハッシュインデックスは行数が急激に拡張するテーブルには適していないかもしれません。
ハッシュインデックスには4種類のページがあります。 静的に確保された制御情報を持つメタページ(ページ0)、主バケットページ、オーバーフローページ、解放されて再利用が可能なオーバーフローページを追跡するビットマップページ、です。 アドレッシング目的という点では、ビットマップページはオーバーフローページのサブセットと見なされます。
インデックスのスキャンおよびタプルの挿入は両者とも、与えられたタプルが置かれるべきバケットを特定する必要があります。 これを実施するためには、メタページ内のバケット数、上位マスク、下位マスクが必要です。 しかし、性能上の理由で、そのような操作を行うたびにメタページをロックしてピンを立てるのは好ましいことではありません。 そうする代わりに、それぞれのバックエンドのリレーションキャッシュ(relcache)のエントリにキャッシュされたメタページの複製を保持します。 最後にキャッシュが更新されてから目的のバケットが分割されていない限り、これは正しいバケットのマッピングを生成します。
特定のインデックスにおいて、バケット数に対して必要なオーバーフローページは増減する可能性があるため、主バケットページとオーバーフローページは独立して確保されます。 ハッシュコードは可変数のオーバーフローページをサポートするために一連の興味深いアドレッシング規則を使用しており、主バケットページを作成後に移動する必要もありません。
インデックス付されたテーブル内の各行はハッシュインデックスにおいては単一のインデックスタプルで表現されています。 ハッシュインデックスタプルはバケットページに格納され、オーバーフローページが存在するならそこにも存在します。 一つのインデックスページにインデックスエントリをハッシュコードによりソートされた状態で保持することで、インデックスページ内での二分探索を可能とし、探索を高速化しています。 しかし、バケット内の異なるインデックスページ間においては、ハッシュコードの相対的な順序付けに何も前提はないことに留意してください。
ハッシュインデックスを拡張するためにバケットを分割するアルゴリズムは複雑過ぎてここで言及するには及びませんが、より詳細がsrc/backend/access/hash/README
に記載されています。
分割アルゴリズムはクラッシュ耐性があり、正常に完了していなくても再スタートできます。