BitMap及其在ClickHouse中的应用
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BitMap及其在ClickHouse中的应用相关的知识,希望对你有一定的参考价值。
参考技术A 问题要从面试或者大数据场景下最常见的一个算法说起,问题是这样的,假如有几十亿个unsigned int类型的数据,要求去重或者计算总共有多少不重复的数据?最简单的办法就是直接利用一个HashMap,进行去重。但是这里面有个内存使用量的问题,几十亿个元素,即使不考虑HashMap本身实现所用到的数据结果,单单key本身,假如每个unsigned int占用4个字节,简单算一下的话,这里都需要几十GB的内存占用,因此,这里就引出了BItMap。BItMap的思想非常简单,就是用一个bit表示一个二元的状态,比如有或者没有,存在或者不存在,用bit本身的位置信息,对应不同的数据。比如针对上面的问题,我们可以开辟一个2^32 bit的内存空间,每一个bit存储一个unsigned int类型的数据,有就是1,没有就是0,总共需要存储unsigned int类型的最大范围个数据,也就是2^32 个数据,这个2^32其实就是所谓的基数。如下图所示:
假如存在数字8,那就把对应的第8位的值赋为1。上图插入的数据为1、3、7、8。接着依次把所有的数据遍历然后更新这个BitMap。这样我们就可以得到最终结果。
假如上面的问题变成了对几十亿个URL做判断,那应该怎么去做呢?URL没有办法和BitMap的位置关系对应上,所以,我们需要加一层哈希,把每个URL经过哈希运算得到一个整数,然后对应上BitMap。如下图所示:
但是有哈希,肯定会存在碰撞,如果BitMap基数(也就是长度)比较小,那碰撞的概率就大,如果基数比较大,那占用的空间又会比较多。Bloom Filter的思想就是引入多个哈希函数来解决冲突的问题。也就是说对每个URL,经过多个哈希函数的运算,得到多个值,每个数值对应的BitMap的对应的位置都赋值为1。这个两个URL经过多个哈希函数结果还是一样的概率就大大降低。
但是由于依然存在冲突的可能性(其实冲突就是来源于我们BitMap的长度小于了数据量的基数,这也就是牺牲了准确性换来了空间使用的减少),所以Bloom Filter 存在假阳性的概率,不适用于任何要求 100% 准确率的场景,也就是说Bloom Filter 只能用来判无,不能用来判有。比如一个URL经过多次哈希运算之后,发现对应的BitMap的位置都已经是1了,那也不能说明,这个URL之前存在过了,也有可能是哈希冲突的结果。但是一个URL经过多次哈希运算之后,发现对应的BitMap的位置不是都是1,那当前URL之前一定是没有存在过的。
可以看到,Bloom Filter 引入多次哈希,在查询效率和插入效率不变的情况下,用较少空间的BitMap解决大数据量的判断问题。
大部分情况下仅仅做有无的判断是不能满足使用需求的,我们还是需要真正意义上的BitMap(可以方便的用来做交并等计算),但是最好可以在基数比较大的时候,依然可以占用相对比较小的空间。这就是RoaringBitMap所要实现的。
简单来说RoaringBitMap是BitMap的一种带索引的复杂BitMap数据结构。以32位的RoaringBitMap为例,首先划分2^16 个空间(Container),每个Container内部都是一个大小为2^16 bit的BitMap,总的内存使用量还是2^32 = 512Mb。这样的话和普通的BitMap是没有区别的,而RoaringBitMap的创新之处在于每个Container内的BitMap是在没有使用到的情况下是可以不分配内存空间的。这样可以大大减小内存的使用量。
(这个图片是Roaring Bitmaps: Implementation of an Optimized Software Library 论文原图)
要将一个4个字节的数据插入RoaringBitMap,首先要用数据的高16位,找到对应的Container,然后用数据的低16在Container中插入。
在每个Container内部,RoaringBitMap不是简单的用BitMap来进行数据的存储,而是把Container的类型划分为几种,不同的Container用来存储不同情况的数据。
当2个字节(4个字节的原数据,低16位用来插入具体的Container中)的数据,总的个数小于4096个的时候,当前Container使用 array Container。为什么是4096个呢?4096*2B=8Kb,而一个Container如果是bitmap的结构的话,最多也就是2^16bit=8Kb的空间。所以这里当数据个数小于4096使用array Container会更节省空间。当然这里名字为array Container,实际上是链表结构,不需要最开始就初始化4096个short int的数组。
当array Container存储的数到4096个的时候(也就是使用内存到8Kb的时候),array Container会转换为bitmap container,bitmap container就是一个2^16 bit普通的bitmap,可以存储2^16 = 65536个数据。这个8Kb还有一个好处,是可以放到L1 Cache中,加快计算。
这个严格的说,只是一种数据压缩存储方法的实现。其压缩原理是对于连续的数字只记录初始数字以及连续的长度,比如有一串数字 12,13,14,15,16 那么经过压缩后便只剩下12,5。从压缩原理我们也可以看出,这种算法对于数据的紧凑程度非常敏感,连续程度越高压缩率也越高。当然也可以实现其他的压缩方法。
RoaringBitMap其核心就在于加了一层索引,利用复杂的数据结构换取了空间上的效率。需要注意的是这里并没有增加计算的复杂度,其出色的数据结构让其在做交并计算的时候性能也毫不逊色。
ClickHouse中有bloom_filter类型的Skipping indexs,可以方便的用来过滤数据。
ClickHouse实现了大量的BitMap的函数,用来操作BitMap。ClickHouse中的BitMap在32位的时候用的是Set实现的,大于32位的时候也是使用RoaringBitMap实现的。我们这里不看具体的函数,我们来看一个典型的使用场景。
最常见的一个场景是根据标签来进行用户的圈选。常见的解决办法是有一张用户标签表,比如
要查询标签tag1='xx'和tag2='xx'的用户需要执行SQL:
但是由于不可能对每个tag列构建一级索引,所以这条SQL执行的效率并不高。可选的一种方式是先构建关于标签的BitMap数据结果,然后进行查询:
(1) 创建tag的bitmap表:
(2)写入数据
(3)查询
如果有多张tag表,进行交并计算(要比普通的用户表进行JOIN或者IN计算要高效很多):
在 Clickhouse 中的多个列上应用 argMax
【中文标题】在 Clickhouse 中的多个列上应用 argMax【英文标题】:Apply argMax over multiple columns in Clickhouse 【发布时间】:2021-08-19 14:31:04 【问题描述】:我的 Clickhouse 表有一个主键列 (pk
)、一个插入时间戳列 (insert_ts
) 和一堆数据列。我想获取每个数据列的最新值。我的查询可能如下所示:
SELECT pk, argMax(data1, insert_ts), argMax(data2, insert_ts), ... GROUP BY pk
这非常冗长,我更喜欢使用带有 EXCEPT/APPLY 的通配符,如下所示:
SELECT * EXCEPT(insert_ts) APPLY(argMax) GROUP BY pk
但我无法指定argMax
的第二个参数。有什么想法吗?
我对这个特定问题的答案以及对 XY 问题的答案都感兴趣,这些答案提出了一种不同的方式来构建我的表格。
【问题讨论】:
不可能github.com/ClickHouse/ClickHouse/issues/27877 【参考方案1】:感谢 Clickhouse 团队快速实施解决方案!
SELECT * EXCEPT(insert_ts) APPLY(x->argMax(x,insert_ts)) GROUP BY pk
对于 XY 问题,Clickhouse 提供了 ReplacingMergeTree 引擎,用于明确保留最新的行:
https://altinity.com/blog/2020/4/14/handling-real-time-updates-in-clickhouse https://kb.altinity.com/engines/mergetree-table-engine-family/replacingmergetree【讨论】:
最新的行只保留在一个分区内,并在分区中合并部分后。如果你有大分区或频繁插入或同时插入多个分区,你将面临ReplacingMergeTree中具有相同PK的多行。而且,当然,这些东西只在一个分片内有效。以上是关于BitMap及其在ClickHouse中的应用的主要内容,如果未能解决你的问题,请参考以下文章
《ClickHouse企业级应用:入门进阶与实战》8 基于ClickHouse Bitmap实现DMP用户画像标签圈人
《ClickHouse企业级应用:入门进阶与实战》8 基于ClickHouse Bitmap实现DMP用户画像标签圈人