What Is a Bitmap Index?
A bitmap index uses a bit array per distinct value to speed up queries on low-cardinality columns, and combines multiple filters with fast bitwise AND/OR.
A bitmap index is a database index that represents each distinct value in a column as a bit array — one bit per row, set to 1 if that row holds the value and 0 otherwise. Instead of storing pointers to matching rows the way a B-tree does, a bitmap index stores presence as bits, which makes combining several filter conditions extremely fast: the database just runs bitwise AND, OR, or NOT across the arrays.
How it’s structured
Picture a status column on a million-row orders table with only three possible values: pending, shipped, and delivered. A bitmap index on that column builds one bit array per value, each a million bits long:
row: 1 2 3 4 5 6 ...
pending: 1 0 0 1 0 0
shipped: 0 1 0 0 0 1
delivered: 0 0 1 0 1 0
A query for WHERE status = 'shipped' just reads the shipped bit array and returns the rows where the bit is 1 — no tree traversal, no pointer chasing. A query for WHERE status = 'shipped' AND region = 'west' — assuming a bitmap index also exists on region — computes the bitwise AND of the two bit arrays and gets the matching rows directly from the resulting bitmap, with no need to intersect row-ID lists the way a B-tree-based query plan typically would.
This is the property that makes bitmap indexes distinct from every other common index type: they’re built for combining multiple filter conditions cheaply, not for optimizing any single one.
Why cardinality matters so much
Bitmap indexes are extremely efficient when a column has a small number of distinct values relative to the row count — low cardinality. A status column with 3–10 possible values, a boolean flag, or a region column with a couple dozen options are ideal candidates: the bit arrays stay compact and dense, and bitwise operations over them are cheap.
The same design falls apart for high-cardinality columns. An index on email or order_id, where nearly every row has a distinct value, would need one bit array per distinct value — millions of nearly-empty arrays, each a million bits long with a single 1 in it. That’s enormously wasteful compared to a B-tree, which handles high-cardinality, highly selective lookups efficiently by design. This is the opposite of how B-tree indexes behave: B-trees get less useful as cardinality drops toward a handful of values, since the database ends up scanning a large fraction of the table anyway once a value matches millions of rows.
| Bitmap index | B-tree index | |
|---|---|---|
| Best for | Low-cardinality columns (few distinct values) | High-cardinality columns (many distinct values) |
| Combining filters | Fast bitwise AND/OR across columns | Typically one index used per query, or index intersection |
| Storage as cardinality rises | Grows poorly — one bitmap per value | Scales well |
| Concurrent write behavior | Poor — a single row update can touch a whole bitmap | Good — updates are localized |
| Common in | Data warehouses, read-heavy analytics | OLTP systems, general-purpose querying |
Why writes are the trade-off
The same structure that makes bitmap indexes fast for read-heavy filtering makes them expensive to maintain under write-heavy workloads. Updating a single row’s indexed column means flipping bits in more than one bitmap — clearing the old value’s bit, setting the new value’s bit — and depending on the implementation, that can require locking a wider chunk of the bitmap than a B-tree would ever need to lock for the equivalent row update. This is why bitmap indexes are common in analytical, OLAP-style systems and data warehouses, where data is loaded in batches and queried heavily afterward, and rare in OLTP systems with constant row-by-row inserts and updates.
Where you’ll actually encounter them
Not every database offers bitmap indexes as a first-class, persisted index type. Oracle has supported them natively for decades and they’re a standard recommendation there for low-cardinality warehouse columns. PostgreSQL doesn’t let you create a bitmap index directly — instead, its query planner can build a bitmap heap scan on the fly at query time, combining several regular indexes (typically B-trees) into a temporary in-memory bitmap when a query filters on multiple columns, which gets much of the same benefit without a dedicated on-disk structure. Many columnar analytical databases use bitmap-style encoding internally as part of how they store and compress low-cardinality columns in the first place.
The takeaway
A bitmap index trades the general-purpose flexibility of a B-tree for a specific strength: cheap bitwise combination of filters over columns with only a handful of distinct values. That makes it a strong fit for status flags, categories, and other low-cardinality columns in read-heavy analytical workloads, and a poor fit for anything with high cardinality or frequent writes — which is exactly the profile a B-tree handles well instead.
Tagged
Keep reading
The Lycoris Team · · 5 min read Clustered vs Non-Clustered Index Explained
A clustered index determines the physical row order on disk; a non-clustered index is a separate lookup structure. How they differ and when to use each.
The Lycoris Team · · 4 min read The N+1 Query Problem: What It Is and How to Fix It
The N+1 query problem fires one query per row instead of one query total, quietly turning a fast page into hundreds of round trips. How to spot and fix it.
The Lycoris Team · · 5 min read Hash Join vs Nested Loop Join vs Merge Join
Hash joins, nested loop joins, and merge joins are the three ways a database executes a JOIN — here's when the query planner picks each one.