【テクニカル・上級編】 B-treeインデックス – PostgreSQL

B-treeの深淵:PostgreSQLのインデックスが「当たり前」に動く理由

PostgreSQLの `CREATE INDEX` を叩くとき、あなたはそこに何を見ているだろうか。単なるパフォーマンス向上のための魔法の杖だろうか。

もちろん、それも間違いではない。だが、PostgreSQLを長年使い込んでいるエンジニアなら、裏側で繰り広げられている平衡木の静かな戦いに思いを馳せたことがあるはずだ。今回は、PostgreSQLの屋台骨であるB-treeインデックスについて、マニュアルの行間にある「現場のリアリティ」を深掘りしてみよう。

—

なぜ今さらB-treeなのか

PostgreSQLのB-treeは、正確には「Lehman & Yao」の論文に基づくアルゴリズムをベースにした、非常に洗練された実装だ。特筆すべきは、「左から右への順次アクセス」と「同時実行制御」の両立にある。

一般的なB-treeは、構造変更(分割や結合)のたびにツリー全体をロックしたくなる衝動に駆られるが、PostgreSQLは違う。各ページに「ハイキー(High Key)」と「右兄弟へのリンクポインタ」を持たせることで、ロックの粒度を極限まで小さくしている。つまり、誰かがページ分割をしている最中でも、読み取りクエリは隣のページへ迷いなく突き進めるわけだ。この設計の美しさが、高負荷な環境下でもPostgreSQLが音を上げない理由の一つだ。

「インデックスが効かない」のその先へ

現場でよく遭遇する「インデックスがあるのに遅い」という現象。これの多くは、ただの「カーディナリティ不足」で片付けられない。我々が注目すべきは、インデックスの「肥大化(Bloat)」と「断片化」だ。

  • HOT (Heap Only Tuple) の恩恵と限界: 更新頻度の高いテーブルでは、HOT更新が効いている間はインデックスの更新コストは最小化される。しかし、インデックス列自体を更新した瞬間にその恩恵は消え、古いエントリはゴミとして残る。これが積み重なると、インデックススキャンは本来不要なページまで読み込む羽目になり、物理I/Oの無駄打ちが始まる。
  • インデックス・オンリー・スキャンへの執着: 私は常に、クエリが `Index Only Scan` に収まっているかを `EXPLAIN (ANALYZE, BUFFERS)` で確認する。ここで重要なのは「Heap Fetches」の数だ。もしこれがゼロでなければ、PostgreSQLは可視性マップを信頼できず、わざわざヒープ領域まで覗きに行っている。この微々たるオーバーヘッドが、高トラフィック下では大きな壁になる。

パフォーマンストラブルシューティングの勘所

もしあなたが今、クエリの遅延に頭を抱えているなら、まずは以下の3点を疑ってみてほしい。

1. ページ分割の過多: インデックスに対してランダムな値(UUIDなど)を突っ込んでいないか? 挿入位置がバラバラだと、ページ分割が頻発し、ツリーの密度が下がる。もしUUIDを使うなら、`pgcrypto` の `gen_random_uuid()` ではなく、時系列順に並ぶ工夫を検討すべきだ。
2. 多重インデックスの罠: 「念のため」で貼られた複合インデックスは、書き込み性能の死神だ。特に、更新頻度が高いカラムをインデックスの先頭に持ってくるのは、インデックスの再構築コストを直に食らうことになる。
3. `fillfactor` の再考: デフォルトの 90% が常に正解とは限らない。頻繁に更新されるテーブルのインデックスであれば、あえて `fillfactor` を下げてページ内に空きを作り、ページ分割を抑制する。これはメモリとトレードオフだが、現場での「最後のひと押し」には非常に有効だ。

最後に:データベースは生きている

B-treeは、単なるアルゴリズムの静的な実装ではない。我々の発行するクエリと、アプリケーションのデータライフサイクルに合わせて、彼らもまた呼吸している。

「インデックスを貼る」という行為は、データベースとの対話だ。「このデータはこういう風に検索されるべきだ」という我々の意思を、B-treeという構造体に刻み込む作業に他ならない。

ツールとしてのPostgreSQLを使いこなす段階から、そのアーキテクチャの呼吸を感じる段階へ。そうすれば、インデックス設計という地味な作業も、少しだけ芸術的に見えてくるはずだ。

さあ、次は `pg_stat_user_indexes` を開いて、あなたのデータベースが今、どんな悲鳴を上げているか確認してみよう。そこには必ず、改善のヒントが眠っている。

コメント

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