这是《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 KEY 与 ORDER 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 放排序键首位 | 日期查询无法利用前缀 |
| 所有查询共用一个排序 | 不同模式性能差异大 |
| 用分区替代排序键 | 细粒度过滤仍需大量扫描 |
本章小结
分区服务于数据生命周期和粗粒度裁剪,排序键服务于物理局部性和稀疏索引。先用时间分区控制范围,再按高频过滤条件设计排序前缀,最后用投影或汇总表覆盖差异化的查询模式。
思考题
- 分区和排序键各自减少什么成本?
- 为什么不建议按小时分区?
- 排序键前缀为什么重要?
- 什么场景适合 SAMPLE?
- 如何安全重构大表排序键?