首页/数据/数仓建模速查/ClickHouse 排序键与稀疏索引
湖仓与引擎63 条

ClickHouse 排序键与稀疏索引

ORDER BY 决定数据物理顺序并生成稀疏主键索引:把高频过滤列放前面、基数列放后面,范围过滤才能命中索引减少扫描。

数仓建模速查数据更新于 2026-10-13

排序键与稀疏索引

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 和耗时对比,避免上线后才发现顺序反了。
主键可以单独声明,但必须是排序键的前缀。两者不一致时,超出前缀部分的过滤条件享受不到索引裁剪,只能靠数据块扫描。

易错点

  • 排序键把低基数列放最前:索引区分度低,过滤效果差,扫描量接近全表
  • 只按排序键非前缀列过滤:索引完全无法裁剪,查询性能与无索引无异
  • 上线后频繁调整排序键:每次都需新建表并重灌数据,成本极高
  • 排序键与主要查询形态不匹配:靠 FINAL 或投影硬撑,延迟长期居高不下
  • 忽略分区与排序键的配合:分区列没进过滤条件,先要扫描全部活动分区

数据来源与参考

  • ClickHouse 官方文档:Sparse Primary Indexes
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌