なぜ「ビットマップヒープスキャン」は現場の救世主なのか?
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` を叩いて、ビットマップがどう振る舞っているか見てみてほしい。データベースと対話する楽しさが、少しでも伝われば嬉しいな。
もし現場で「このクエリ、ビットマップが大きすぎて困ってるんだよね」という壁にぶつかったら、またいつでも相談してくれ。一緒にコードを読み解こう。
コメント