排序键与稀疏索引
ClickHouse 的 MergeTree 按 ORDER BY 指定的顺序把数据写入文件,并据此生成稀疏主键索引:默认每 8192 行构成一个 granule,索引只记录每个 granule 首行的键值。查询命中索引前缀时,引擎可以直接跳过大量 granule,只读相关部分。设计原则是把高频等值过滤、区分度高的列放在前面,把时间列放在其后用于范围裁剪;排序键改动需要新建表并重灌数据,属于一次性决策,上线前应当用真实查询验证。
基本信息
| 作用 | 决定数据物理顺序,并生成稀疏主键索引 |
|---|---|
| 顺序原则 | 高频过滤列在前,基数大且常用于范围的列在后 |
| 粒度 | index_granularity 默认 8192 行一个 granule |
| 主键 | PRIMARY KEY 可单独指定,但必须是 ORDER BY 的前缀 |
| 分区配合 | PARTITION BY 先切分数据,ORDER BY 在分区内排序 |
| 调整成本 | 改排序键需新建表并重灌数据 |
| 验证方式 | EXPLAIN indexes = 1 查看索引使用,或对比读取行数 |
典型使用场景
- 设备或门店的时间序列:ORDER BY (store_id, event_time),等值前缀加时间范围都能裁剪
- 按事件时间的范围查询为主:把时间列放在排序键首位,范围过滤直接命中索引
- 前缀匹配优化:只有排序键前缀列能被有效利用,因此最常用的过滤列必须放最前
- 压缩收益:相同前缀的数据物理相邻,压缩比明显好于随机顺序
- 查询形态多样时,用投影或物化视图为其他过滤组合单独建一份排序
排序键设计与验证
排序键设计与验证
CREATE TABLE dw.flow_event (
dt Date,
store_id String,
user_id String,
event_time DateTime,
event_type String
)
ENGINE = MergeTree
PARTITION BY dt -- 先按天分区
ORDER BY (store_id, event_time) -- 高频过滤列在前,时间列提供范围裁剪
SETTINGS index_granularity = 8192; -- 每个 granule 默认 8192 行
-- 验证索引是否生效:关注跳过的 granule 数量
EXPLAIN indexes = 1
SELECT count(*) FROM dw.flow_event
WHERE store_id = 'S1' AND event_time >= '2026-10-12 00:00:00';排序键写法对照
| 查询形态 | 排序键建议 | 原因 |
|---|---|---|
| 门店加时间范围 | (store_id, event_time) | 等值前缀加范围,两级都能裁剪 |
| 全量按时间范围 | (event_time) | 时间列在首位,范围过滤命中索引 |
| 按事件类型等值过滤 | (event_type, event_time) | 频繁过滤时有效,被跳过时前缀浪费 |
| 按用户维度查询 | (user_id, event_time) | 区分度高的列放最前 |
排序键是结构性决策。修改它要从头重灌数据,上线前用真实查询跑一遍 EXPLAIN 和耗时对比,避免上线后才发现顺序反了。
主键可以单独声明,但必须是排序键的前缀。两者不一致时,超出前缀部分的过滤条件享受不到索引裁剪,只能靠数据块扫描。