clickhouse

ClickHouse遇见RoaringBitmap

2020-08-21  本文已影响0人  LittleMagic

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);
    }
}

位图操作函数的定义与实现都位于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.

民那晚安晚安。

上一篇下一篇

猜你喜欢

热点阅读