先说结论
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 也难以跳过。索引类型要和物理排序、列基数和查询谓词一起看。
验证流程
- 从 query log 选出高频且扫描量大的真实 SQL。
- 在代表性数据上创建索引并 materialize 到历史 part。
- 对比读取 rows/bytes、marks、CPU、延迟和写入/merge 开销。
- 验证参数变化、不同时间范围和热点值,不只测一个“最有利”查询。
- 没有稳定收益就删除索引,避免永久写放大。
执行计划和查询日志比“索引创建成功”更有证明力。某些索引类型只有在特定函数、运算符或常量表达式下才会被优化器利用。
与全文检索的边界
Bloom 或 ngram 跳数索引可改善特定过滤,但不替代 Elasticsearch 的倒排索引、词法分析、相关性评分和复杂检索能力。若需求是模糊文本搜索和排序,应评估专用搜索系统;若需求是在分析 SQL 中过滤少数 token,跳数索引可能足够。
维护成本
每增加一个跳数索引,INSERT、part merge、磁盘和备份都增加负担。索引表达式引用复杂函数还可能增加计算成本。建立索引前要明确它服务的查询集合、预期节省的读取量和何时下线。
设计与验证
先优化分区和排序键,再考虑跳数索引。用真实数据建立索引物化,比较读取行数、读取字节、CPU 与延迟;索引粒度过细会增加存储和判断成本。
容易踩坑的地方
高基数不代表 Bloom Filter 一定有效,关键是同一块是否能被排除。给每个过滤列都建索引会增加写入、存储和 Merge 成本,也可能完全没有收益。
常见问题
追问:为什么索引存在但查询没变快?
可能谓词不受该索引支持、数据分布无法裁剪、读取本来就少,或索引判断成本抵消收益,应查看查询日志和执行计划中的跳过情况。
追问:Bloom Filter 的假阳性会导致错误结果吗?
不会。假阳性只让本可跳过的块被继续读取,影响性能;假阴性才会漏数据,正确实现的 Bloom Filter 不应产生假阴性。
追问:可以对每列都建跳数索引吗?
不应该。大多数索引在真实分布下没有裁剪收益,却会持续增加写入和 merge 成本。优先优化表排序和主查询。
追问:新增索引后历史数据会自动有索引吗?
新写入 part 会携带索引,历史 part 通常需要显式 materialize 或在后续重写中生成,具体操作要评估线上 I/O 与时间窗口。