这是《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 剪枝,跳数索引只是补充。主索引解决物理顺序,跳数索引解决非排序键条件,投影解决不同排序和预聚合需求。索引设计要以高频查询和扫描量收益为依据。
思考题
- 为什么主索引是稀疏索引?
- bloom filter 为什么会误判?
- 新增跳数索引后历史数据如何生效?
- GRANULARITY 变大有什么影响?
- 跳数索引和投影如何取舍?