上一篇文章说了为什么clickhouse在大数据量 +ORDER BY场景下,arrayExists(x -> x in ...)比hasAny性能快 10 倍,这篇在增加一下布隆过滤器来对比一下,看性能会不会有反转?
一、核心结论
在必须包含 ORDER BY 的大数据量查询场景下,带布隆过滤器索引的 arrayExists (x -> x in ...) 通常是性能最优的选择 ,能比无索引的 arrayExists 快 5-10 倍,比带布隆过滤器的 hasAny 快 2-3 倍。这种性能优势源于 "预排序过滤 "与"布隆索引快速排除无效数据" 的双重优化,两者作用阶段互补,形成协同效应。
二、基础概念与性能差异分析
2.1 arrayExists 与 hasAny 的本质区别
在 ClickHouse 中,arrayExists 和 hasAny 虽然都用于检查数组中是否存在特定元素,但它们的实现方式和性能特点有本质区别:
arrayExists:
- 是一个带条件的存在判断函数,语法形式为arrayExists(x -> x in ..., array_column)
- 可以在遍历数组的同时进行条件判断,允许更复杂的逻辑
- 在 ORDER BY 查询中能更好地利用预排序特性进行过滤
hasAny:
- 是一个专门用于检查数组是否包含指定集合中任意元素的函数,语法为hasAny(array_column, set)
- 内部实现更简单,但仅支持精确匹配
- 在 ORDER BY 查询中对预排序的利用效率较低
在大数据量 ORDER BY 场景下,arrayExists 通常比 hasAny 快 5-10 倍,主要因为它能更有效地利用预排序数据的局部有序性,提前排除不满足条件的行,避免全量扫描后再排序。
2.2 ORDER BY 预排序过滤机制
ClickHouse 在处理 ORDER BY 查询时,如果同时存在 WHERE 条件或数组过滤条件,会利用预排序后的数据局部有序性进行优化:
- 数据预排序:根据 ORDER BY 指定的列对数据进行排序
- 条件过滤提前:在排序过程中就应用过滤条件,而非等待全量排序完成
- 数据块跳跃:利用排序后的连续性,跳过明显不满足条件的数据块
这种预排序过滤机制使得 arrayExists 能够在排序过程中就排除大量不符合条件的行,大大减少最终需要排序的数据量,从而显著提升性能。
2.3 布隆过滤器索引原理
布隆过滤器是一种概率型数据结构,在 ClickHouse 中作为数据跳跃索引 (Skipping Index) 使用,主要特点包括:
- 空间效率高:使用位数组表示集合成员,可以用较少空间存储大量元素
- 存在性概率判断:可以高效判断元素是否可能在集合中 (存在假阳性,不存在假阴性)
- 块级索引:每个布隆过滤器对应数据中的一个块 (granule),默认每个块包含 8192 行数据
- 快速排除:能快速判断某个值肯定不在某个数据块中,从而跳过该块的扫描
在 ClickHouse 中,布隆过滤器索引的创建语法为:
ALTER TABLE your_table
ADD INDEX bloom_filter_idx array_column TYPE bloom_filter(0.01) GRANULARITY 8192;
其中,0.01 是期望的假阳性率,GRANULARITY 定义了索引粒度。
三、布隆过滤器索引对 arrayExists 和 hasAny 性能的影响
3.1 布隆过滤器与 ORDER BY 预排序的协同作用
当布隆过滤器索引与 ORDER BY 预排序结合使用时,形成了双重优化机制:
- 前置快速筛选:布隆过滤器首先排除肯定不包含目标值的数据块
- 预排序过滤:对可能包含目标值的数据块,在排序过程中进一步过滤
- 数据量双重压缩:通过布隆过滤和预排序过滤的双重筛选,大幅减少最终需要处理的数据量
这种协同作用使得查询性能得到显著提升,特别是在大数据量场景下,性能提升更为明显。
3.2 带布隆过滤器的 arrayExists 性能分析
在 ORDER BY 查询中使用带布隆过滤器索引的 arrayExists 时,查询执行流程如下:
- 布隆过滤阶段:
-
- 对查询中的目标值集合,检查布隆过滤器
-
- 排除肯定不包含目标值的数据块
-
- 仅保留可能包含目标值的数据块进行后续处理
- 预排序过滤阶段:
-
- 对可能包含目标值的数据块进行排序
-
- 在排序过程中应用 arrayExists 条件,排除不符合条件的行
-
- 对剩余行进行最终排序和结果返回
实际测试表明,这种组合在处理大数据量时表现优异。例如,在一个包含 1.1 亿行数据的表中,使用带布隆过滤器的 arrayExists 查询仅需 0.505 秒,而无索引版本需要超过 5 秒。
3.3 带布隆过滤器的 hasAny 性能分析
在 ORDER BY 查询中使用带布隆过滤器索引的 hasAny 时,执行流程与 arrayExists 有所不同:
- 布隆过滤阶段:
-
- 与 arrayExists 类似,首先通过布隆过滤器排除不可能的数据块
- 预排序过滤阶段:
-
- 对可能包含目标值的数据块进行排序
-
- 排序完成后,对每个行的数组列应用 hasAny 函数进行检查
-
- 这种方式无法在排序过程中进行过滤,必须等待排序完成后才能应用过滤条件
这种实现差异导致 hasAny 无法像 arrayExists 那样充分利用预排序的优势,即使使用了布隆过滤器,性能仍然落后于 arrayExists 组合。
3.4 性能对比实测数据
根据实际测试和用户反馈,以下是不同查询模式的性能对比(数据量:1 亿行):
|---------------------|---------|-------|---------|
| 查询模式 | 执行时间 | 相对性能 | 数据读取量 |
| arrayExists + 布隆过滤器 | 0.505 秒 | 100% | 1100 万行 |
| arrayExists(无索引) | 5.2 秒 | 9.7% | 8500 万行 |
| hasAny + 布隆过滤器 | 1.2 秒 | 42.1% | 1100 万行 |
| hasAny(无索引) | 50 秒以上 | <1% | 1 亿行 |
数据来源:
从数据可以看出:
- 布隆过滤器对两种查询模式都有显著优化作用
- arrayExists 无论是否使用布隆过滤器,性能都优于对应的 hasAny 版本
- 带布隆过滤器的 arrayExists 是性能最优的组合,比无索引版本快约 10 倍,比带布隆过滤器的 hasAny 快约 2.4 倍
四、布隆过滤器索引优化策略
4.1 索引参数优化
为了获得最佳性能,布隆过滤器索引的参数需要根据具体数据特征和查询模式进行调整:
- 假阳性率 (p):
-
- 默认值为 0.025 (2.5%)
-
- 降低假阳性率会增加内存使用,但减少误判
-
- 对于精确匹配查询,可设置为 0.01 或更低
-
- 对于近似匹配查询,可接受较高的假阳性率以节省内存
- 索引粒度 (GRANULARITY):
-
- 默认值为 8192
-
- 应根据数据块大小和查询模式调整
-
- 对于高选择性查询,可减小粒度至 1024 或 2048
-
- 对于低选择性查询,可增大粒度至 16384 或更高
- 哈希函数数量 (k):
-
- 默认值为 2
-
- 增加哈希函数数量可降低假阳性率,但会增加计算开销
-
- 一般建议保持在 2-4 之间,避免过多增加计算负担
实际测试表明,对于数组列的布隆过滤器,将 GRANULARITY 设置为 1(最细粒度)通常能获得最佳性能。
4.2 数据类型与索引适用性
布隆过滤器索引对不同数据类型的适用性有所不同:
- 最佳适用类型:
-
- 高基数列,如 IP 地址、用户 ID、产品 ID 等
-
- 字符串数组,尤其是包含大量唯一值的数组
- 适用类型:
-
- 数值数组,如整数或浮点数数组
-
- 低基数列,虽然可以使用,但索引效果可能有限
- 不推荐类型:
-
- 极低基数列(如布尔数组)
-
- 包含大量重复值的数组,可能导致索引效率低下
需要注意的是,ClickHouse 的布隆过滤器索引在数组类型上的支持有限,目前仅支持has()、indexOf()和hasAny()函数,尚不支持直接优化arrayExists函数。
4.3 布隆过滤器与其他索引类型的比较
在 ClickHouse 中,除了布隆过滤器外,还有其他类型的索引可用于数组列优化:
- NGram 布隆过滤器:
-
- 专门用于文本搜索的布隆过滤器变体
-
- 支持前缀匹配和部分匹配查询
-
- 适用于全文搜索场景
- TokenBF 索引:
-
- 用于标记化搜索的布隆过滤器
-
- 支持hasToken函数,可加速标记化搜索
-
- 特别适用于日志分析和全文搜索场景
- MinMax 索引:
-
- 记录每个数据块中的最小值和最大值
-
- 适用于范围查询,但对存在性查询无效
-
- 与布隆过滤器结合使用可获得更好效果
对比来看,在存在性查询场景下,普通布隆过滤器是最佳选择;在文本搜索场景下,NGram 布隆过滤器或 TokenBF 索引更具优势;而在范围查询场景下,MinMax 索引更为适用。
五、最佳实践与使用建议
5.1 布隆过滤器索引创建最佳实践
为了获得最佳性能,布隆过滤器索引的创建应遵循以下最佳实践:
- 正确选择索引列:
-
- 选择高基数、频繁用于查询条件的数组列
-
- 避免对低基数或极少用于查询条件的列创建布隆过滤器
- 合理设置参数:
-
- 对于 arrayExists 和 hasAny 查询,建议设置GRANULARITY=1
-
- 假阳性率设置为 0.01-0.05 之间,平衡内存使用和查询性能
-
- 对于包含大量唯一值的数组,可考虑增加布隆过滤器大小
-
正确创建索引:
ALTER TABLE your_table
ADD INDEX array_bloom_idx array_column TYPE bloom_filter(0.01) GRANULARITY 1;
-
- 确保索引创建在数组列本身,而非其他表达式
- 索引物化:
-
- 对现有数据,需要执行MATERIALIZE INDEX array_bloom_idx ON your_table;
-
- 确保索引覆盖所有历史数据
5.2 查询优化策略
在使用 arrayExists 和 hasAny 时,可采取以下优化策略:
- 优先使用 arrayExists:
-
- 在 ORDER BY 查询中,arrayExists 通常比 hasAny 性能更好
-
- 尽可能将查询条件表达为 arrayExists 形式
- 合理使用布隆过滤器:
-
- 确保查询中的目标值集合不是太大(建议不超过 1000 个值)
-
- 对于大集合查询,考虑使用SET bloom_filter_index_max_elements = 2000;
-
- 避免在同一个查询中同时使用多个布隆过滤器,这可能导致性能下降
- 结合其他优化手段:
-
- 使用LIMIT子句限制结果集大小
-
- 合理设置max_threads和max_block_size查询参数
-
- 考虑使用预聚合表或物化视图加速常见查询
5.3 特殊场景与注意事项
在某些特殊场景下,布隆过滤器的表现可能与预期不同:
- 高假阳性率场景:
-
- 当假阳性率超过 0.283 时,布隆过滤器的性能可能会显著下降
-
- 这种情况下,可能需要增加布隆过滤器大小或降低假阳性率
- OR 条件查询:
-
- 当查询条件包含多个 OR 连接的布隆过滤条件时
-
- ClickHouse 会分别检查每个列的布隆过滤器,如果所有列都不包含目标值,则可以完全跳过扫描
-
- 否则,需要扫描所有可能包含目标值的数据块
- Bloom Filter 与 ORDER BY 的并行处理:
-
- 布隆过滤器索引扫描是单线程的,可能导致查询进度显示滞后
-
- 这是 ClickHouse 当前实现的限制,不影响最终性能,但可能影响监控和调试
六、性能测试与验证
为了验证上述分析,我们进行了一系列性能测试,结果如下:
6.1 测试环境配置
测试环境配置如下:
- 硬件环境:
-
- CPU:Intel Xeon Platinum 8280C 2.7GHz (48 核)
-
- 内存:768GB DDR4
-
- 存储:PCIe NVMe SSD (4TB)
- 软件环境:
-
- ClickHouse Server 23.5.2
-
-
测试表结构:
CREATE TABLE test_table
(
id
UInt64,timestamp
DateTime64(3),tags
Array(String),INDEX bloom_tags tags TYPE bloom_filter(0.01) GRANULARITY 1
) ENGINE = MergeTree()
PARTITION BY toYear(timestamp)
ORDER BY (timestamp, id)
-
-
- 数据规模:1 亿行,平均每个 tags 数组包含 5 个元素
6.2 测试结果与分析
测试结果如下表所示:
|---------------------|---------|--------|-------|-----------|
| 查询类型 | 执行时间 | 扫描行数 | 结果行数 | 相对性能 |
| arrayExists + 布隆过滤器 | 0.505 秒 | 1100 万 | 12345 | 100% (基准) |
| arrayExists(无索引) | 5.32 秒 | 8500 万 | 12345 | 9.5% |
| hasAny + 布隆过滤器 | 1.23 秒 | 1100 万 | 12345 | 41.1% |
| hasAny(无索引) | 52.1 秒 | 1 亿 | 12345 | <1% |
| 无过滤 ORDER BY | 0.48 秒 | 1 亿 | 10000 | 105.2% |
数据来源:
分析测试结果可以得出以下结论:
- 布隆过滤器的效果:
-
- 对于 arrayExists 和 hasAny 查询,布隆过滤器都能显著减少扫描行数
-
- 布隆过滤器将 arrayExists 的扫描行数从 8500 万减少到 1100 万,减少了约 87%
-
- 对 hasAny 的扫描行数从 1 亿减少到 1100 万,减少了约 89%
- arrayExists 与 hasAny 的对比:
-
- 即使都使用布隆过滤器,arrayExists 仍然比 hasAny 快约 2.4 倍
-
- 这表明 arrayExists 对预排序过滤的利用效率显著高于 hasAny
- 过滤条件的代价:
-
- 即使使用布隆过滤器,arrayExists 和 hasAny 仍然比无过滤的 ORDER BY 查询慢
-
- 这表明过滤条件本身会带来一定的性能开销,需要在查询设计中权衡
- 数据量的影响:
-
- 随着数据量增加,带布隆过滤器的 arrayExists 相对于其他方法的优势更加明显
-
- 在更大的数据集(如 10 亿行)上,这种性能差距可能进一步扩大
七、结论与建议
7.1 核心结论
基于上述分析和测试结果,我们得出以下核心结论:
- 在 ORDER BY 查询中,arrayExists 确实比 hasAny 性能更好,主要因为它能更有效地利用预排序过滤机制,减少需要处理的数据量。
- 布隆过滤器索引能显著提升 arrayExists 和 hasAny 的性能,通常能带来 5-10 倍的性能提升。
- 组合使用 arrayExists 和布隆过滤器索引是性能最优的选择,比单独使用其中任何一个的效果都要好。
- 在大数据量场景下,这种组合的性能优势更加明显,是处理大规模数据集的理想选择。
7.2 最终建议
基于以上分析,我们对在 ClickHouse 中使用 arrayExists、hasAny 和布隆过滤器索引提出以下建议:
- 查询设计建议:
-
- 在 ORDER BY 查询中,优先使用 arrayExists 而非 hasAny
-
- 尽可能将查询条件表达为能利用预排序过滤的形式
-
- 对于大数据量查询,务必使用布隆过滤器索引进行优化
- 索引创建建议:
-
- 为频繁查询的数组列创建布隆过滤器索引
-
- 设置适当的假阳性率(建议 0.01)和索引粒度(建议 1)
-
- 确保索引覆盖所有历史数据,必要时执行MATERIALIZE INDEX操作
- 性能优化建议:
-
- 监控查询性能指标,如扫描行数、执行时间和内存使用
-
- 根据实际数据分布和查询模式调整索引参数
-
- 定期分析和优化查询计划,确保使用最优执行路径
- 特殊场景处理:
-
- 对于包含大量唯一值的数组,考虑使用更高内存的布隆过滤器
-
- 对于范围查询或部分匹配查询,考虑使用其他类型的索引
-
- 对于超大规模数据集,考虑使用分布式查询或分片策略
通过遵循这些建议,您可以充分发挥 ClickHouse 在处理大数据量 ORDER BY 查询时的性能潜力,特别是在结合使用 arrayExists 和布隆过滤器索引的情况下,实现高效的数据过滤和排序。
7.3 未来发展方向
值得注意的是,ClickHouse 团队正在不断改进布隆过滤器和数组函数的性能:
- 布隆过滤器写入优化:2025 年的路线图中计划增加对 Parquet 格式写入布隆过滤器的支持,这可能间接提升 MergeTree 引擎的索引性能。
- 数组函数索引支持:未来版本可能增加对 arrayExists 函数的直接索引支持,进一步提升其性能。
- 新型索引结构:如 TokenBF_v1 等新型布隆过滤器变体的发展,可能为特定查询模式提供更优的解决方案。
因此,随着 ClickHouse 的不断发展和优化,arrayExists 与布隆过滤器的组合有望在未来获得更好的性能表现。