先说结论

ClickHouse 跳数索引为一组 granule 保存摘要,查询条件证明某些数据块不可能命中时直接跳过。它是主排序键之外的辅助裁剪手段,不像 OLTP 二级索引那样返回精确行位置。

索引有效性高度依赖数据局部性;若目标值均匀散布在每个数据块中,索引几乎无法跳过任何内容。

常见类型

minmax 适合数值或时间在局部有序的数据;set 适合每块不同值很少的列;Bloom Filter 适合等值、集合或特定字符串搜索,允许假阳性但不能漏掉真实匹配。

跳数索引如何判断“可跳过”

每个数据 granule 或一组 granule 记录摘要。查询例如 status = 'PAID' 时,set 索引若明确说明该块不包含 PAID,就不读取该块;Bloom Filter 若判断“不可能存在”,也可跳过,判断“可能存在”只能继续读。它不会像 B+Tree 一样精确定位行,而是减少需要解压和扫描的数据块。

ALTER TABLE events
ADD INDEX idx_user user_id TYPE bloom_filter(0.01) GRANULARITY 4;

GRANULARITY 4 表示索引摘要覆盖多个主键 granule。粒度越小,摘要更细、可能跳过更多;代价是索引文件、写入和 merge 成本增加。

类型选择的前提

索引 合适的块内分布 不适合情况
minmax 值随排序局部单调或范围集中 每块都覆盖全值域
set 每块不同值数量少 高基数随机散布
bloom_filter 查询等值值在多数块不存在 目标值几乎每块都有
ngram/tokenbf 特定文本匹配 需要精确全文相关性排序

如果 user_id 已经位于排序键前缀,主键裁剪通常更有效;如果 user_id 在每个 part 中随机分布且几乎每块都有许多用户,Bloom Filter 也难以跳过。索引类型要和物理排序、列基数和查询谓词一起看。

验证流程

  1. 从 query log 选出高频且扫描量大的真实 SQL。
  2. 在代表性数据上创建索引并 materialize 到历史 part。
  3. 对比读取 rows/bytes、marks、CPU、延迟和写入/merge 开销。
  4. 验证参数变化、不同时间范围和热点值,不只测一个“最有利”查询。
  5. 没有稳定收益就删除索引,避免永久写放大。

执行计划和查询日志比“索引创建成功”更有证明力。某些索引类型只有在特定函数、运算符或常量表达式下才会被优化器利用。

与全文检索的边界

Bloom 或 ngram 跳数索引可改善特定过滤,但不替代 Elasticsearch 的倒排索引、词法分析、相关性评分和复杂检索能力。若需求是模糊文本搜索和排序,应评估专用搜索系统;若需求是在分析 SQL 中过滤少数 token,跳数索引可能足够。

维护成本

每增加一个跳数索引,INSERT、part merge、磁盘和备份都增加负担。索引表达式引用复杂函数还可能增加计算成本。建立索引前要明确它服务的查询集合、预期节省的读取量和何时下线。

设计与验证

先优化分区和排序键,再考虑跳数索引。用真实数据建立索引物化,比较读取行数、读取字节、CPU 与延迟;索引粒度过细会增加存储和判断成本。

容易踩坑的地方

高基数不代表 Bloom Filter 一定有效,关键是同一块是否能被排除。给每个过滤列都建索引会增加写入、存储和 Merge 成本,也可能完全没有收益。

常见问题

追问:为什么索引存在但查询没变快?

可能谓词不受该索引支持、数据分布无法裁剪、读取本来就少,或索引判断成本抵消收益,应查看查询日志和执行计划中的跳过情况。

追问:Bloom Filter 的假阳性会导致错误结果吗?

不会。假阳性只让本可跳过的块被继续读取,影响性能;假阴性才会漏数据,正确实现的 Bloom Filter 不应产生假阴性。

追问:可以对每列都建跳数索引吗?

不应该。大多数索引在真实分布下没有裁剪收益,却会持续增加写入和 merge 成本。优先优化表排序和主查询。

追问:新增索引后历史数据会自动有索引吗?

新写入 part 会携带索引,历史 part 通常需要显式 materialize 或在后续重写中生成,具体操作要评估线上 I/O 与时间窗口。