上篇文章聊了bitmap这个用户可以额外添加的索引功能,初步讲了下它能体现的优点以及缺陷。
那么同样也叫bitmap,它还有一个功能,那就是作为value字段的字段类型,用于一种特殊功能的聚合,从官方文档描述来看,这个功能主要是用来实现对数据去重的。
可能你会说,不就是去个重嘛,至于这么费劲吗?
你要知道,数据去重这个计算类型,跟普通的count、sum这些聚合不太一样,这些聚合只需要记录“数值”,并将数值相加就可以,而“去重”,则是需要每一条记录进行比较之后才能确定的。
因为需要每一条记录都要进行比较,因此一般我们用distinct去重的效率理论上是比较低的,它是一个时间复杂度相对比较高的计算过程,但是如果考虑用bitmap(或者hll方式)来存储你想要去重的字段值,情况会不会变得不一样呢?
上一篇文章就已经说了bitmap这个数据结构的特点,它的功能之一就是为了数据去重。
既然Doris利用了bitmap这个数据结构特性,那咱们就用实际案例来证明一下,跟普通的distinct功能相比,它到底能不能带来性能上的提升,或者能提升多少呢?
PS:当前Doris版本为1.2.3
0. Doris支持的去重方式
HLL去重(近似):
这个是Doris提供的一种为了效率而牺牲准确度的一种去重方式,具体算法这里就不赘述了,官网说的很清楚(https://doris.apache.org/zh-CN/docs/1.2/advanced/using-hll).
这个玩法,让我想起了Elasticsearch的cardinality,同样也是表达distinct语义,但为了查询效率,ES默认它的去重就是近似值,当然,你也可以通过一个精度参数进行调整(详见官网说明),来让这个去重结果尽可能的准确。
distinct去重(精确):
这个是所有数据库中最传统,最老套,当然也是最准确,但是最耗时(理论上)的去重方式,因为需要逐个数据对比,因此它的时间复杂度最高。
bitmap去重(接近精确):
这个就是今天这篇文章要讲的重点内容,这个号称能够大大提高计算去重效率的技术,到底能不能在实际生产中真正提高生产效率,结论到底会不会让人失望,咱跑起来试试看...
1. 验证准备
为了验证在同一个场景,公平公正地对同一个字段数据的去重效率对比,我在同一张表中通过不同字段类型的设置,来达到这一横向对比的目的,表结构如下:

因为bitmap或者hll的用法必须是基于aggregate模型表来进行,因此你就看到了上面这样的建表方式,有key列,以及进行聚合的value列。
建这张表的目的在于计算target_ip这个字段,分别用如下4种存储方式时,其去重的效率如何:
1. target_ip字段:原生存储方式,定义target_ip这个字段类型为varchar(200),即以普通的字符类型去存储;
2. bitmap_hash64_target_ip 字段:通过bitmap_hash64()这个函数对target_ip的值求hash,然后写入到bitmap中,因为我们知道,hash函数是会存在hash碰撞的,而官方文档描述bitmap_hash64()这个hash函数能进一步减少这个碰撞概率,提高准确度;
3. bitmap_hash_target_ip 字段:跟上面一样,只不过是通过bitmap_hash()这个函数来取target_ip的hash值,该函数会有一定小概率的hash碰撞问题存在,所以理论上其去重精度无法保证100%;
4. hll_target_ip 字段:这个就是用hll的方式,来对数据进行近似去重的,这个字段设置的目的,就是用来对比跟普通distinct去重以及bitmap去重有多大差异的。
2. 开始灌数据
一开始呢,我是用最简单的routine load的方式从kafka写数据到这张表的,因为目标表的字段数量,跟数据源的字段数量不一样(从kafka捞出原始数据,然后用分隔符分出来的字段数),所以这个导入任务是这么创建的:
CREATE ROUTINE LOAD test10 ON dns_logs_from_kafka02
WITH APPEND
COLUMNS TERMINATED BY "|",
COLUMNS(client_ip,
domain,
time,
target_ip,
bitmap_hash64_target_ip=bitmap_hash64(`target_ip`),
bitmap_hash_target_ip=bitmap_hash(`target_ip`),
hll_target_ip=hll_hash(`target_ip`),
tmpadd_msg,
tmpdns_ip)
PROPERTIES
(
"desired_concurrent_number" = "3",
"max_error_number" = "9999999999",
"max_batch_interval" = "20",
"max_batch_rows" = "300000",
"max_batch_size" = "209715200"
)
FROM KAFKA
(
"kafka_broker_list" = "192.168.211.107:6667",
"kafka_topic" = "test",
"property.kafka_default_offsets" = "OFFSET_BEGINNING",
"property.group.id" = "test10",
"property.client.id" = "test10"
);
这里的字段映射需要注意的是,根据官方文档的说明:字段数量必须跟数据源的保持一致,但是如果对应位置的数据源字段在目标表中不存在,就在前面加tmp。
按理说应该没什么问题,数据一开始是可以正常灌进去的,但是到后面了之后,数据入表的效率变得越来越低,然后一查导入日志发现如下报错:

说的是字段数量对不上,这个就比较尴尬了,明明根据官方文档要求的方式做了映射了,但还是出现了这个报错,经过几次的尝试之后还是不行,所以只能说通过这种映射方式的导入不靠谱。
既然这种不行,那我就只能换另一种方式了,因为考虑到需要用bitmap_hash和hll_hash这两个函数,所以我就考虑用下面这种最“笨拙”的方式,从另一张表的数据给insert到这张目标表中:

很快我就给灌入了2亿+条数据:

3. 测试性能对比
既然数据灌入了,接下来就分别来看下target_ip这个字段的值,在不同的存储状态下的去重效率到底如何。
官方文档里给了很多花里花哨的,关于bitmap的去重使用方案(比如关于用户留存,转化率之类的场景),但是对当下的我来说,我的诉求就很简单,就是在两种情况下的去重target_ip有多少个。
3.1 distinct的去重效率
作为基准对比,我们先来看用最普通的distinct去重方式,来看看target_ip有多个非重复记录,先来看target_ip这个字段:

可以看到,这个去重结果为4852,耗时为1.45秒(多次执行效率接近),其实效率看着也还行。
然后再看,如果我要知道每个client_ip访问了多少个不同的target_ip,这个查询效率如何,先看查询语句:
select
client_ip,
count(distinct(target_ip)) as unique_target_ip_count
from
dns_logs_from_kafka02
group by
client_ip
order by
unique_target_ip_count desc
limit 10;
再看查询结果:

查询效率好像看着也还凑合。
同样的两种去重查询,咱们再来看看其他3个用不同方式保存target_ip数据的列,查询效率如何?
3.2 bitmap_hash64的去重效率
再来看字段bitmap_hash64_target_ip的去重效率。
查询方式1:

可以看到,其查询结果跟上面的4852保持一致,但是查询效率就不敢恭维了,比上面的1.45秒慢了近4秒钟。
查询方式2:

可以看到,这个查询结果虽然正确,但是其查询效率同样让人失望。
查询方式3:

同样,其查询效率跟最初的3.84秒比,有差距。
3.3 bitmap_hash的去重效率
接着看字段bitmap_hash_target_ip的去重效率。
查询方式1:

可以看到查询结果正确,但是其查询效率还是比最开始的1.45秒多了2秒多。
查询方式2:

查询结果正确,其效率跟方式1几乎没有区别。
查询方式3:

跟最开始的3.84秒相比,还是要慢一点。
3.4 hll的去重效率
最后再来看hll_target_ip这个字段的去重查询效率。
查询方式1:

可以看到,且其查询结果跟官网描述的一样,只能是近似值,而且如果去重的基数越小,其误差就越大,关键结果不对就算了,当前查询效率还很低,就离谱。
查询方式2:

同样的,其查询结果不出意料的让人失望。
查询方式3:

只能说,还是失望。
注:为了确保上面测试结果的严谨性,每个查询结论都是经过了多次执行对比之后,取的中间值作为结论。
4. 小结一下
本文呢,就是从一个最基础的数据去重需求出发,针对2两个去重查询场景,设计了针对target_ip这个字段的4种不同存储方式,来分别做查询对比。
从测试结论可以看出来,Doris这个所谓的bitmap和hll去重优化,针对我当前这个数据实验场景来说,不但没有提高效率,反而比原始的distinct效率还要低。
核心原因可能是因为我当前测试的数据集中,target_ip这个字段的基数太低(4852个)导致的,如果换成一个高基列,效果应该会好很多。
至于Doris的这个bitmap和hll去重功能在高基列的表现到底如何,只能等到下一次测试了。
怎么样,这次验证的结论,有颠覆你的认知吗?
