ClickHouse社区提供了 bitmap 数据处理能力,详见Bitmap Functions | ClickHouse Docs 在差不多同期,ByteHouse 也自研了一套处理 bitmap 数据的类型和相关函数。它们两者的异同如下:
相同点:
差异点:
AggregateFunction(groupBitmap, UIntX)
,X 可以是 8,16,32,64,代表上游数据的位长;BitMap64
,默认选择 64 位整型值。你可以使用类型关键字BitMap64
在表中定义 bitmap 类型。示例如下:
CREATE TABLE test.bitmap_test ( `p_date` Date, `slice_id` UInt64, `tag_id` Int32, `tag_value` String, `uids` BitMap64 ) ENGINE = MergeTree PARTITION BY p_date ORDER BY (tag_id, tag_value) SETTINGS index_granularity = 128;
bitmap目前有两种构造函数:使用 bitmapFromColumn
函数从一列数据构造,亦或使用 arrayToBitmap
从一个数组构造。示例如下:
select bitmapFromColumn(number) as rbm, toTypeName(rbm) from numbers(10); -- {0,1,2,3,4,5,6,7,8,9}, "BitMap64" select arrayToBitmap(groupArray(number)) as rbm, toTypeName(rbm) from numbers(10); -- {0,1,2,3,4,5,6,7,8,9}, "BitMap64"
本节分为外部系统写入 ByteHouse表,和 ByteHouse 内部写入两部分。
目前主要是通过文本格式(CSV, JSONEachRow)和 Parquet 格式的文件进行导入。
在文本文件中,可以用{}
包裹集合数据,也可以用[]
包裹集合数据,引擎自动解析;
在 Parquet 中,可以用 LIST来存储集合数据,引擎自动转换。
CSV 格式的文件导入如下:
cat ./test_bitmap_input.csv -- "2024-01-01",0,11,"a","{0,1,2,3,4,5,6,7,8,9}" insert into test.bitmap_test format CSV infile 'test_bitmap_input.csv';
Parquet 格式的文件导入如下:
parquet-meta ./test_bitmap_input.parquet --- file schema: schema ------------------------------------------------- p_date: REQUIRED INT32 O:UINT_16 R:0 D:0 slice_id: REQUIRED INT64 O:UINT_64 R:0 D:0 tag_id: REQUIRED INT32 R:0 D:0 tag_value: REQUIRED BINARY O:UTF8 R:0 D:0 uids: REQUIRED F:1 .list: REPEATED F:1 ..element: OPTIONAL INT64 O:UINT_64 R:1 D:2 INSERT INTO test.bitmap_test FORMAT Parquet INFILE 'test_bitmap_input.parquet'
这种通常是从一张表计算得到 bitmap 后写入目标表,通常使用insert select
实现,此时可能需要结合【创建 BitMap64 类型数据】中的构造函数一同使用。
示例如下:
INSERT INTO test.bitmap_test SELECT p_date, slice_id, 100 AS tag_id, 'test2' as tag_value, uids FROM ( SELECT p_date, slice_id, subBitmap(uids, 1, 2) AS uids FROM test.bitmap_test WHERE tag_id = 1 and tag_value = 't1' );
INSERT INTO test.bitmap_test SELECT '2024-01-01' as p_date, 1 as slice_id 101 AS tag_id, 'test2' as tag_value, bitmapFromColumn(number) as uids FROM numbers(10)
INSERT INTO test.bitmap_test values ('2024-01-01', 1, 102, 'test3', [1,2,3,4]);
ByteHouse BitMap64 类型是可以直接通过 select 查询并展示的。不顾需要注意当 bitmap 元素个数比较大(比方说数万个),打印到屏幕上耗时较大,不建议直接打印。
SELECT tag_id, uids FROM test.bitmap_test WHERE tag_id = 101
如果 bitmap元素个数比较多,建议打印出元素个数进行查看。
SELECT tag_id, bitmapCardinality(uids) FROM test.bitmap_test WHERE tag_id = 101
请参考ByteHouse BitMap64在 jdbc中的用法提供的用法。
函数接收一行记录或者一个BitMap64对象,返回一行结果
序号 | 函数接口 | 参数说明 | 返回值 | 功能释义 |
---|---|---|---|---|
1 | arrayToBitmap([x1, …]) | UIntN类型的数组 | BitMap64对象 | 通过一个数组构造一个bitmap对象 |
2 | bitmapToArray(bitmap) | 一个BitMap64对象 | UInt64数组 | 将一个bimap对象转换为数组格式 |
3 | bitmapAnd(bitmap1, bitmap2) | 两个BitMap64对象 | BitMap64对象 | bitmap的交运算 |
4 | bitmapOr(bitmap1, bitmap2) | 两个BitMap64对象 | BitMap64对象 | bitmap的并运算 |
5 | bitmapXor(bitmap1, bitmap2) | 两个BitMap64对象 | BitMap64对象 | bitmap的异或运算 |
6 | bitmapAndnot(bitmap1, bitmap2) | 两个BitMap64对象 | BitMap64对象 | bitmap的差运算 |
7 | bitmapCardinality(bitmap) | 一个BitMap64对象 | UInt64数值 | bitmap中元素的个数 |
8 | bitmapMin(bitmap) | 一个BitMap64对象 | UInt64数值 | bitmap中最小的元素 |
9 | bitmapMax(bitmap) | 一个BitMap64对象 | UInt64数值 | bitmap中最大的元素 |
10 | bitmapAndCardinality | 两个BitMap64对象 | UInt64数值 | bitmap的交运算之后的元素个数 |
11 | bitmapOrCardinality | 两个BitMap64对象 | UInt64数值 | bitmap的并运算之后的元素个数 |
12 | bitmapXorCardinality | 两个BitMap64对象 | UInt64数值 | bitmap的异或运算之后的元素个数 |
13 | bitmapAndnotCardinality | 两个BitMap64对象 | UInt64数值 | bitmap的差运算之后的元素个数 |
14 | bitmapContains(bitmap, integer) | 一个BitMap64对象 | UInt8枚举 | 检查bitmap中是否包含指定元素 |
15 | bitmapHasAll | 两个BitMap64对象 | UInt8枚举 | 检查sub_bitmap是否是bitmap的子集 |
16 | bitmapHasAny | 两个BitMap64对象 | UInt8枚举 | 检查两个bitmap是否存在交集 |
17 | bitmapSubsetInRange | 一个BitMap64对象,两个UIntN数字,标识取值范围 | BitMap64对象 | 检查并返回bitmap中符合给定元素大小范围的值组成的bitmap。 |
18 | bitmapSubsetLimit | 一个BitMap64对象,两个UIntN数字,标识下标启示范围,和子集大小 | BitMap64对象 | 检查并返回由bitmap中指定元素开始的,不超过指定数量的元素组成的bitmap。 |
19 | subBitmap(bitmap, start_offset, length) | 一个BitMap64对象, | BitMap64对象 | 提取bitmap中指定下标范围的数据,类比subString。 |
这些函数主要是对ClickHouse社区相关函数的兼容,详见:
https://clickhouse.tech/docs/en/sql-reference/functions/bitmap-functions/
使用示例见【函数示例章节】。
函数接收多行记录,进行聚合操作,返回一行结果
序号 | 函数接口 | 参数说明 | 返回值 | 功能释义 |
---|---|---|---|---|
41 | bitmapColumnAnd | BitMap64类型的一列数据 | BitMap64对象 | 接收一个bimap列,该列所有bitmap做交运算 |
42 | bitmapColumnOr | BitMap64类型的一列数据 | BitMap64对象 | 接收一个bimap列,该列所有bitmap做并运算 |
43 | bitmapColumnXor | BitMap64类型的一列数据 | BitMap64对象 | 接收一个bimap列,该列所有bitmap做异或运算 |
44 | bitmapColumnCardinality | BitMap64类型的一列数据 | UInt64数值 | 接收一个bimap列,该列所有bitmap做并运算,返回最终结果bitmap的元素个数 |
45 | bitmapColumnHas | BitMap64类型的一列数据 | UInt8枚举 | 接收一个bimap列,检查该列是否包含指定元素 |
46 | bitmapFromColumn(id) -> bitmap | 整数列 | BitMap64对象 | 接收一列整数,聚合成一个bitmap |
试想一个场景:有若干个 bitmap 对象,如何以给定的方式进行计算呢?比方说第一个和第二个求并集,再和第三个求交集。。。 你可能想到了通过 JOIN 和上文中的【普通函数】进行两两计算。如果有100 甚至 1000 个这样的 bitmap,那可能 JOIN 会有 100 个、1000 个,此时 SQL复杂性,可读性,包括计算性能会变得非常差。有没更直观和简介的方法来实现这个需求呢?就是接下来要介绍的高阶聚合函数。
ByteHouse 提供了一系列聚合函数用于批量处理大量 bitmap 之间的复杂计算。通过给每个待计算的 bitmap编序号,并用序号书写表达出集合的交并差运算,ByteHouse 可以根据指定的运算表达式完成对应的 bitmap 运算,直接把计算可视化和简便化,极大提升了写 SQL的体验!
简单图示如下:
带表达式的高阶聚合函数
这种函数有两个括号传递参数:
第一个括号称为parameter,用于控制聚合函数的行为 (控制流)
第二个括号称为argument,用于向聚合函数传递输入数据列 (数据流)
说明
这部分高阶函数并不复杂,但是文字解释函数较为困难,可以结合上图,以及后文详细的【函数示例】章节
序号 | 函数接口 | 参数说明 | 返回值 | 功能释义 |
---|---|---|---|---|
61 | bitmapCount | expression是与或差表达式, | UInt64数值 | 对于bitmap列中的每一行,取其标记idx,存入 map 中。最终依据expression指定的计算方式进行bitmap运算,返回最终bitmap中元素个数 |
62 | bitmapExtract | expression是与或差表达式, | BitMap64对象 | 对于bitmap列中的每一行,取其idx,并依据expression指定的计算方式进行bitmap运算,返回最终计算结果的bitmap |
63 | bitmapMultiCount | expression是与或差表达式, | UInt64数组 | 对于bitmap列中的每一行,取其编号 idx,并依据每个expr指定的计算方式进行多组bitmap运算,返回每组expr计算的结果bitmap中元素个数 |
64 | bitmapMultiExtract | expression是与或差表达式, | BitMap64数组 | 对于bitmap列中的每一行,取其编号 idx,并依据每个expr指定的计算方式进行多组bitmap运算,返回每组expr计算的结果的bitmap数据 |
65 | bitmapMultiCountWithDate | expression是与或差表达式, | UInt64数组 | 在bitmapMultiCount的基础上增加了日期维度,可以区别出不同日期中的编号。即 bitmapMultiCount 只支持一个编号维度,该函数则额外增加了日期维度。 |
66 | bitmapMultiExtractWithDate 注:vanilla-1.5新增,低版本无 | expression是与或差表达式, | BitMap64数组 | 在bitmapMultiExtract的基础上增加了日期维度,可以区别出不同日期中的标签。即 bitmapMultiExtract 只支持一个编号维度,该函数则额外增加了日期维度。 |
Tips:
示例用表 test.bitmap_test,schema 详见【定义 ByteHouse BitMap64 类型】一节。
数据涉及两个标签,tag_id=11,有两个标签值'a'和'b';tag_id=12,有一个标签值'c'。
每个标签值有两行,其 slice_id 不一样。slice_id 是为了把大 bitmap 进行切分,减小每行 bitmap量级,可优化构建效率和内存使用量,同时也能够为分布式存储提供分片凭据。
insert into test.bitmap_test values ('2024-01-01', 0, 11, 'a', [1,2,3,4,5]), ('2024-01-02', 1, 11,'a', [11,12,13,14,15]); insert into test.bitmap_test values ('2024-01-01', 0, 11, 'b', [3,4,5,6,7,8]), ('2024-01-02', 1, 11,'b', [13, 14,15,16]); insert into test.bitmap_test values ('2024-01-01', 0, 12, 'c', [3,4,6,7,9]), ('2024-01-02', 1, 12,'c', [13,15,16,18]);
普通函数的使用和社区demo,以及其它函数并无差异。仅举几个例子。
SELECT arrayToBitmap([1, 2, 3, 4, 5]) FORMAT TSV -- 结果: {1,2,3,4,5}
SELECT bitmapToArray(uids) FROM test.bitmap_test WHERE tag_value = 'a' FORMAT TSV -- 结果: [11,12,13,14,15] [1,2,3,4,5] SELECT bitmapToArray(arrayToBitmap([1, 2, 3, 4, 5])) FORMAT TSV -- 结果: [1,2,3,4,5]
SELECT bitmapCardinality(arrayToBitmap([1, 2, 3, 4, 5])) FORMAT TSV -- 结果: 5
-- 求两个 bitmap 的交集 SELECT bitmapColumnAnd(uids) FROM test.bitmap_test WHERE (slice_id = 0) AND (tag_value IN ('a', 'b')) FORMAT TSV -- 结果: {3,4,5} -- 求两个 bitmap 的并集(两个函数同义) SELECT bitmapCardinality(bitmapColumnOr(uids)), bitmapColumnCardinality(uids) FROM test.bitmap_test WHERE (slice_id = 0) AND (tag_value IN ('a', 'b')) FORMAT TSV -- 结果: 8 8 -- 判断某个 bitmap中是否存在给定元素 SELECT bitmapColumnHas(uids, 12) FROM test.bitmap_test WHERE tag_value = 'a' FORMAT TSV -- 结果: 1
SELECT bitmapFromColumn(number) FROM ( SELECT number FROM system.numbers LIMIT 100, 10 ) FORMAT TSV -- 结果 {100,101,102,103,104,105,106,107,108,109}
**用途:**实现多个bitmap之间的复杂交并差运算。用户通过输入表达式表示出 bitmap 的运算,函数解析后进行对应计算。示例参考上文【聚合函数】- 【高阶聚合函数】小节的图例。
**输入:**编号列,bitmap列。编号列用于标识 bitmap,可以是表中字段(如 tag_id),也可以是人为定义变量(如 idx)。
**表达式:**一个编号锚定一个 bitmap。编号表达式是与或差操作,对应 bitmap的交并差运算。
支持的编号运算符:
**输出:**bitmap 交并差运算之后的结果 bitmap 中的元素个数。
Case 1: 求某个标签'a'总共元素个数:
SELECT bitmapColumnCardinality(uids) FROM test.bitmap_test WHERE tag_value = 'a' FORMAT TSV -- 结果: 10 -- 求标签'a'总共元素个数。等同于bitmapColumnCardinality的计算 SELECT bitmapCount('1')(If(tag_value = 'a', toInt32(1), toInt32(999)) AS idx, uids) AS card FROM test.bitmap_test WHERE tag_value = 'a' FORMAT TSV -- 结果: 10 -- 求标签'a'总共元素个数,标签为字符串,可以直接用字符串表达式 SELECT bitmapCount('a')(tag_value, uids) FROM test.bitmap_test WHERE tag_value = 'a' FORMAT TSV -- 结果: 10
Case 2: 求两个标签的交集'a'和'b'
SELECT bitmapCardinality(bitmapColumnAnd(uids)) AS card FROM test.bitmap_test WHERE (tag_value IN ('a', 'b')) AND (slice_id = 0) FORMAT CSV -- 结果: 10 -- 等同于bitmapCardinality(bitmapColumnAnd)的计算 SELECT bitmapCount('1&2')(multiIf(tag_value = 'a', toInt32(1), tag_value = 'b', 2, toInt32(999)) AS idx, uids) AS card FROM test.bitmap_test WHERE (tag_value IN ('a', 'b')) AND (slice_id = 0) FORMAT TSV -- 结果: 3 -- 上述sql的另一种写法,适合复杂sql嵌套 SELECT bitmapCount('1&2')(idx, uids) FROM ( SELECT toInt32(1) AS idx, uids FROM test.bitmap_test WHERE (slice_id = 0) AND (tag_value IN ('a')) UNION ALL SELECT toInt32(2) AS idx, uids FROM test.bitmap_test WHERE (slice_id = 0) AND (tag_value IN ('b')) ) FORMAT TSV -- 结果: 3
Case 3: 更多标签的交并差运算
-- 写法一 SELECT bitmapCount('1&2|3')(multiIf(tag_value = 'a', toInt32(1), tag_value = 'b', toInt32(2), tag_value = 'c', toInt32(3), toInt32(999)) AS idx, uids) AS card FROM test.bitmap_test FORMAT TSV -- 结果: 11 -- 写法二:刚好 tag_value是字符串类型,可以使用字符串表达式 SELECT bitmapCount('a&b|c')(tag_value, uids) FROM test.bitmap_test FORMAT TSV -- 结果: 11
这个函数计算方法和bitmapCount是一样的,区别是它返回计算结果的bitmap, 不是个数。
至于怎么组合标签,等同于bitmapCount函数。bitmapCount 那里使用的是 tag_value 作为示例,这里展示一个 tag_id 作为示例的 case: 求两个标签的交集:
SELECT bitmapExtract('1&2')(multiIf(tag_id = 11, toInt32(1), tag_id = 12, 2, toInt32(999)) AS idx, uids) AS card FROM test.bitmap_test WHERE (tag_id IN (1,2)) AND (slice_id = 0) FORMAT TSV -- 结果(没有交集): {} 另一种写法: SELECT bitmapExtract('11&12')(tag_id, uids) FROM test.bitmap_test WHERE (tag_id IN (1, 2)) AND (slice_id = 0) FORMAT TSV -- 结果(没有交集): {}
这个聚合函数再bitmapCount的基础上,支持了多组表达式。
**用途:**实现多个bitmap之间的复杂交并差运算,同时支持了多组运算和运算复用。用户通过输入表达式表示出 bitmap 的运算,函数解析后进行对应计算。示例参考上文【聚合函数】- 【高阶聚合函数】小节的图例。
**输入:**编号列,bitmap列。编号列用于标识 bitmap,可以是表中字段(如 tag_id),也可以是人为定义变量(如 idx)。
**表达式:**一个编号锚定一个 bitmap。编号表达式是与或差操作,对应 bitmap的交并差运算。同时可以提供多组表达式,函数依次执行对应计算,把多组结果一并返回。
支持的编号运算符:
**输出:**bitmap 交并差运算之后的结果 bitmap 中的元素个数。每个表达式对应一个结果,存储在数组中。
-- '_3|3'等价于 '(1&2)|3' SELECT bitmapMultiCount('1', '2', '1&2', '_3|3')(multiIf(tag_value = 'a', toInt32(1), tag_value = 'b', toInt32(2), tag_value = 'c', toInt32(3), toInt32(999)) AS idx, uids) AS card FROM test.bitmap_test WHERE slice_id = 0 FORMAT TSV -- 结果: [5,6,3,6] -- tag_value 刚好是字符串类型,所以也可以使用字符串表达式 SELECT bitmapMultiCount('a', 'b', 'a&b', '_3|c')(tag_value, uids) AS card FROM test.bitmap_test WHERE slice_id = 0 FORMAT TSV -- 结果: [5,6,3,6]
_3
表示复用了位置3处的表达式,即'1&2'。
有一组集合关系可以验证是否正确。该关系是
A + B = AUB + A交B
SELECT bitmapMultiCount('1', '2', '1&2', '1|2')(multiIf(tag_value = 'a', toInt32(1), tag_value = 'b', toInt32(2), tag_value = 'c', toInt32(3), toInt32(999)) AS idx, uids) AS card FROM test.bitmap_test WHERE slice_id = 0 FORMAT TSV -- 结果: [5,6,3,8] -- tag_value 刚好是字符串类型,所以也可以使用字符串表达式 SELECT bitmapMultiCount('a', 'b', 'a&b', 'a|b')(tag_value, uids) FROM test.bitmap_test WHERE slice_id = 0 FORMAT TSV -- 结果: [5,6,3,8]
含义用户同bitmapMultiCount,差异点在返回值,返回 bitmap 数据。
SELECT bitmapMultiExtract('1', '2', '1&2', '1|2')(multiIf(tag_value = 'a', toInt32(1), tag_value = 'b', toInt32(2), tag_value = 'c', toInt32(3), toInt32(999)) AS idx, uids) AS card FROM test.bitmap_test WHERE slice_id = 0 FORMAT TSV [{1,2,3,4,5},{3,4,5,6,7,8},{3,4,5},{1,2,3,4,5,6,7,8}] -- tag_value 刚好是字符串类型,所以也可以使用字符串表达式 SELECT bitmapMultiExtract('a', 'b', 'a&b', 'a|b')(tag_value, uids) FROM test.bitmap_test WHERE slice_id = 0 FORMAT TSV -- 结果: [{1,2,3,4,5},{3,4,5,6,7,8},{3,4,5},{1,2,3,4,5,6,7,8}]
**用途:**这个函数在bitmapMultiCount的基础上又加入了日期维度,可以在表达式中区别不同日期的标签。
注意日期列需要是 Int64 类型,一般日期用 Date类型,需要使用 toYYYYMMDD 函数进行转换。
**输入:**日期列,编号列,bitmap 列。
**表达式:**一个日期和一个编号组合锚定一个 bitmap,无法单独使用。所以日期_编号
作为整体使用,如示例中的'20240101_1' 才能标识一个 bitmap。上文的函数bitmapMultiCount只需要'1'就可以,这是两者的差异点。
**输出:**bitmap 交并差运算之后的结果 bitmap 中的元素个数。每个表达式对应一个结果,存储在数组中。
SELECT bitmapMultiCountWithDate('20240101_1', '20240102_2', '20240101_1|20240102_2')(CAST(toYYYYMMDD(p_date), 'Int64') AS date1, multiIf((tag_value = 'a') AND (p_date = '2024-01-01'), toInt64(1), (tag_value = 'b') AND (p_date = '2024-01-02'), toInt64(2), (tag_value = 'c') and (p_date = '2024-01-01'), toInt64(3),toInt32(999)) AS idx, uids) AS card FROM test.bitmap_test FORMAT TSV -- 结果: [5,4,9] -- tag_value刚好是字符串类型,所以可以使用字符串表达式 SELECT bitmapMultiCountWithDate('20240101_a', '20240102_b', '20240101_a|20240102_b')(CAST(toYYYYMMDD(p_date), 'Int64') AS date1, tag_value, uids) AS card FROM test.bitmap_test FORMAT TSV -- 结果: [5,4,9]
功能和用法同bitmapMultiCountWithDate,差异点在返回值,返回 bitmap 数据。
SELECT bitmapMultiExtractWithDate('20240101_1', '20240102_2', '20240101_1|20240102_2')(CAST(toYYYYMMDD(p_date), 'Int64') AS date1, multiIf((tag_value = 'a') AND (p_date = '2024-01-01'), toInt64(1), (tag_value = 'b') AND (p_date = '2024-01-02'), toInt64(2), (tag_value = 'c') and (p_date = '2024-01-01'), toInt64(3),toInt32(999)) AS idx, uids) AS card FROM test.bitmap_test FORMAT TSV -- 结果: [{1,2,3,4,5},{13,14,15,16},{1,2,3,4,5,13,14,15,16}] -- tag_value刚好是字符串类型,所以可以使用字符串表达式 SELECT bitmapMultiExtractWithDate('20240101_a', '20240102_b', '20240101_a|20240102_b')(CAST(toYYYYMMDD(p_date), 'Int64') AS date1, tag_value, uids) AS card FROM test.bitmap_test FORMAT TSV -- 结果: [{1,2,3,4,5},{13,14,15,16},{1,2,3,4,5,13,14,15,16}]