【実務・中級編】 ビットマップヒープスキャン – PostgreSQL

なぜ「ビットマップヒープスキャン」は現場の救世主なのか?

PostgreSQLのクエリチューニングをしていると、必ずと言っていいほどぶつかるのが「Bitmap Heap Scan」という表示だ。

「インデックスを使っているのに、なぜわざわざビットマップなんて経由するんだ?」
「これってインデックススキャンより遅いんじゃないの?」

昔の僕もそう思っていた。でも、実務で数千万行のデータを扱うようになると、この挙動を理解しているかどうかが、クエリを「爆速」にするか「ゴミ」にするかの分かれ道だと気づくんだ。今日は、この縁の下の力持ちであるビットマップヒープスキャンの正体と、そのパフォーマンスを左右する`work_mem`の秘密について、少し深掘りしてみよう。

—

ビットマップヒープスキャンの「作戦」

まず、インデックススキャンには大きく分けて2つのパターンがある。

1. Index Scan: インデックスを辿って、直接ヒープ(テーブル本体)の特定の行を見に行く。
2. Bitmap Heap Scan: インデックスから「該当しそうな行の場所(ページ)」をリストアップし、メモリ上にビットマップ(0と1の地図)を作る。その地図を元にヒープをまとめて読み込む。

なぜわざわざそんな回りくどいことをするのか? それは「ランダムアクセスのコスト」を減らすためだ。

例えば、あるクエリで「ユーザーID」と「登録日」という2つのインデックスを使った検索をするとしよう。別々のインデックスから得られた検索結果をマージする際、毎回ヒープに飛びに行くと、ディスクI/Oが暴走してパフォーマンスはガタ落ちする。

そこでPostgreSQLはこう考える。「よし、まずはインデックスから該当しそうなページの地図(ビットマップ)を作って、それから効率よくヒープをスキャンしよう」と。これがビットマップヒープスキャンの本質だ。

実践:ビットマップが作られるとき

実際にクエリを見てみよう。こんな感じのクエリがあるとする。

EXPLAIN ANALYZE
SELECT FROM orders
WHERE user_id = 12345 AND status = ‘pending’;

もし`user_id`と`status`に個別のインデックスが張られていたら、PostgreSQLはそれぞれのインデックスをスキャンしてビットマップを作り、メモリ上で`AND`演算をしてからヒープへアクセスする。

ここでのポイントは、「ビットマップはメモリに乗る必要がある」ということだ。

鍵を握るパラメータ:`work_mem`

ここでエンジニアとして意識すべきパラメータが`work_mem`だ。

ビットマップは、検索対象の行数が増えれば増えるほど巨大化する。もしビットマップがメモリ(`work_mem`)に入りきらなくなったらどうなるか? PostgreSQLは、ビットマップを「損失あり(Lossy)」モードに切り替える。

  • 完全なビットマップ: 「この行は条件に合致する」と1ビット単位で正確に把握。
  • 損失あり(Lossy)ビットマップ: 「このページの中に条件に合う行が含まれている可能性がある」というページ単位の管理に格下げ。

こうなると、ヒープを読み込んだ後に、さらに「本当に条件に合致するか?」を個別に確認するフィルタリング処理(Recheck Cond)が発生する。これがクエリを重くする原因の一つなんだ。

もし `EXPLAIN ANALYZE` の結果に `Lossy Bitmap Blocks: XXXXX` と表示されていたら、「メモリが足りていないよ」というデータベースからの悲鳴だと思っていい。

チューニングの心得

現場で意識すべきは、以下の3点だ。

1. `work_mem`の調整:
`EXPLAIN ANALYZE`を見て、もし頻繁に `Lossy` が発生しているなら、そのセッション、あるいは全体で `work_mem` を少し増やしてみる価値はある。ただし、コネクション数に応じてメモリ消費が増えるので、サーバーの空きメモリと相談しながら慎重にね。
2. 複合インデックスの検討:
結局のところ、ビットマップ生成に頼りすぎるのは「インデックスの使い方が最適ではない」ことの裏返しでもある。もし特定の組み合わせで頻繁に検索するなら、ビットマップを介さず一発で絞り込める「複合インデックス」を作る方が、CPU負荷もメモリ負荷も圧倒的に低くなる。
3. 統計情報の更新:
`ANALYZE`をサボると、オプティマイザは間違った見積もりをして、不必要なビットマップスキャンを選んだりする。基本中の基本だけど、ここが崩れていると何をしても無駄になる。

—

最後に

ビットマップヒープスキャンは、PostgreSQLが過酷な環境で生き残るための「工夫」だ。決して悪者じゃない。ただ、その性質を知らずに放置すると、システムが急に重くなる爆弾にもなり得る。

「なぜインデックスが効かないんだろう?」と悩んだら、まずは `EXPLAIN` を叩いて、ビットマップがどう振る舞っているか見てみてほしい。データベースと対話する楽しさが、少しでも伝われば嬉しいな。

もし現場で「このクエリ、ビットマップが大きすぎて困ってるんだよね」という壁にぶつかったら、またいつでも相談してくれ。一緒にコードを読み解こう。

コメント

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