PostgreSQLインデックス:数百万行のデータを処理するならB-Treeだけに頼ってはいけない

Database tutorial - IT technology blog
Database tutorial - IT technology blog

背景:インデックスがもはやデフォルトの「魔法」ではなくなるとき

MySQLで動くEコマースからPostgreSQL上のビッグデータシステムまで、実際のプロジェクトに携わる中で学んだ教訓があります。それは、インデックスこそがパフォーマンスの魂であるということです。

インデックスのない1億行のテーブルを想像してみてください。それは目次のない巨大な図書館で針を探すようなものです. 結果は?CPU使用率は100%に跳ね上がり、ディスクI/Oはボトルネックとなり、アプリケーションはタイムアウトまで読み込み中が続くことになります。

しかし、多くの開発者の最大の過ちは、デフォルトのCREATE INDEXしか知らないことです。PostgreSQLはGIN、GiST、BRINといった専用の「武器庫」を提供しています。不適切なインデックスを選択すると、書き込み処理(INSERT/UPDATE)が遅くなるだけでなく、数十GBものディスク容量を無駄に消費してしまいます。

実装:本番環境での鉄則

PostgreSQLでのインデックス作成は構文こそ簡単ですが、稼働中のテーブルに対して行う場合は非常にリスクが高くなります。

-- システムを「破壊」するインデックス作成方法(テーブルロックが発生)
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

-- 安全なインデックス作成方法(本番環境での推奨)
CREATE INDEX CONCURRENTLY idx_orders_created_at ON orders(created_at);

現場からの注意点: 常にCONCURRENTLYキーワードを使用してください。これにより、ユーザーの注文や決済を妨げることなくインデックスを作成できます。作成時間は少し長くなりますが、少なくともピーク時にアプリを停止させて上司から呼び出されることは避けられます。

分類と応用:正しい武器の選択

各インデックスは、それぞれ特定の課題を解決するために生まれています。適材適所が重要です。

1. B-Treeインデックス:万能な「サバイバルナイフ」

これはデフォルトのタイプです。範囲検索(range)や、<=>といった直接比較に非常に優れています。

使用場面: ID、メールアドレス、日付の検索や、ORDER BYを使用するケースの90%に該当します。データがフラットでシンプルな場合、B-Treeが第一候補となります。

2. GIN (Generalized Inverted Index):JSONBの救世主

JSONBカラムや配列(Array)に柔軟な情報を保存している場合、B-Treeは全く役に立ちません。GINは、そのような複雑なデータ内部の各キー/値に対して深くインデックスを張ります。

実測値: 動的な属性を持つ50万商品を管理するプロジェクトでGINを使用したところ、色やサイズによるフィルタリングクエリが2.8秒からわずか12msに短縮されました。

-- JSONBカラムを強力に最適化
CREATE INDEX idx_products_metadata ON products USING GIN (metadata jsonb_path_ops);

3. BRIN (Block Range Index):ビッグデータの秘策

BRINは、数十億行のログテーブルに対する「重火器」です。各行にインデックスを張る代わりに、各データブロックの最小値と最大値のみを保存します。

具体例: 500GBのログテーブルでB-Treeを使用すると、インデックスファイルが40〜60GBに達することがあります。これをBRINに切り替えると、インデックスサイズは約150MBにまで抑えられ、時間軸での検索速度も非常に高速に保てます。

CREATE INDEX idx_huge_logs_ts ON big_logs USING BRIN (created_at);

4. Hashインデックス:完全一致比較専用

Hashインデックスは=演算子のみをサポートします。PostgreSQL 10以降、安定性が向上し、UUIDのようなユニークな値を持つカラムではB-Treeよりわずかに高速ですが、B-Treeほど一般的ではありません。

検証と最適化:感覚に頼らない

インデックスを作成して終わりではありません。PostgreSQLは、テーブル全体のスキャン(Seq Scan)の方が速いと判断した場合、作成したインデックスを無視することがあります。

EXPLAIN ANALYZEで実行計画を読み解く

以下のコマンドを使用して、SQLの動作を常に詳細に確認してください:

EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';

Index Scanという行が表示されれば、正しい方向に向かっています。数百万行のテーブルでSeq Scanが表示される場合は、データ型を再確認するか、インデックスが正常に作成されているかを確認してください。

不要な「ゴミ」インデックスの整理

インデックスは諸刃の剣です。インデックスが多すぎると、データベースがすべてのインデックスを同時に更新しなければならないため、INSERT処理が極端に遅くなります。以下のスクリプトを実行して、一度も使用されていないインデックスを見つけ、思い切って削除しましょう:

SELECT relname, indexrelname, idx_scan 
FROM pg_stat_user_indexes 
WHERE idx_scan = 0 AND idx_unique = false;

結論として、PostgreSQLの最適化は魔法ではなく、データを深く理解することにあります。テーブルが小さければB-Treeで十分です。データが膨大であればBRINを検討し、JSONBを扱うならGINが必須です。データベースを完全にマスターできることを願っています!

Share: