美文网首页clickhouse
ClickHouse遇见RoaringBitmap

ClickHouse遇见RoaringBitmap

作者: LittleMagic | 来源:发表于2020-08-21 22:42 被阅读0次

    Q&A

    Q:如图。

    A:当然是自带的。其实RoaringBitmap正是ClickHouse位图的底层实现(笑

    RoaringBitmap的预备知识请见这里

    在CH中产生位图

    使用普通函数bitmapBuild可以由无符号整形数的数组直接产生位图,e.g.

    WITH bitmapBuild([32, 65, 127, 1026]) AS bm
    SELECT bm,toTypeName(bm);
    
    ┌─bm─┬─toTypeName(bm)─────────────────────────┐
    │  A� │ AggregateFunction(groupBitmap, UInt16) │
    └────┴────────────────────────────────────────┘
    

    可见,位图在CH中的本质是AggregateFunction(groupBitmap, UInt*)类型的(*由位图中的最大值弹性决定),即groupBitmap这个聚合函数产生的中间结果。根据聚合函数的combinator语法,加上-Merge后缀再查询一次:

    WITH bitmapBuild([32, 65, 127, 1026]) AS bm
    SELECT bm,groupBitmapMerge(bm);
    
    ┌─bm─┬─groupBitmapMerge(bm)─┐
    │  A� │                    4 │
    └────┴──────────────────────┘
    

    也就是说,groupBitmap函数返回的是位图的基数(即1的数量)。显然,与-Merge后缀相对,我们还可以用-State后缀来对某一列生成位图:

    SELECT groupBitmapState(toUInt64(user_id)) FROM ods.analytics_access_log_local
    WHERE ts_date = today();
    
    -- 这个位图很长,所以显示大量乱码hhh
    

    ClickHouse提供了很多位图操作的函数,参见官方文档,稍后会给出示例。

    翻翻源码

    既然位图都是从groupBitmap函数构建出来的,就来看看与其相关的实现吧。来到AggregateFunctionGroupBitmapData.h,定义如下:

    template <typename T>
    struct AggregateFunctionGroupBitmapData
    {
        bool doneFirst = false;
        RoaringBitmapWithSmallSet<T, 32> rbs;
        static const char * name() { return "groupBitmap"; }
    };
    

    其核心就是RoaringBitmapWithSmallSet这个数据结构,继续看代码:

    template <typename T, UInt8 small_set_size>
    class RoaringBitmapWithSmallSet : private boost::noncopyable
    {
    private:
        using Small = SmallSet<T, small_set_size>;
        using ValueBuffer = std::vector<T>;
        Small small;
        roaring_bitmap_t * rb = nullptr;
    
        void toLarge()
        {
            rb = roaring_bitmap_create();
    
            for (const auto & x : small)
                roaring_bitmap_add(rb, x.getValue());
        }
    
    public:
        // ......
    }
    

    roaring_bitmap_t就是RBM在CRoaring库中的实现,SmallSet又是什么鬼?其实它的定义位于SmallTable.h中,是一个限定大小(无法扩容)的小集合,底层用数组来存储。在前面的结构体里,模板参数small_set_size设为32,说明它最多只能容下32个元素。

    继续向下读一段:

    public:
        bool isLarge() const { return rb != nullptr; }
    
        bool isSmall() const { return rb == nullptr; }
    
        ~RoaringBitmapWithSmallSet()
        {
            if (isLarge())
                roaring_bitmap_free(rb);
        }
    
        void add(T value)
        {
            if (isSmall())
            {
                if (small.find(value) == small.end())
                {
                    if (!small.full())
                        small.insert(value);
                    else
                    {
                        toLarge();
                        roaring_bitmap_add(rb, value);
                    }
                }
            }
            else
                roaring_bitmap_add(rb, value);
        }
    
        UInt64 size() const
        {
            return isSmall()
                ? small.size()
                : roaring_bitmap_get_cardinality(rb);
        }
        // ....
    

    这下就非常明显了:当位图的基数少于32时,仅使用SmallSet存储;一旦超过此阈值,就调用toLarge()方法转化为RoaringBitmap,此后都在RoaringBitmap上操作。之所以有这种区别,想来是因为SmallSet在小数据量下的性能比RBM更优吧。

    当然,在RoaringBitmapWithSmallSet之间进行运算时,就需要分情况讨论了。举个栗子,两个CH位图做按位与运算(对应bitmapAnd函数),对应方法如下:

    void rb_and(const RoaringBitmapWithSmallSet & r1)
    {
        ValueBuffer buffer;
        if (isSmall() && r1.isSmall())
        {
            // intersect
            for (const auto & x : small)
                if (r1.small.find(x.getValue()) != r1.small.end())
                    buffer.push_back(x.getValue());
            // Clear out the original values
            small.clear();
            for (const auto & value : buffer)
                small.insert(value);
            buffer.clear();
        }
        else if (isSmall() && r1.isLarge())
        {
            for (const auto & x : small)
                if (roaring_bitmap_contains(r1.rb, x.getValue()))
                    buffer.push_back(x.getValue());
            // Clear out the original values
            small.clear();
            for (const auto & value : buffer)
                small.insert(value);
            buffer.clear();
        }
        else
        {
            roaring_bitmap_t * rb1 = r1.isSmall() ? r1.getNewRbFromSmall() : r1.getRb();
            roaring_bitmap_and_inplace(rb, rb1);
            if (r1.isSmall())
                roaring_bitmap_free(rb1);
        }
    }
    
    • 当this为小位图时,遍历this并逐个检查其中的元素在r1是否存在(只是根据r1的大小不同调用的方法不同而已),将重合的元素放入缓存,再重新插入this中。
    • 当this为大位图时,直接调用CRoaring自带的roaring_bitmap_and_inplace()方法做与运算。注意如果r1为小位图,需要先调用getNewRbFromSmall()方法生成新的RBM。

    位图操作函数的定义与实现都位于FunctionsBitmap.h内,例如刚才说过的bitmapAnd函数:

    using FunctionBitmapAnd = FunctionBitmap<BitmapAndImpl, NameBitmapAnd>;
    
    template <typename T>
    struct BitmapAndImpl
    {
        static void apply(AggregateFunctionGroupBitmapData<T> & bitmap_data_1, const AggregateFunctionGroupBitmapData<T> & bitmap_data_2)
        {
            bitmap_data_1.rbs.rb_and(bitmap_data_2.rbs);
        }
    };
    

    位图部分的源码在整个ClickHouse体系中算是相当友好的,看官也可自行查看,不再赘述。

    CH位图的应用

    我们知道,位图在需要精确统计基数及快速求交集、并集的场景非常有用。以用户行为(点击流)数据为例,创建以下的表:

    CREATE TABLE tmp.analytics_access_log_bmp (
      ts_date Date,
      event_type LowCardinality(String),
      user_bmp AggregateFunction(groupBitmap, UInt64)
    )
    ENGINE = AggregatingMergeTree()
    PARTITION BY ts_date
    ORDER BY event_type
    SETTINGS index_granularity = 16;
    

    这里是以日期和事件类型作为key,将符合条件的用户ID以位图存储。注意为了配合AggregateFunction,引擎应使用AggregatingMergeTree,并且此表的行数肯定偏少,所以索引粒度可适当降低。

    取一些数据灌入表中:

    INSERT INTO tmp.analytics_access_log_bmp
    SELECT 
      ts_date,event_type,
      groupBitmapState(toUInt64(user_id))
    FROM ods.analytics_access_log_local
    WHERE ts_date >= '2020-08-15' AND ts_date <= '2020-08-20'
    GROUP BY ts_date,event_type;
    

    这样,我们就可以非常快速地统计每天查看过商品详情页的用户数:

    SELECT 
      ts_date,
      bitmapCardinality(user_bmp) AS user_cnt
    FROM tmp.analytics_access_log_bmp
    WHERE event_type = 'OpenGoodsDetail';
    

    以及统计前一日打开过App,后一日点击过“立即购买”的用户转化率:

    SELECT c1 / c0
    FROM (
      SELECT bitmapCardinality(
        (SELECT user_bmp FROM tmp.analytics_access_log_bmp
        WHERE ts_date = '2020-08-17' AND event_type = 'AppStart')
      ) AS c0,
      bitmapCardinality(bitmapAnd(
        (SELECT user_bmp FROM tmp.analytics_access_log_bmp
        WHERE ts_date = '2020-08-17' AND event_type = 'AppStart'),
        (SELECT user_bmp FROM tmp.analytics_access_log_bmp
        WHERE ts_date = '2020-08-18' AND event_type = 'BuyNow')
      )) AS c1
    );
    

    毫无疑问,由于位图操作消灭了GROUP BY、JOIN等复杂的关系代数操作,效率有极为明显的提升。

    The End

    Happy Friday night.

    民那晚安晚安。

    相关文章

      网友评论

        本文标题:ClickHouse遇见RoaringBitmap

        本文链接:https://www.haomeiwen.com/subject/gjtpjktx.html