面试考察点
- 是否理解跳数索引用于跳过数据块而非逐行定位。
- 能否根据列分布和查询谓词选择索引类型。
- 是否会用实际扫描量验证收益。
核心答案
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 与时间窗口。
总结
跳数索引的价值是证明“整块不需要读”,选型必须同时匹配谓词、局部数据分布和索引粒度。
机制全景图
下面把「ClickHouse 跳数索引有什么作用?」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。
flowchart LR
A["按 granule 生成索引摘要"]
A --> B["查询计算谓词范围"]
B --> C["索引判断可能命中"]
C --> D["跳过确定不匹配 granule"]
D --> E["读取剩余列并过滤"]
完整链路:从输入到结果
沿着「按 granule 生成索引摘要 → 查询计算谓词范围 → 索引判断可能命中 → 跳过确定不匹配 granule → 读取剩余列并过滤」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。
1. 按 granule 生成索引摘要
跳数索引在写入时按 GRANULARITY 记录 minmax、集合、Bloom 等摘要,不保存每行位置。
2. 查询计算谓词范围
查询条件必须被索引类型支持,函数和数据类型转换可能阻止使用。
3. 索引判断可能命中
索引只能证明一段数据“不可能命中”从而跳过,可能命中的 granule 仍需读取验证。
4. 跳过确定不匹配 granule
效果取决于列与 ORDER BY 的相关性、基数和数据聚集;完全随机高基数值可能让每段都“可能命中”。
5. 读取剩余列并过滤
索引增加写入计算、磁盘与合并成本,应通过 EXPLAIN indexes 和 read_rows 实测收益。
源码与实现定位
| 入口 | 阅读重点 |
|---|---|
| EXPLAIN indexes=1 | 跳数索引命中与 granule |
| system.data_skipping_indices | 索引定义 |
源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。
参数配置与可复现实验
ALTER TABLE events ADD INDEX idx_status status TYPE set(100) GRANULARITY 4;
构造有序/随机分布并比较 minmax、set、Bloom 的跳过率。
验证步骤与预期结果
1. 固定输入和基线
先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「granules dropped」为主基线,记录值应满足「记录稳态基线」;同时保存 索引命中过滤率、read_rows 前后差异,使后续变化能够回到同一时间轴比较。
2. 从实现入口确认路径
在「EXPLAIN indexes=1」确认请求确实进入「跳数索引命中与 granule」对应的实现,再沿「system.data_skipping_indices」观察「索引定义」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。
3. 注入本文特有的失败模式
优先复现「索引类型与谓词不匹配」,并把单一变量逐级放大,直到「granules dropped」越过「低于 10%」。随后再分别验证「未验证 read_rows 就判断索引有效」和「为大量列建索引拖慢写入和合并」,三类故障分开执行,避免多个变量同时变化而无法归因。
4. 执行止损和根因修复
第一轮只应用「谓词匹配索引类型」,确认它能控制影响范围;第二轮应用「无收益索引删除」,验证核心链路恢复;最后落实「优先优化 ORDER BY」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。
5. 通过退出条件
实验只有同时满足三项才算通过:「granules dropped」回到「记录稳态基线」、「granules dropped P99」回到「同负载可复现」、「结果核对」回到「差异为 0」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。
量化基线
| 指标 | 样例基线/口径 | 风险线 | 结论 |
|---|---|---|---|
| granules dropped | 记录稳态基线 | 低于 10% | 索引无收益 |
| granules dropped P99 | 同负载可复现 | 超过基线 2 倍 | 索引无收益 |
| 结果核对 | 差异为 0 | 任意非零 | 停止切换并修复 |
这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。
事故复盘:给随机 UUID 加 Bloom 索引仍未加速范围查询
Bloom 适合等值或集合包含,不支持大小范围,且 UUID 在每个 granule 中高度分散。调整查询模型、把可范围过滤的时间/租户放进排序键,比继续放大 Bloom 参数更有效。
| 失败模式 | 首要证据 | 第一处置动作 |
|---|---|---|
| 索引类型与谓词不匹配 | 索引命中过滤率 | 谓词匹配索引类型 |
| 未验证 read_rows 就判断索引有效 | read_rows 前后差异 | 无收益索引删除 |
| 为大量列建索引拖慢写入和合并 | 索引文件大小 | 优先优化 ORDER BY |
发布与回滚检查点
- 发布前:确认「EXPLAIN indexes=1」对应实现和上述配置在目标版本仍然有效,并保存「granules dropped」基线。
- 灰度中:同时观察 索引命中过滤率、read_rows 前后差异、索引文件大小;任一指标越过表中风险线,就停止继续扩量。
- 回滚时:先执行「谓词匹配索引类型」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
- 发布后:至少覆盖一个完整峰值周期,确认「索引类型与谓词不匹配」没有再次出现,才关闭变更观察窗口。
方案对比与选型
| 方案 | 更适合的场景 | 主要收益 | 代价与边界 |
|---|---|---|---|
| minmax | 数值/时间在数据中有聚集 | 摘要小、范围跳过有效 | 随机分布时几乎无收益 |
| set | 每 granule 低基数枚举 | 等值判断精确 | 基数超阈值会失效或膨胀 |
| Bloom filter | 字符串等值、token 或 ngram | 允许概率摘要高基数值 | 有假阳性且写入/存储成本较高 |
选型至少带上 日增量、分区规模、查询并发、扫描行数和压缩比,并用上面的量化基线验证;未知数据应明确为待测假设。
设计边界与工程取舍
跳数索引是主键排序的补充,不是行存二级索引;如果绝大多数 granule 都可能命中,它就无法减少读取。
工程落地遵循:以数据布局减少扫描,以批量写入减少小 Part,避免照搬行存思路。回答时直接引用「EXPLAIN indexes=1」、配置实验和事故数据,比复述固定模板更有说服力。