「性別・ステータス・地域コードなど、取りうる値の種類が少ない列での検索を高速化したい」「データウェアハウスで複数の条件を AND/OR 組み合わせた集計クエリを速くしたい」という場面で検討するのがビットマップインデックス(Bitmap Index)です。
B-Tree インデックスが各行を「キー値 → ROWID」の形式で格納するのに対し、ビットマップインデックスは「各キー値ごとに全行のビットマップ(0/1 の配列)」を格納します。複数条件の AND/OR 演算をビット演算で高速に処理できるため、低カーディナリティ列に特に効果的です。
この記事でわかること
- ビットマップインデックスの内部構造と B-Tree との違い
- CREATE BITMAP INDEX の作成方法
- 低カーディナリティ列(カーディナリティが低い列)での効果と高カーディナリティ列との使い分け
- 複数列ビットマップの AND/OR 演算の仕組み
- DML 時のロック競合問題と OLTP には不向きな理由
- ビットマップジョインインデックスの概念と活用
B-Tree インデックスとビットマップインデックスの違い
| 項目 | B-Tree インデックス | ビットマップインデックス |
|---|---|---|
| 内部構造 | キー値 → ROWID のB-Treeノード | キー値ごとのビットマップ配列(1行=1ビット) |
| 適したカーディナリティ | 高い(ユニークに近い列) | 低い(数〜数百種類の値のみ) |
| ストレージ効率 | 値ごとにエントリが存在 | 同じ値の行が多いほど圧縮効果が高い |
| AND/OR 演算 | 複数インデックスのマージ(比較的重い) | ビット演算で高速(非常に軽い) |
| DML 時のロック | 行レベルロック | ビットマップセグメント単位のロック(他行もブロック) |
| COUNT(*) 集計 | インデックス全体を走査 | ビットの COUNT で高速 |
| 推奨環境 | OLTP(高更新頻度) | DWH・OLAP(読み取り中心) |
CREATE BITMAP INDEX でビットマップインデックスを作成する
ビットマップインデックスの作成
-- 低カーディナリティ列(ステータス列: 取りうる値が数種類)にビットマップインデックスを作成する
CREATE BITMAP INDEX sales_status_bix
ON sales(status)
TABLESPACE idx_tbs;
-- status 列の値が 'PENDING', 'SHIPPED', 'DELIVERED', 'CANCELLED' の 4 種類なら
-- ビットマップが 4 本作成される(各値ごとに全行の 0/1 ビット列)
-- 性別列(M/F)にビットマップインデックス
CREATE BITMAP INDEX customers_gender_bix
ON customers(gender);
-- 地域コード列にビットマップインデックス
CREATE BITMAP INDEX orders_region_bix
ON orders(region_code);
-- 複数列のビットマップインデックス(複合ビットマップ)
CREATE BITMAP INDEX sales_status_region_bix
ON sales(status, region_code);
-- ただし複合ビットマップは値の組み合わせごとにビットマップが作成されるため
-- カーディナリティの積が大きくなると効果が薄れる
-- ビットマップインデックスを確認する
SELECT index_name, index_type, table_name, status
FROM DBA_INDEXES
WHERE table_name = 'SALES' AND owner = 'SALES_DWH';
-- INDEX_TYPE: 'BITMAP'
複数列ビットマップの AND/OR 演算
ビットマップインデックスの最大のメリットは、複数条件の AND/OR をビット演算で高速処理できる点です。例えば「status=’SHIPPED’ AND region_code=’EAST’」という条件は、それぞれのビットマップを AND 演算するだけで対象行を特定できます。
複数ビットマップ条件の AND/OR クエリ例
-- ビットマップインデックスが効果的な複合条件クエリ
-- status ビットマップ AND region_code ビットマップ → 高速
SELECT COUNT(*) FROM sales
WHERE status = 'SHIPPED'
AND region_code = 'EAST'
AND sale_year = 2024;
-- OR 条件も高速(status='SHIPPED' OR status='DELIVERED')
SELECT order_id, amount
FROM sales
WHERE status IN ('SHIPPED', 'DELIVERED')
AND region_code IN ('EAST', 'WEST');
-- NOT 条件もビット反転で高速
SELECT COUNT(*) FROM sales
WHERE status != 'CANCELLED'
AND region_code = 'NORTH';
-- NULL 値もビットマップで管理される
SELECT COUNT(*) FROM sales WHERE status IS NULL;
-- 実行計画でビットマップ演算を確認する
EXPLAIN PLAN FOR
SELECT COUNT(*) FROM sales
WHERE status = 'SHIPPED' AND region_code = 'EAST';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format => 'TYPICAL'));
-- BITMAP CONVERSION TO ROWIDS
-- BITMAP AND
-- BITMAP INDEX SINGLE VALUE (SALES_STATUS_BIX)
-- BITMAP INDEX SINGLE VALUE (ORDERS_REGION_BIX)
-- のような実行計画が表示される
ビットマップインデックスと DML のロック競合
OLTP 環境でビットマップインデックスを使ってはいけない理由
ビットマップインデックスへの DML(INSERT/UPDATE/DELETE)は、変更された行のビット周辺にある複数の行に対してロックをかけます。たとえば status=’PENDING’ の行を 1 行 UPDATE すると、status=’PENDING’ のビットマップセグメント全体がロックされ、同じステータスの他の行への DML がブロックされます。高更新頻度の OLTP テーブルに使うと深刻なロック競合が発生します。
ビットマップインデックスへの DML(INSERT/UPDATE/DELETE)は、変更された行のビット周辺にある複数の行に対してロックをかけます。たとえば status=’PENDING’ の行を 1 行 UPDATE すると、status=’PENDING’ のビットマップセグメント全体がロックされ、同じステータスの他の行への DML がブロックされます。高更新頻度の OLTP テーブルに使うと深刻なロック競合が発生します。
ビットマップインデックスの適切な使用場面の判断基準
-- ビットマップインデックスが適しているか確認するクエリ
-- カーディナリティ(値の種類数)を確認する
SELECT
column_name,
num_distinct, -- 値の種類数(低いほどビットマップ向き)
num_nulls,
density -- 1 / num_distinct に近い値(均等分布)
FROM DBA_TAB_COL_STATISTICS
WHERE table_name = 'SALES' AND owner = 'SALES_DWH'
ORDER BY num_distinct;
-- 一般的な目安:
-- num_distinct が 1000 未満 → ビットマップインデックス候補
-- num_distinct が 100 未満 → ビットマップインデックスが特に効果的
-- num_distinct がテーブル行数の 1% 未満 → ビットマップ向き
-- 更新頻度を確認する(DBA_TAB_MODIFICATIONS がある場合)
SELECT table_name, inserts, updates, deletes, timestamp
FROM DBA_TAB_MODIFICATIONS
WHERE table_name = 'SALES';
-- 更新頻度が高い場合は B-Tree を検討する
ビットマップジョインインデックス
ビットマップジョインインデックスは、JOIN を事前計算してインデックスに格納するDWH特有の機能です。スタースキーマのディメンションテーブルの列をファクトテーブルのインデックスに含めることができます。
ビットマップジョインインデックスの作成(スタースキーマ例)
-- スタースキーマの例
-- SALES(ファクトテーブル)と CUSTOMERS(ディメンション)のジョインを事前計算する
CREATE BITMAP INDEX sales_cust_region_bjix
ON sales(customers.region_code) -- ディメンション側の列
FROM sales, customers -- ジョインするテーブル
WHERE sales.customer_id = customers.customer_id;
-- TABLESPACE idx_tbs;
-- このインデックスにより、以下のクエリでジョインなしに region_code での絞り込みが可能になる
-- SELECT COUNT(*) FROM sales, customers
-- WHERE sales.customer_id = customers.customer_id
-- AND customers.region_code = 'EAST';
-- → BITMAP INDEX SINGLE VALUE (SALES_CUST_REGION_BJIX) がジョイン前に適用される
-- ジョインインデックスの制約:
-- ・ファクトテーブル(最大 1 テーブル)と複数のディメンション
-- ・ファクトテーブルには WHERE 句のみ(GROUP BY / ORDER BY は不可)
-- ・ディメンションテーブルの列は主キーで結合する必要がある
まとめ
- ビットマップインデックス:キー値ごとに全行のビット列を保持する。低カーディナリティ列での AND/OR 複合条件に非常に効果的
- CREATE BITMAP INDEX:B-Tree と同様の構文で BITMAP キーワードを追加するだけ
- OLTP では使わない:DML 時のロックが行単位でなくビットマップセグメント単位のため、高更新頻度のテーブルではロック競合が深刻になる
- DWH・読み取り専用テーブルで効果最大:集計クエリの COUNT / SUM・複合条件の絞り込みが高速になる
- ビットマップジョインインデックス:スタースキーマのJOINを事前計算。ファクトテーブルにディメンション列のビットマップを保持する
B-Tree インデックスの詳細・関数ベースインデックスの使い方は インデックス完全ガイドを参照してください。非表示インデックスでビットマップインデックスの効果をテストする方法は 非表示インデックス完全ガイドも参照してください。