二级索引也能“边查边算”?ClickHouse 25.9 用流式索引把查询延迟砍掉 4 倍
本文字数:3411;估计阅读时间:9 分钟
作者:Tom Schreiber
TL;DR
过去,ClickHouse 会在查询真正开始执行之前,对二级索引进行一次完整扫描。
流式二级索引让索引评估变成按需、增量执行,避免了不必要的前置工作,同时降低了查询延迟和内存占用。
在 ClickHouse 25.9 之前,二级索引( e.g., minmax、set、bloom filter、vector、text )都会在读取底层表数据之前被统一评估。
系统会先扫描索引条目,用来判断哪些 granules( ClickHouse 中最小的处理单元,通常每个 granule 覆盖 8,192 行 )可能包含符合查询 WHERE 过滤条件的行。
下面的动画展示了这个流程:
① 索引扫描与 granule 选择 - ClickHouse 会检查每一个索引条目,以决定需要读取哪些 granules。只有被判定为可能匹配的 granules 才会被选中,其余的会被跳过。
② 查询执行 - 被选中的 granules 会被以流式方式送入查询引擎,最终生成查询结果。
这种在查询开始前完成的索引评估方式存在几个明显问题:
启动延迟:只有在索引分析全部完成之后,真正的查询执行才会开始。
索引扫描成本高:在某些场景下( e.g., 在超大表上执行 WHERE 条件高度选择性的查询 ),扫描索引本身的开销,甚至可能超过实际读取并处理命中数据的成本。
LIMIT 场景效率低:即使查询因为 LIMIT 提前结束,ClickHouse 仍然需要在前期完整扫描整个索引( 并且可能会选中比实际需要更多的数据 )。
在 ClickHouse 25.9 中,通过将数据读取和索引检查交错执行,这些问题得以解决,整体流程如下图所示:
① 索引扫描、granule 选择,以及 ② 查询执行( 并发 )– 当 ClickHouse 即将读取某个表数据 granule( e.g.,因为它已经被主索引分析选中 )时,会先检查对应的二级索引条目( 如果存在 )。如果二级索引表明该 granule 可以被跳过,那么该 granule 根本不会被读取;否则,它会被读取并交由查询引擎处理,同时系统继续扫描后续的 granules。
这种一边读取表 granules、一边检查对应二级索引条目的增量式双流处理方式,就是“流式二级索引”这一特性的由来( 通过设置 use_skip_indexes_on_data_read 控制 )。
( 注:为简化说明,动画中展示的是单线程查询引擎;在真实场景中,通常会有多个线程并发处理大量 granules。 )
通过这种并发执行方式,启动延迟被消除,同时避免了无谓的工作量浪费。例如,对于因为 LIMIT 提前结束的查询,一旦结果满足条件,ClickHouse 就会立即停止后续的索引检查和 granule 读取。
在发布网络研讨会上,Alexey 展示了流式二级索引在超大规模 ClickHouse 表上的效果。这些表包含来自测试运行的数万亿条日志记录,这些测试运行关联到 pull requests 和 commits。在这些数据集中,仅某些单独的二级索引,压缩后体积就超过了 6 TB。
由于在本地复现这种规模并不现实,我们这里使用一个简化的人工示例,你可以自行运行并验证。
实验运行在一台 AWS EC2 m6i.8xlarge 实例上( 32 vCPUs,128 GiB RAM )。
首先,我们创建了一张包含 String 列的表,并在该列上添加了一个 Bloom filter 索引:
CREATE TABLE test (s String,index s_idx s type bloom_filter(0.0001) granularity 1)ENGINE = MergeTreeORDER BY ()SETTINGS index_granularity = 1024;
接着,我们向表中插入了十亿行数据:
INSERT INTO testSELECT if(number % 1024 == 0, 'needle', randomPrintableASCII(64))FROM numbers_mt(1_000_000_000);
此时可以看到,bloom filter 索引的大小已经超过 2 GiB:
SELECTname,type_full,formatReadableSize(data_uncompressed_bytes) AS sizeFROM system.data_skipping_indicesWHERE database = 'default' AND table = 'test';
┌─name──┬─type_full────────────┬─size─────┐1. │ s_idx │ bloom_filter(0.0001) │ 2.21 GiB │└───────┴──────────────────────┴──────────┘
为了保证对比公平,在下面两次测试查询之前,我们都会清空 OS 页面缓存。
echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null在未启用流式索引的情况下( use_skip_indexes_on_data_read = 0 ),使用 LIMIT 1 查找单行数据大约需要 10 秒。
需要注意的是,我们将 max_threads 设置为 1,并关闭了查询条件缓存,以便单独观察二级索引处理带来的影响。
SELECT * FROM test WHERE s = 'needle' LIMIT 1SETTINGSmax_threads = 1,use_query_condition_cache = 0, use_skip_indexes_on_data_read = 0;
┌─s──────┐1. │ needle │└────────┘1 row in set. Elapsed: 10.173 sec. Processed 29.70 thousand rows, 2.14 MB (2.92 thousand rows/s., 210.00 KB/s.)Peak memory usage: 8.90 MiB.
在启用流式索引之后( use_skip_indexes_on_data_read = 1 ),同样的查询大约在 2.4 秒内返回 —— 速度提升超过 4 倍,同时内存使用也更低。
SELECT * FROM test WHERE s = 'needle' LIMIT 1SETTINGSmax_threads = 1,use_query_condition_cache = 0, use_skip_indexes_on_data_read = 1;
┌─s──────┐1. │ needle │└────────┘1 row in set. Elapsed: 2.471 sec. Processed 29.70 thousand rows, 2.14 MB (12.02 thousand rows/s., 864.57 KB/s.)Peak memory usage: 4.48 MiB.
需要再次强调的是,这里的性能提升主要来自两个方面( ① 和 ② ):
在未使用流式索引时:在查询处理真正开始之前,ClickHouse 必须完整扫描并处理索引,提前找出所有可能匹配的 granules。
在使用流式索引时:
① ClickHouse 会一边扫描索引,一边在查询引擎中处理已经命中的 granules。
② 并且一旦找到第一条匹配的数据( LIMIT 1 ),系统就会立即停止后续的索引扫描和 granule 读取,从而避免不必要的工作。
通过并发地进行索引检查和数据读取,而不是先扫描索引再执行查询,ClickHouse 消除了启动延迟,并且在满足 LIMIT 条件时能够立刻停止执行,从而显著降低查询耗时和内存占用。这一点在大表上的提前退出查询场景中尤为重要。
/END/
试用阿里云 ClickHouse企业版
轻松节省30%云资源成本?阿里云数据库ClickHouse 云原生架构全新升级,首次购买ClickHouse企业版计算和存储资源组合,首月消费不超过99.58元(包含最大16CCU+450G OSS用量)了解详情:https://t.aliyun.com/Kz5Z0q9G
征稿启示
面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:[email protected]