「B-treeだけがインデックスじゃない」:巨大データを救うBRINインデックスの極意
やあ。データベースのパフォーマンスチューニングに頭を悩ませているなら、ちょうどいいタイミングだ。
PostgreSQLを使っていると、とりあえず「困ったらB-treeインデックス」という思考になりがちだよな。もちろん、B-treeは万能だ。でも、数億行、あるいは数十億行を超えるような超巨大テーブルを扱っているとき、B-treeの「サイズ」に戦慄したことはないか?
インデックスがメモリに乗らなくなってディスクI/Oが爆発し、更新のたびにインデックスの再構築でサーバーが悲鳴を上げる……。そんな悪夢から君を救ってくれるかもしれない、ちょっとした「飛び道具」の話をしよう。
それがBRIN(Block Range Index)だ。
BRINは「インデックス界の軽量級ボクサー」
B-treeが「どの値がどこにあるか」を詳細なツリー構造で管理するのに対し、BRINは「このブロックの範囲には、最小値と最大値がこれだけ入っていますよ」という情報だけを保持する。
言ってみれば、「辞書の索引」ではなく「図書館の棚のラベル」みたいなものだ。
「この棚には『あ』から『え』までの本が入っている」という情報だけがあれば、ピンポイントの検索はできなくても、範囲外の棚を丸ごと無視できるだろ?
これがBRINの強みだ。
- サイズが圧倒的に小さい: B-treeに比べて数千分の1以下になることも珍しくない。
- メンテナンスコストが低い: データが追加されても、ブロックの範囲情報(最小・最大)を更新するだけだから、インデックスの肥大化がほとんどない。
どんな時に使うべきか?
BRINは魔法の杖じゃない。適材適所がある。
一番刺さるのは、「時系列データ」のような、物理的な格納順序と値の順序が一致しているデータだ。
例えば、`created_at`(作成日時)のようなカラム。データは基本的に追加順に入るから、物理的なブロックの並びと日時は自然と一致する。こういうテーブルにBRINを貼ると、驚くほど効率よくスキャンをスキップしてくれるんだ。
逆に、ランダムなIDや、値と物理順序が全く関係ないカラムにBRINを貼っても、ただのフルスキャンと変わらない(あるいはそれ以下になる)から注意してくれ。
実践:BRINをどう定義するか
使い方はシンプルだ。通常のインデックスとほとんど変わらない。
— 数億件あるログテーブルを想定
CREATE INDEX idx_logs_created_at ON logs USING BRIN (created_at);
これだけでいい。もし「ページ範囲(何ブロックごとに最小・最大を記録するか)」を調整したいなら、オプションを付けることもできる。
CREATE INDEX idx_logs_created_at_pages
ON logs USING BRIN (created_at)
WITH (pages_per_range = 32);
`pages_per_range` を小さくすれば検索精度は上がるけどインデックスサイズは増える。デフォルトの128(つまり約1MB単位)でまずは試して、必要に応じてチューニングするのが現場の定石だね。
気をつけてほしい「落とし穴」
BRINを導入する前に、一つだけ覚えておいてほしいことがある。
それは、「BRINは完璧な絞り込みをしない」ということだ。BRINが返してくるのは「この範囲にデータがある可能性がある」というヒントに過ぎない。だから、インデックスで絞り込んだ後、PostgreSQLは該当するブロックを実際に読み込んで、中身を精査する(Bitmap Heap Scanというやつだ)。
つまり、データが物理的にバラバラに配置されていると、BRINが「この範囲かも」と言ったブロックを全部読みに行くことになり、結局パフォーマンスが死ぬ。
だからこそ、「データの物理的な並び順」を意識すること。
`CLUSTER`コマンドで定期的にデータを物理的に並べ替える運用をしているなら、BRINは最強の武器になるはずだ。
まとめ:道具を使い分けるのが一流の証
B-treeを捨てる必要はない。でも、巨大なログテーブルや履歴テーブルを扱うときは、一度立ち止まって考えてみてほしい。
「これはB-treeである必要があるのか? それとも、BRINで十分なのか?」
そうやってコストと性能のバランスを吟味できるようになると、データベースエンジニアとしての引き出しは一気に広がる。もし君のプロジェクトで「インデックスが重すぎて……」という悲鳴が聞こえたら、ぜひBRINを思い出してくれ。
何か具体的なテーブル設計で迷ったら、いつでも相談に乗るよ。それじゃ、また。
コメント