Blog

Small Bitmap, Big Gains: Optimizing Large-Scale Bitmap Aggregation in Databend

avatarKKouldJul 6, 2026
Small Bitmap, Big Gains: Optimizing Large-Scale Bitmap Aggregation in Databend

Bitmap functions are commonly used in SQL databases to represent sets efficiently and accelerate set operations. They are especially useful for high-dimensional filtering and exact deduplication at large scale. Databend already supports a broad set of bitmap functions. For usage details, see the Databend bitmap function documentation. ` In distributed aggregation, a typical bitmap workflow looks like this: read many small bitmaps from many rows, aggregate them layer by layer, and eventually produce a larger bitmap that contains the final union, intersection, or other set result.

For example:

SELECT bitmap_intersect(user_tags)
FROM events
WHERE dt = '...';

Before this optimization, the execution path for this type of query could be simplified as:

row bytes -> deserialize RoaringTreemap -> aggregate state operate -> result

There are two obvious cost centers in this path:

  • Bitmaps are usually stored as bytes. When a query touches many rows, deserializing every row into a full bitmap structure amplifies read and construction cost by row count. For bitmaps with only a few elements, it is better to compute directly on bytes or on a lightweight representation whenever possible.

  • When two bitmaps are combined, the engine should reuse existing structures as much as possible. Rebuilding bitmap objects on every operation creates extra allocations and temporary objects.

Databend Workloads That Benefit

This optimization is especially useful for the following Databend workloads:

  • User segmentation:

    bitmap_intersect
    /
    bitmap_union
    across different user tag groups.

  • Behavioral analytics: computing sets for visits, clicks, purchases, and other events.

  • Funnel analysis: intersecting user sets across funnel steps.

  • Permission or resource set checks: quickly testing set containment or overlap.

  • Large-scale low-cardinality aggregation: many rows contain small bitmaps that are eventually merged into one result.

These workloads share the same shape: the number of input rows is large, but each row-level bitmap is small. If every row is expanded into a full

RoaringTreemap
, query cost grows with the number of rows rather than the actual size of the sets.

The purpose of

HybridBitmap
is to keep small bitmaps in a lightweight representation for as long as possible, and switch to
RoaringTreemap
only when the result truly becomes large.

HybridBitmap: One Representation for Small and Large Sets

Bitmap functions are mostly used in aggregation, where data often follows this pattern:

many small row-level bitmaps -> gradually larger aggregate result

Databend therefore represents bitmaps with two structures:

  • SmallBitmap
    : represents small sets with
    smallvec
    , with low construction cost.

  • RoaringTreemap
    : represents large sets with Roaring Bitmap, which is well suited for compressed high-cardinality sets and set operations.

The state transition is straightforward:

SmallBitmap -> threshold exceeded -> RoaringTreemap

SmallBitmap: Lower Construction Cost for Small Sets

SmallBitmap
uses a specialized vector representation. When the number of elements is small, it uses stack-backed storage by default. Once the number of elements exceeds a threshold, it switches to heap-backed dynamic storage.

When modified,

SmallBitmap
uses binary search to keep elements sorted and unique, preserving the same set semantics as a regular bitmap.

Because

SmallBitmap
primarily relies on stack-backed storage for small inputs, it can deserialize and construct row-level bitmaps with very low overhead. This improves scan efficiency when a query needs to read many small bitmaps.

In some operations such as

xor
, intermediate results may also shrink. In those cases, Databend can try to convert a large bitmap back into a small bitmap and reduce later materialization cost.

RoaringTreemap: Compression and Fast Operations for Large Sets

RoaringTreemap
is based on the Roaring Bitmap algorithm, which is widely used in high-performance systems such as Spark, Hive, and Lucene. For more details, see the Roaring Bitmap paper: https://arxiv.org/pdf/1603.06549.

Compressed bitmaps matter in database systems. Without compression, large row-level bitmaps can introduce heavy read and compute overhead.

There are other mature compressed bitmap formats, such as Oracle BBC, WAH, and EWAH. However, these formats do not support efficient random access. Computing an intersection may require fully decompressing a large bitmap.

Roaring Bitmap takes a different approach: it partitions data into multiple containers. This makes it fast to check whether a value exists, while still providing good compression ratio and set operation performance in many workloads.

The trade-off is that, compared with

SmallBitmap
,
RoaringTreemap
still has relatively high construction cost for low-cardinality sets.

With this hybrid representation, Databend can choose the right structure at different stages of computation. For example, in a

bitmap_intersect
workload where most row-level bitmaps are small:

  • When reading row-level bitmaps, Databend keeps bytes in a small bitmap representation whenever possible.

  • During result computation, Databend switches to

    RoaringTreemap
    only if the result grows.

  • When one side is a

    RoaringTreemap
    and the other side is a serialized small bitmap, Databend tries to compute the intersection directly from the small bitmap bytes instead of fully deserializing every row into
    RoaringTreemap
    .

This reduces the read and construction cost of many small bitmaps, while preserving the performance benefits of

RoaringTreemap
for large sets.

Operation Matrix: Choosing the Right Computation Path

HybridBitmap
is not a replacement for
RoaringTreemap
. Its goal is to choose different computation paths for different data sizes, and avoid paying the full
RoaringTreemap
construction cost too early.

Left stateRight stateStrategy
SmallSmallPerform set operations directly on sorted small vectors, avoiding
RoaringTreemap
construction.
SmallLargePromote to Large only when the operation requires it. For example,
union/xor
may grow the set, while
intersect/sub
can often stay small.
LargeSmallIterate over the small set and apply local operations on the Large bitmap, such as
contains
or
insert
or
remove
LargeLargeUse native
RoaringTreemap
set operations.
Largeserialized smallCompute directly against serialized bytes when possible, reducing the cost of deserializing each small bitmap into a full
RoaringTreemap

In aggregation workloads, inputs are usually read from columns as bytes. If every small row-level bitmap is first deserialized into a full

RoaringTreemap
, the actual cost can be much higher than the set size suggests.

By separating the

small
,
large
, and
serialized small
paths, Databend keeps small bitmaps in a low-cost representation and switches to
RoaringTreemap
only when the result needs it.

Benchmark: Faster Small-Bitmap Aggregation

Because benchmark coverage, function naming, and later implementation details differ before and after PR 19041, this section only shows small-bitmap aggregation paths that can be reproduced and aligned between the two versions.

The

bitmap_intersect
numbers come from the PR 19041 summary. The
bitmap_union
,
bitmap_intersect_empty
, and
bitmap_union_disjoint
numbers were locally measured before and after PR 19041 using the same optimization approach.

BenchmarkMedian before PR 19041Median after PR 19041Improvement
bitmap_intersect/100000
15.27 ms4.729 ms69.0%
bitmap_intersect/10000000
1.609 s533.1 ms66.9%
bitmap_union/100000
19.67 ms7.628 ms61.2%
bitmap_union/10000000
2.009 s831.9 ms58.6%
bitmap_intersect_empty/100000
11.81 ms4.197 ms64.5%
bitmap_intersect_empty/1000000
122.9 ms41.56 ms66.2%
bitmap_union_disjoint/100000
36.61 ms7.236 ms80.2%
bitmap_union_disjoint/1000000
345.5 ms84.98 ms75.4%

These results cover several common paths: small bitmap intersection, union, fast convergence to an empty result, and disjoint union where the result keeps growing. The biggest gains appear when a large number of small bitmaps participate in aggregation.

Before the optimization, every row-level bitmap had to be deserialized into a full

RoaringTreemap
. Even if a row contained only a few elements, it still paid the construction cost of a heavier structure.

After the optimization,

SmallBitmap
can represent those inputs directly with a low-cost structure and reduce the number of full
RoaringTreemap
objects created during computation. This significantly lowers CPU overhead and memory allocation.

When This Optimization Helps Most

These benchmarks focus on aggregation over many small bitmaps. They do not imply the same speedup for every bitmap workload.

When input bitmaps or intermediate results quickly grow into large bitmaps, the bottleneck shifts back to large-set operations inside

RoaringTreemap
.

In other words,

HybridBitmap
is not intended to speed up every bitmap computation equally. It works best when the input contains many small sets and the aggregate result changes gradually.

Conclusion: Delay the Cost of Large Bitmaps

The bottleneck in bitmap aggregation does not always come from the set operation itself. Often, it comes from constructing a full large-set data structure too early for a very small set.

When many row-level small bitmaps participate in aggregation, deserialization, object construction, and memory allocation are all multiplied by row count. That can become a major part of query execution cost.

The core value of

HybridBitmap
is to delay the cost of
RoaringTreemap
:

  • Use

    SmallBitmap
    while the bitmap is still small, keeping construction and allocation overhead low.

  • Switch to

    RoaringTreemap
    only when the set actually grows, and then rely on its compression and large-set operation performance.

This optimization does not replace Roaring Bitmap. It avoids using Roaring Bitmap before it is needed.

The benchmark results show that the most visible gains come from aggregating many small bitmaps, such as

bitmap_intersect
and
bitmap_union
. When inputs or intermediate results quickly become large, the bottleneck naturally returns to
RoaringTreemap
operations and the improvement becomes less pronounced.

This makes

HybridBitmap
a layered model of the bitmap lifecycle: small sets stay in a lightweight structure, while high-cardinality sets are handed over to compressed bitmap processing.

For Databend, this optimization is not only about making a few bitmap functions faster.

In a cloud data warehouse, many query costs come from repeatedly constructing, deserializing, and allocating small objects.

HybridBitmap
reflects Databend's broader optimization direction: keep execution paths close to real workloads, reduce unnecessary data structure conversions, and improve cost-performance without sacrificing generality.

For users, the result is lower CPU usage, fewer memory allocations, more stable aggregation performance, and better cost-performance in user profiling, tag analytics, funnel analysis, and similar workloads.

Share this post

Subscribe to our newsletter

Stay informed on feature releases, product roadmap, support, and cloud offerings!