下面给你一份 ClickHouse 表设计/使用的“生产最佳实践清单” (偏通用、落地的那种),按你建表和跑业务最容易踩坑的点来讲。
1) 选对表引擎:99% 用 MergeTree 家族
明细事实表(日志/订单/曝光)
- ✅
MergeTree/ReplicatedMergeTree - 分布式集群上:本地表用
ReplicatedMergeTree,查询用Distributed
聚合结果表(日报/分钟级看板)
- ✅
SummingMergeTree:适合sum/count这类可加和指标 - ✅
AggregatingMergeTree:适合uniqExact/quantile/...聚合状态(用xxxState/xxxMerge) - ✅
ReplacingMergeTree:去重/最新状态(带 version 更稳)
别用普通
View当加速器;要加速就用 物化视图 + 聚合表。
2) 分区(PARTITION BY):用于“管理数据”,不是为了加速查询
最佳实践:
- 大多数场景:按日期分区
PARTITION BY toYYYYMM(dt)或toDate(dt) - 分区不要太细(别按小时/分钟分区,除非有强需求)
- 分区数量建议:每个表几百以内更稳(长期运行)
查询加速主要靠 ORDER BY(排序键) ,不是分区。
3) 排序键(ORDER BY):决定性能的核心
设计原则
- 把最常用过滤条件放前面
- 再放高基数字段(如 user_id、ad_id)用于范围定位
- 再放时间用于同 key 内的时序扫描
举例(广告曝光日志):
ORDER BY (dt, adx, campaign_id, user_id, ts)
常见坑
ORDER BY (ts)单时间排序:范围查时间可以,但按业务维度过滤会很痛- 把低基数字段放太后:过滤会扫很多 granule
4) 主键/索引:理解 CK 的“稀疏索引”模型
- CK 的 primary key 本质是 基于 ORDER BY 的稀疏索引
- 不是 MySQL 那种逐行 B+Tree
所以:排序键必须贴合查询模式
另外:
index_granularity默认一般够用,别乱改- 真要加速特定过滤:用数据跳过索引(skip index),比如
bloom_filter/set/minmax(谨慎、先压测)
5) 数据类型:越“贴近真实”越快越省
推荐
- 计数/金额:
UInt32/UInt64、金额有小数用Decimal(18, x) - 枚举/小范围:
LowCardinality(String)(对低基数维度很香) - 时间:
DateTime64(3)(毫秒) - 可空:尽量少用
Nullable(会慢,会占空间)
反例
- 所有字段都
String - 随处
Nullable(String) - 用
Float存钱
6) TTL & 数据生命周期:强烈建议上
例如保留 30 天明细,自动删:
TTL dt + INTERVAL 30 DAY DELETE
冷热分层(有多卷/多磁盘时):
TTL dt + INTERVAL 7 DAY TO VOLUME 'cold'
7) 写入(INSERT)最佳实践:批量、顺序、避免小文件
- ✅ 批量写:单批至少几千~几万行(看行大小)
- ✅ 尽量按排序键大致有序写(能减少 merge 压力)
- ❌ 避免超高频小批次写(会把 merges 打爆)
日志场景常见组合:Kafka -> Materialized View -> MergeTree
8) 分布式表:本地表存数据,Distributed 只负责查询路由
推荐结构:
xxx_local(ReplicatedMergeTree)存数据xxx(Distributed)对外查询- MV 建在
xxx_local上,不要建在Distributed上
9) 聚合表 + 物化视图:报表/看板的最优解
- 明细表负责吞吐
- MV 在写入时做聚合
- 聚合表负责低延迟查询
这是 CK 在生产里最“值回票价”的用法。
10) 生产上最常见的 5 个坑
ORDER BY不贴合查询,导致扫全表- 分区太细,parts 爆炸,merge 压力巨大
- 小批次写入过多,导致后台合并常年 100%
- 所有字段 String/Nullable,存储大、CPU 重
- 在 MV 里 JOIN 维表 / 基于 Distributed 建 MV(分布式下更容易炸)