【実務・中級編】 BRINインデックスの特性とチューニング – PostgreSQL

「インデックスを貼れば速くなる」……エンジニアなら誰もが通る道だよね。でも、PostgreSQLのB-treeインデックスばかりに頼っていると、テラバイト級の巨大なログテーブルを扱うときに痛い目を見る。

今日は、そんな「巨大データとの戦い」において最強の武器になり得るBRIN(Block Range Index)について、現場の視点から深掘りしてみようか。

—

なぜ巨大データでB-treeが「重荷」になるのか

まず、基本の確認ね。B-treeは非常に優秀だけど、データ量に対してインデックスサイズが肥大化しやすい。テーブルが数億行を超えてくると、インデックスだけでメモリを食いつぶし、ディスクI/Oがボトルネックになってクエリが走らなくなる。

そこで登場するのが「BRIN」だ。

BRINの考え方はシンプル。テーブルを「ブロック範囲(デフォルトは128ページ)」という塊で区切って、その「最小値」と「最大値」だけを保持するんだ。
辞書で言えば、ページごとの全単語を載せるのがB-treeなら、BRINは「このページには『あ』から『こ』までが載ってますよ」と、表紙にメモ書きしているようなもの。圧倒的にコンパクトだよね。

BRINの真価は「物理的な順序」に宿る

ただ、BRINには一つだけ、絶対に守らなきゃいけない鉄の掟がある。

それは、「データが物理的にソートされていること」だ。

例えば、`created_at`のような時系列データ。これは自然と物理的に順序が保たれているから、BRINとの相性は最高だ。逆に、ランダムなIDや更新頻度が高いカラムにBRINを貼っても、範囲が広がりすぎて(=最小値が小さく、最大値が大きすぎて)、結局全スキャンと変わらない性能になってしまう。

ここを勘違いして「インデックスサイズが小さいから」という理由だけでテキトーに貼ると、後で泣きを見ることになるから注意してね。

実践:BRINを構築してみる

使い方は簡単だ。例えば、ログテーブルを想定してみよう。

— ログテーブルの作成
CREATE TABLE access_logs (
id BIGSERIAL,
created_at TIMESTAMP NOT NULL,
user_id INT,
payload JSONB
);

— 時系列データなので、BRINインデックスが輝く
CREATE INDEX idx_access_logs_created_at
ON access_logs USING BRIN (created_at);

これだけで、B-treeなら数GBかかるインデックスが、わずか数MB程度に収まることもある。この「軽さ」が、大規模環境では正義になるんだ。

チューニングの秘訣:pages_per_range

BRINを使いこなすなら、`pages_per_range`というオプションを覚えておくといい。

CREATE INDEX idx_access_logs_created_at
ON access_logs USING BRIN (created_at)
WITH (pages_per_range = 32);

デフォルトは128だけど、これを小さくすれば精度が上がる(=検索範囲が狭まる)。逆に大きくすればインデックスサイズはさらに小さくなる。データの密度に合わせて、ここを調整するのがプロの仕事だ。

先輩からのアドバイス:いつ使うべきか?

実務でBRINを検討すべきは、こんなシチュエーションだ。

1. データが数億行を超えていて、B-treeのサイズが物理メモリに収まらないとき
2. 時系列データのような、インサート順とクエリ条件が一致しているカラム
3. 「最近のデータ」を絞り込むクエリがメインのとき

逆に、更新頻度が激しいカラムや、データがバラバラに挿入されるカラムには絶対に使っちゃダメだよ。それなら大人しくB-treeを使うか、BRINの特性を活かせるようにテーブルをパーティショニングする設計を考えるべきだね。

—

最後に

BRINは「魔法の杖」じゃない。でも、適材適所で使えば、PostgreSQLの限界を押し広げてくれる強力なツールになる。

「インデックスはB-tree一択」という思い込みを捨てて、データの性質に合わせたインデックス選びができるようになると、パフォーマンスチューニングの景色がガラッと変わるはず。

もし今のプロジェクトで、「巨大なログテーブルの検索が重い…」と悩んでいるなら、一度`EXPLAIN ANALYZE`を眺めながら、BRINの導入を検討してみてほしい。きっと、その軽快さに驚くはずだよ。

それじゃ、また現場で会おう!

コメント

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