ClickHouseNotes

第 11 章:分区与排序键

zjc 于 2026-01-11 发布

这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 分区和排序键是 ClickHouse 表设计中最容易出错的两件事。分区负责生命周期和粗粒度剪枝,排序键决定 part 内数据物理顺序和稀疏索引能力。

维度 PARTITION BY ORDER BY
作用范围 一组数据 part 内每行顺序
主要目标 过期删除、分区裁剪 稀疏索引、局部性
粒度
常见依据 时间 查询过滤维度
过多后果 part 和目录过多 写入排序成本高

一句话概括:分区解决“哪段时间可以不看或删除”,排序键解决“哪些 granule 可以跳过”。

11.1 分区设计

按月:

PARTITION BY toYYYYMM(event_date)

按天:

PARTITION BY toDate(event_time)

组合分区:

PARTITION BY (toYYYYMM(event_date), platform)

组合分区会显著放大分区数量,除非生命周期和隔离需求明确,否则不建议。

11.2 分区选择流程

1. 数据保留多久?
2. 每天写入多少行、多少字节?
3. 常用查询时间范围是多少?
4. 是否需要按天快速删除?
5. 合并压力是否可控?

参考:

场景 建议
事件保留 30 天,日写入 TB 级 按天
事件保留 1 年,日写入百 GB 内 按月
小表 不分区
只按城市隔离 谨慎,数据倾斜时部分分区过大

11.3 排序键设计

示例:

ORDER BY (event_date, event_type, city_id, channel, user_id)

前缀查询有效:

WHERE event_date = '2026-08-25'

WHERE event_date = '2026-08-25' AND event_type = 'pay'

WHERE event_date BETWEEN '2026-08-01' AND '2026-08-31'
  AND event_type = 'pay'
  AND city_id = 1

跳跃前缀效果弱:

WHERE city_id = 1

WHERE user_id = 1001

这不是不能查,而是无法充分利用稀疏索引。

11.4 排序键设计步骤

1. 收集 top 慢查询和高频查询
2. 提取必带条件
3. 统计过滤列基数
4. 低基数等值列放前
5. 时间范围列尽量靠前
6. 高基数用户 ID 通常放后
7. 不同查询模式用投影补齐

常见排序:

业务 可选排序
行为事件 日期,事件类型,城市,用户
监控指标 日期,指标 ID,机器
日志检索 日期,服务,主机,级别
订单分析 日期,状态,城市,商家
用户画像 更新日期,用户 ID

11.5 主键与排序键

ClickHouse 允许 PRIMARY KEYORDER BY 不同,但主键必须是排序键前缀。多数表只设置 ORDER BY 即可。

ENGINE = MergeTree
ORDER BY (event_date, city_id, user_id)
PRIMARY KEY (event_date, city_id)

不必要地定义 PRIMARY KEY 容易让使用者误解为 OLTP 唯一键。业务建模时建议避免这种混淆。

11.6 采样键

大扫描探索分析可使用采样:

CREATE TABLE analytics.events_sampled
(
    event_date Date,
    user_id UInt64,
    amount Decimal64(2)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id)
SAMPLE BY user_id;

查询:

SELECT count(), sum(amount)
FROM analytics.events_sampled
SAMPLE 0.1
WHERE event_date = today();

采样必须显式指定 SAMPLE BY。报表和计费类结果不能默认使用采样。

11.7 分区操作

查看分区:

SELECT table, partition, sum(rows) AS rows, sum(bytes_on_disk) AS bytes
FROM system.parts
WHERE database = 'analytics' AND active
GROUP BY table, partition
ORDER BY bytes DESC;

删除分区:

ALTER TABLE analytics.events_local DROP PARTITION '202608';

冻结备份:

ALTER TABLE analytics.events_local FREEZE PARTITION '202608';

挂载分区:

ALTER TABLE analytics.events_local
ATTACH PARTITION '202608';

11.8 修改排序键

排序键不能像普通列一样随意修改。常见做法:

1. 新建目标表
2. 分批 INSERT SELECT
3. 校验行数和指标
4. 重命名切换表

示例:

RENAME TABLE analytics.events_local TO analytics.events_old;
RENAME TABLE analytics.events_new TO analytics.events_local;

切换分布式表或高可用环境时,要考虑写入停止窗口、任务暂停和元数据同步。

11.9 反模式

反模式 后果
每小时一个分区 part 数量失控
高基数城市、用户组合做分区 分区过多且倾斜
排序键过长 写入和索引成本增加
user_id 放排序键首位 日期查询无法利用前缀
所有查询共用一个排序 不同模式性能差异大
用分区替代排序键 细粒度过滤仍需大量扫描

本章小结

分区服务于数据生命周期和粗粒度裁剪,排序键服务于物理局部性和稀疏索引。先用时间分区控制范围,再按高频过滤条件设计排序前缀,最后用投影或汇总表覆盖差异化的查询模式。

思考题

  1. 分区和排序键各自减少什么成本?
  2. 为什么不建议按小时分区?
  3. 排序键前缀为什么重要?
  4. 什么场景适合 SAMPLE?
  5. 如何安全重构大表排序键?