「インデックスを貼れば速くなる」……エンジニアの駆け出しの頃は、僕もそう信じて疑わなかったよ。でも、テーブルが数億行、数十億行と膨れ上がったとき、B-treeインデックスの限界にぶち当たって冷や汗をかいたことはないかな?
「インデックスだけでメモリを数ギガ食いつぶす」「インデックスの更新負荷で書き込みが詰まる」。こんな悲劇を回避する切り札として、今回はBRIN (Block Range Index)という、玄人好みの面白い選択肢を紹介したいと思う。
—
巨大データとの付き合い方を変える「BRIN」
PostgreSQLの標準であるB-treeインデックスは、データ一つひとつに対してポインタを保持するから、どうしてもサイズが肥大化しがちだ。
一方でBRINは、「ブロック範囲の最小値と最大値」だけを覚えておく、いわば「ざっくりとした地図」のようなものなんだ。
なぜBRINが最強になり得るのか
BRINの凄さは、その圧倒的な「軽さ」にある。
例えば、100万行のデータがあったとして、B-treeならインデックスサイズが何百MBにもなるようなケースでも、BRINなら数KB〜数十MBで収まってしまうことがある。
ただし、注意が必要だ。BRINは「物理的な格納順序」と「検索キー」に強い相関がある場合にしか真価を発揮しない。例えば、`created_at`(作成日時)で順次INSERTされているログテーブルのようなものだね。
—
実践:BRINを試してみよう
まずは、数千万行のログテーブルを想定して、インデックスを作ってみよう。
— ログテーブルの作成
CREATE TABLE access_logs (
id BIGSERIAL,
created_at TIMESTAMP NOT NULL,
user_id INT,
payload TEXT
);
— 普通ならB-treeを貼るところだけど、今回はBRINを試す
— pages_per_rangeは、何ブロックを1つの範囲として扱うかの設定。
— 小さくすれば精度は上がるけどサイズは増える。デフォルトは128ブロック。
CREATE INDEX idx_brin_created_at ON access_logs USING BRIN (created_at)
WITH (pages_per_range = 32);
このインデックスがどう動くのか
クエリを投げたとき、PostgreSQLはこう考える。
「検索条件が `created_at = ‘2023-10-01’` だな。BRINの地図を見ると、ブロック範囲Aは『2023-09-01 〜 2023-09-30』、ブロック範囲Bは『2023-10-01 〜 2023-10-31』か。じゃあ、範囲Bだけをスキャンすればいいや!」
この「範囲指定」によって、巨大なテーブルでも、読み込むデータ量を劇的に減らせるんだ。
—
実務で「ハマる」ポイント
BRINを導入するなら、以下の3点だけは覚えておいてほしい。
1. 「物理順序」が命
もしデータがバラバラの順序でINSERTされていたら、BRINの「最小値・最大値」の範囲が広がりすぎて、ほとんどのブロックをスキャンすることになる。これじゃあインデックスの意味がないよね。`CLUSTE`コマンドで物理順序を整理するか、時系列データのように最初から順序性があるものに使うのが鉄則だ。
2. 更新が激しいテーブルには不向き
データが後から更新(UPDATE)されると、ブロック内の最小・最大値が変わる可能性があるよね。BRINはこういう動的な変化には弱い。基本は「追記型」のデータに使うものだと割り切ろう。
3. B-treeとの併用は考えない
「とりあえず両方貼っておこう」なんてのは悪手だよ。ディスク容量とメンテナンスコストを無駄にするだけだ。
—
まとめ:いつBRINを使うべきか?
僕が現場でBRINを検討する基準は、シンプルにこれだ。
- テーブルサイズがテラバイト級になりそうで、B-treeの肥大化が不安なとき。
- 検索条件が「日付」や「IDの範囲指定」など、データの物理的順序と相関があるもの。
- B-treeのインデックスメンテナンス(REINDEXや肥大化管理)に限界を感じているとき。
BRINは魔法の杖じゃない。でも、適材適所で使えば、サーバーのスペックを上げることなくクエリを爆速にできる、エンジニアとしての「腕」の見せ所でもあるんだ。
もし今、巨大なログテーブルの重さに悩んでいるなら、一度 `EXPLAIN ANALYZE` を取った上で、BRINを試してみると世界が変わるかもしれないよ。
何か不明な点があれば、またいつでも聞いてくれ。現場からは以上だ!
コメント