【テクニカル・上級編】 配列型(Array Types)のインデックス – PostgreSQL

PostgreSQLの配列型とGINインデックス:その「魔法」の裏側を覗く

PostgreSQLを使っていると、ふとした瞬間に「あ、ここはRDBの正規化ルールを少し崩してでも、配列型(`ARRAY`)に逃げた方が健全だな」と判断する場面が訪れます。JSONBが注目されがちですが、配列型は型安全でメモリ効率も良く、使いこなせれば強力な武器になります。

ただ、配列型のクエリを書き始めるとすぐに壁にぶつかるはずです。「`WHERE tags @> ARRAY[‘postgres’]`」のようなクエリを投げたとき、フルスキャン(Seq Scan)でCPUが悲鳴を上げているのを見たことはありませんか?

今回は、そんな配列型に対するGINインデックスの裏側と、少し踏み込んだ最適化の話をしようと思います。

—

GINインデックスという「転置インデックス」の正体

配列型に対してインデックスを貼る際、`btree`は役に立ちません。配列の中身をどう比較するか、`btree`には定義できないからです。そこで登場するのがGIN (Generalized Inverted Index) です。

GINの仕組みを一言で言えば「転置インデックス」です。配列内の各要素をキーとして抽出し、そのキーがどの行ID(TID)に含まれているかをリストとして保持します。

CREATE INDEX idx_posts_tags ON posts USING GIN (tags);

この一行を実行した瞬間、PostgreSQLは各配列要素に対して「この値はこの行にある」というマッピングをせっせと構築し始めます。クエリで `@> (包含演算子)` を使うと、PostgreSQLは配列内の各要素をキーとしてインデックスをルックアップし、共通するTIDの交差(BitmapAnd)をとって結果を返します。

ここでのポイントは、「GINはキーの重複を圧縮して保持する」という点です。頻出するタグが含まれる行が多ければ多いほど、インデックスの「エントリリスト」は肥大化し、更新コスト(Write Amplification)が跳ね上がります。

—

包含演算子 `@>` の最適化と落とし穴

`@> ‘{“postgres”, “performance”}’` のようなクエリにおいて、GINインデックスがどのように動くか。実は、インデックスの構築時に配列のすべての要素をインデックス化するのか、あるいは特定の演算子に特化させるのかは、運用の勘所です。

1. 頻出値のインデックス除外

もし「タグ」の中に、全データの90%に含まれるような「ゴミ値(例:’general’など)」がある場合、インデックスは肥大化するだけで検索性能を落とします。こういう時は、`gin_pending_list_limit` を調整するか、あるいはアプリケーション側で頻出値を除外した配列を作成するなどの泥臭い工夫が必要です。

2. マルチカラム・インデックスの罠

たまに見かけるのが、`GIN(tags, category_id)` のようなインデックスです。GINはB-treeのように「先頭カラムから順番に」という制約がないため、複合インデックスを貼る際は慎重になる必要があります。基本的にGINでの複合インデックスは、クエリのパターンが完全に固定されている場合を除き、インデックスサイズを徒に大きくするリスクの方が高いです。

—

多次元配列というパンドラの箱

PostgreSQLは `int[][]` のような多次元配列もサポートしています。しかし、ここで一つ注意が必要です。

GINインデックスは、多次元配列を「フラットな値の集合」として扱います。 つまり、`{{1,2},{3,4}}` という配列に対してインデックスを貼ると、`1, 2, 3, 4` がそれぞれ独立したエントリとして抽出されます。「`{1,2}` というサブ配列が含まれているか」という検索をしたい場合、単純なGINでは力不足です。

多次元配列を使うケースは、多くの場合「行列計算」や「座標データ」だと思いますが、そのような要件であれば、素直に `cube` 拡張や `postgis` のジオメトリ型を検討することをお勧めします。配列型の限界を無理やり突破しようとすると、後でメンテナンスコストという名の借金を返す羽目になりますから。

—

トラブルシューティングの心得

もし皆さんの環境で「配列の検索が遅い」というアラートが鳴ったら、まずは `EXPLAIN (ANALYZE, BUFFERS)` を見てください。

  • Heap Fetches が多発していないか?

GINインデックスの「pending list(未処理リスト)」が溜まりすぎると、検索のたびにリスト全体をスキャンするような挙動になり、性能が劣化します。`gin_clean_pending_list()` を手動で叩くか、`fastupdate = off` にして構築を待つ判断も必要です。

  • インデックスのサイズは適正か?

`pg_size_pretty(pg_relation_size(‘idx_posts_tags’))` でサイズを確認してください。テーブル本体よりもインデックスの方が大きくなっているようなら、それは「設計の敗北」のサインです。

—

最後に

データベースエンジニアとして、私はいつも「配列型は強力なスパイスである」と自分に言い聞かせています。料理の味を引き立てるには最高ですが、そればかりを食べていては身体を壊します。

JSONBにするべきか、正規化して別テーブルに切り出すべきか、それとも配列型で効率的に処理すべきか。その境界線を見極める嗅覚こそが、熟練のエンジニアを分かつポイントなのだと思います。

皆さんのPostgreSQL運用が、少しでも快適なものになりますように。それでは、また。

コメント

タイトルとURLをコピーしました