ClickHouseNotes

第 12 章:索引与跳数索引

zjc 于 2026-01-12 发布

这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 ClickHouse 的索引不是传统 B+Tree。主索引是排序键的稀疏索引,跳数索引则是在 granule 之上构建的辅助数据结构。

12.1 granule

MergeTree 每隔 index_granularity 行形成一个 granule:

rows 0 - 8191       granule 0
rows 8192 - 16383   granule 1
rows 16384 - 24575  granule 2

查询读取列时以 granule 为重要单位。即使只命中一行,也可能需要读取该行所在 granule 对应的列块。

12.2 主索引

CREATE TABLE analytics.events_local
(
    event_date Date,
    event_type LowCardinality(String),
    city_id UInt32,
    user_id UInt64,
    amount Decimal64(2)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, event_type, city_id, user_id);

主索引记录每个 granule 的排序键边界:

granule 0: (08-25, pay, 1, 100)
granule 1: (08-25, pay, 1, 900)
granule 2: (08-25, pay, 2, 120)

当条件命中排序键前缀时,可以跳过大量 granule。

12.3 跳数索引类型

minmax 适合范围查询:

ALTER TABLE analytics.events_local
ADD INDEX idx_amount_minmax amount TYPE minmax GRANULARITY 1;

set 适合少量离散值:

ALTER TABLE analytics.events_local
ADD INDEX idx_event_type event_type TYPE set(100) GRANULARITY 1;

bloom_filter 适合等值查询:

ALTER TABLE analytics.events_local
ADD INDEX idx_user_bloom user_id TYPE bloom_filter GRANULARITY 3;

ngrambf_v1 适合子串匹配:

ALTER TABLE analytics.logs_local
ADD INDEX idx_message_ngram message String TYPE ngrambf_v1(3, 10000, 3, 0)
GRANULARITY 1;

tokenbf_v1 适合 token 或词匹配:

ALTER TABLE analytics.logs_local
ADD INDEX idx_message_token message String TYPE tokenbf_v1(10000, 3, 0)
GRANULARITY 1;

12.4 GRANULARITY 含义

GRANULARITY 3 表示每 3 个主索引 granule 构建一个跳数索引块:

主 granule: 1 2 3 | 4 5 6 | 7 8 9
跳数索引:     A       B       C

跳数索引块命中时,块内所有主 granule 都可能被读取。粒度小更精准但索引更大,粒度大更省空间但跳过能力弱。

12.5 新旧数据生效

新增索引只影响新写入数据:

ALTER TABLE analytics.events_local
ADD INDEX idx_user_bloom user_id TYPE bloom_filter GRANULARITY 3;

历史数据需要物化:

ALTER TABLE analytics.events_local
MATERIALIZE INDEX idx_user_bloom;

查看任务:

SELECT database, table, mutation_id, command, is_done
FROM system.mutations
WHERE table = 'events_local';

12.6 索引代价

代价 说明
写入延迟 需要额外构建索引
磁盘空间 索引本身占空间
合并成本 合并时重建索引
误判 bloom 可能误判存在
维护复杂 过多索引难以治理

Bloom filter 只能判断“可能存在”或“一定不存在”,存在等值误判,因此命中后仍要读取数据判断。

12.7 索引选择

条件类型 首选
排序键前缀等值或范围 主索引
数值范围 minmax
少量枚举 set
高基数等值 bloom_filter
子串匹配 ngrambf_v1
token 匹配 tokenbf_v1
高频全文检索 考虑搜索引擎或专门索引

不要为了每个 WHERE 列都加索引。先看查询频率、扫描收益和维护成本。

12.8 与投影配合

同一张表可能有不同查询模式:

模式 A:按日期 + 城市 + 渠道聚合
模式 B:按用户查最近事件

排序键适合 A,B 可以用投影:

ALTER TABLE analytics.events_local
ADD PROJECTION proj_user_recent
(
    SELECT event_date, user_id, event_type, count(), max(event_time)
    GROUP BY event_date, user_id, event_type
);

投影可以改变存储排列,在这些场景常比额外跳数索引更有效。

12.9 排查索引效果

流程:

1. 记录 query_id
2. 查看 read_rows 和 read_bytes
3. 检查 WHERE 条件与排序键关系
4. 检查跳数索引是否物化
5. 对比索引调整前后的执行结果
6. 结合磁盘和 CPU 指标判断

强制关闭跳数索引的会话设置在不同版本中存在差异,使用前以当前版本文档为准。更稳妥的做法是保留调整前后的扫描量对比。

本章小结

ClickHouse 查询性能来自排序键、分区和 granule 剪枝,跳数索引只是补充。主索引解决物理顺序,跳数索引解决非排序键条件,投影解决不同排序和预聚合需求。索引设计要以高频查询和扫描量收益为依据。

思考题

  1. 为什么主索引是稀疏索引?
  2. bloom filter 为什么会误判?
  3. 新增跳数索引后历史数据如何生效?
  4. GRANULARITY 变大有什么影响?
  5. 跳数索引和投影如何取舍?