ClickHouse遇見RoaringBitmap

Q&A

Q:如圖。

A:當(dāng)然是自帶的。其實(shí)RoaringBitmap正是ClickHouse位圖的底層實(shí)現(xiàn)(笑

RoaringBitmap的預(yù)備知識(shí)請(qǐng)見這里

在CH中產(chǎn)生位圖

使用普通函數(shù)bitmapBuild可以由無(wú)符號(hào)整形數(shù)的數(shù)組直接產(chǎn)生位圖,e.g.

WITH bitmapBuild([32, 65, 127, 1026]) AS bm
SELECT bm,toTypeName(bm);

┌─bm─┬─toTypeName(bm)─────────────────────────┐
│  A? │ AggregateFunction(groupBitmap, UInt16) │
└────┴────────────────────────────────────────┘

可見,位圖在CH中的本質(zhì)是AggregateFunction(groupBitmap, UInt*)類型的(*由位圖中的最大值彈性決定),即groupBitmap這個(gè)聚合函數(shù)產(chǎn)生的中間結(jié)果。根據(jù)聚合函數(shù)的combinator語(yǔ)法,加上-Merge后綴再查詢一次:

WITH bitmapBuild([32, 65, 127, 1026]) AS bm
SELECT bm,groupBitmapMerge(bm);

┌─bm─┬─groupBitmapMerge(bm)─┐
│  A? │                    4 │
└────┴──────────────────────┘

也就是說(shuō),groupBitmap函數(shù)返回的是位圖的基數(shù)(即1的數(shù)量)。顯然,與-Merge后綴相對(duì),我們還可以用-State后綴來(lái)對(duì)某一列生成位圖:

SELECT groupBitmapState(toUInt64(user_id)) FROM ods.analytics_access_log_local
WHERE ts_date = today();

-- 這個(gè)位圖很長(zhǎng),所以顯示大量亂碼hhh

ClickHouse提供了很多位圖操作的函數(shù),參見官方文檔,稍后會(huì)給出示例。

翻翻源碼

既然位圖都是從groupBitmap函數(shù)構(gòu)建出來(lái)的,就來(lái)看看與其相關(guān)的實(shí)現(xiàn)吧。來(lái)到AggregateFunctionGroupBitmapData.h,定義如下:

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

其核心就是RoaringBitmapWithSmallSet這個(gè)數(shù)據(jù)結(jié)構(gòu),繼續(xù)看代碼:

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庫(kù)中的實(shí)現(xiàn),SmallSet又是什么鬼?其實(shí)它的定義位于SmallTable.h中,是一個(gè)限定大?。o(wú)法擴(kuò)容)的小集合,底層用數(shù)組來(lái)存儲(chǔ)。在前面的結(jié)構(gòu)體里,模板參數(shù)small_set_size設(shè)為32,說(shuō)明它最多只能容下32個(gè)元素。

繼續(xù)向下讀一段:

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

這下就非常明顯了:當(dāng)位圖的基數(shù)少于32時(shí),僅使用SmallSet存儲(chǔ);一旦超過(guò)此閾值,就調(diào)用toLarge()方法轉(zhuǎn)化為RoaringBitmap,此后都在RoaringBitmap上操作。之所以有這種區(qū)別,想來(lái)是因?yàn)镾mallSet在小數(shù)據(jù)量下的性能比RBM更優(yōu)吧。

當(dāng)然,在RoaringBitmapWithSmallSet之間進(jìn)行運(yùn)算時(shí),就需要分情況討論了。舉個(gè)栗子,兩個(gè)CH位圖做按位與運(yùn)算(對(duì)應(yīng)bitmapAnd函數(shù)),對(duì)應(yīng)方法如下:

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);
    }
}
  • 當(dāng)this為小位圖時(shí),遍歷this并逐個(gè)檢查其中的元素在r1是否存在(只是根據(jù)r1的大小不同調(diào)用的方法不同而已),將重合的元素放入緩存,再重新插入this中。
  • 當(dāng)this為大位圖時(shí),直接調(diào)用CRoaring自帶的roaring_bitmap_and_inplace()方法做與運(yùn)算。注意如果r1為小位圖,需要先調(diào)用getNewRbFromSmall()方法生成新的RBM。

位圖操作函數(shù)的定義與實(shí)現(xiàn)都位于FunctionsBitmap.h內(nèi),例如剛才說(shuō)過(guò)的bitmapAnd函數(shù):

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

位圖部分的源碼在整個(gè)ClickHouse體系中算是相當(dāng)友好的,看官也可自行查看,不再贅述。

CH位圖的應(yīng)用

我們知道,位圖在需要精確統(tǒng)計(jì)基數(shù)及快速求交集、并集的場(chǎng)景非常有用。以用戶行為(點(diǎn)擊流)數(shù)據(jù)為例,創(chuàng)建以下的表:

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以位圖存儲(chǔ)。注意為了配合AggregateFunction,引擎應(yīng)使用AggregatingMergeTree,并且此表的行數(shù)肯定偏少,所以索引粒度可適當(dāng)降低。

取一些數(shù)據(jù)灌入表中:

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;

這樣,我們就可以非??焖俚亟y(tǒng)計(jì)每天查看過(guò)商品詳情頁(yè)的用戶數(shù):

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

以及統(tǒng)計(jì)前一日打開過(guò)App,后一日點(diǎn)擊過(guò)“立即購(gòu)買”的用戶轉(zhuǎn)化率:

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

毫無(wú)疑問(wèn),由于位圖操作消滅了GROUP BY、JOIN等復(fù)雜的關(guān)系代數(shù)操作,效率有極為明顯的提升。

The End

Happy Friday night.

民那晚安晚安。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容