表引擎选择:查询性能的第一道分水岭
说实话,很多刚接触 ClickHouse 的朋友,上来就默认用 MergeTree。我一开始也这样。直到有一次线上查询慢得离谱,排查了半天,才发现问题出在引擎选型上。
表引擎,说白了就是数据的存储和计算策略。选对了,查询快得像本地数组遍历;选错了,再牛的优化技巧也救不了。今天咱们就把 MergeTree 家族这几个主力引擎掰开揉碎,看看它们到底适合什么场景。
核心观点:没有万能的引擎,只有最匹配业务的引擎。选型前,先想清楚你的数据是「只追加」还是「会更新」,查询是「求明细」还是「要聚合」。
1. MergeTree:最通用的基础引擎
MergeTree 是 ClickHouse 的基石。它支持按主键排序、分区、数据分片和副本复制。我个人的习惯是,只要业务没有特殊要求,先用 MergeTree 兜底。
适用场景:
- 日志数据、事件流数据,只追加不修改
- 需要按时间范围做快速过滤
- 查询以明细数据为主,偶尔做简单聚合
性能特点:
- 写入吞吐极高,单机每秒可写入数十万行
- 主键索引能大幅减少数据扫描量
- 不支持数据去重,同一主键会保留多行
我的经验:有一次做用户行为日志存储,每天几十亿条记录。用 MergeTree 配合 ORDER BY (event_time, user_id),按天分区。查询某用户某天的行为,秒级返回。如果换成 MySQL,同样的数据量得跑几分钟。
2. ReplacingMergeTree:去重利器
这个引擎在 MergeTree 基础上加了去重逻辑。合并分区时,它会保留每个主键的最后一条记录。嗯,这里要注意:去重发生在后台合并时,不是写入时。
适用场景:
- 需要更新状态的业务表,比如订单状态、用户信息
- 数据源会重复发送相同主键的记录
- 能接受短暂的数据重复(合并完成前)
我曾经踩过的坑:有次做实时订单宽表,用 ReplacingMergeTree 存订单状态。业务方要求查询必须精确去重。结果发现刚写入的数据还没合并,查出来有重复行。后来加了 FINAL 修饰符,但性能下降明显。最终方案是:写入时先做一次客户端去重,引擎只做兜底。
性能对比:
| 特性 | MergeTree | ReplacingMergeTree |
|---|---|---|
| 写入速度 | 极快 | 略慢(多版本标记) |
| 查询去重 | 不支持 | 支持(需合并或 FINAL) |
| 存储空间 | 最小 | 略大(保留历史版本) |
| 适用场景 | 日志、事件 | 状态表、维度表 |
3. SummingMergeTree:预聚合的加速器
这个引擎会在合并时,把相同主键的数值列自动求和。你想想看,如果每天有上亿条交易记录,但查询只需要按用户汇总金额,用 SummingMergeTree 能省掉大量实时计算。
适用场景:
- 需要按维度做累加统计,比如 PV/UV、交易金额
- 查询模式固定,不需要明细数据
- 对数据精度要求不极端(合并时可能丢失部分细节)
-- 创建 SummingMergeTree 表
CREATE TABLE daily_sales (
sale_date Date,
product_id UInt32,
amount Decimal(18,2),
quantity UInt32
) ENGINE = SummingMergeTree()
ORDER BY (sale_date, product_id);
-- 写入多条相同主键的数据
INSERT INTO daily_sales VALUES ('2024-01-01', 1001, 50.00, 2);
INSERT INTO daily_sales VALUES ('2024-01-01', 1001, 30.00, 1);
-- 合并后查询,amount 自动变成 80.00,quantity 变成 3
SELECT * FROM daily_sales FINAL;
性能提升:我在一个广告计费项目中,用 SummingMergeTree 替代了原来的 MergeTree + 实时聚合查询。查询耗时从 3 秒降到了 50 毫秒,存储空间也减少了 60%。代价是丢失了单次点击的明细数据——但业务方只需要日报,完全能接受。
4. AggregatingMergeTree:复杂聚合的终极方案
这个引擎更强大。它允许你自定义聚合函数,比如 uniqState(去重计数)、avgState(平均值)、quantileState(分位数)。合并时,它会保留聚合函数的中间状态,而不是最终结果。
适用场景:
- 需要精确去重计数(UV)
- 需要计算分位数、标准差等复杂统计
- 数据量大,但查询维度固定
-- 创建 AggregatingMergeTree 表
CREATE TABLE page_views_agg (
page_date Date,
page_url String,
uv AggregateFunction(uniq, String),
pv SimpleAggregateFunction(sum, UInt64)
) ENGINE = AggregatingMergeTree()
ORDER BY (page_date, page_url);
-- 写入时使用 -State 函数
INSERT INTO page_views_agg
SELECT
today(),
'/index.html',
uniqState(user_id),
sumState(1)
FROM page_views_raw
WHERE page_date = today();
我的建议:AggregatingMergeTree 适合做多层聚合的底层存储。比如先按小时聚合,再按天聚合。但要注意,查询时必须用对应的 -Merge 函数,否则拿不到结果。这个学习曲线稍微陡一点,但性能回报非常可观。
5. 引擎选型决策表
| 业务需求 | 推荐引擎 | 理由 |
|---|---|---|
| 存储原始日志,需要明细查询 | MergeTree | 写入快,查询灵活 |
| 订单表,需要按订单号去重 | ReplacingMergeTree | 支持主键去重 |
| 按天统计销售额,不需要明细 | SummingMergeTree | 自动预聚合,查询极快 |
| 需要精确 UV 和分位数 | AggregatingMergeTree | 支持复杂聚合函数 |
| 既有明细需求,又有聚合需求 | MergeTree + 物化视图 | 兼顾灵活性和性能 |
6. 性能差异的底层原因
为什么这些引擎性能差异这么大?说白了,就是「计算时机」不同。
- MergeTree:查询时计算,最灵活但最慢
- SummingMergeTree:合并时预计算,查询时直接读结果
- AggregatingMergeTree:合并时保留中间状态,查询时做最终聚合
- ReplacingMergeTree:合并时去重,但查询时可能还需要 FINAL
我习惯把 MergeTree 比作「生鲜超市」——什么都有,但每次都要现做。SummingMergeTree 就像「预制菜」——提前做好,热一下就能吃。AggregatingMergeTree 则是「半成品」——需要最后一步加工,但比从头做快得多。
重要提醒:不要为了追求性能盲目使用预聚合引擎。如果业务查询模式经常变化,预聚合的字段可能用不上,反而浪费存储和合并开销。我见过一个项目,用了 SummingMergeTree 但查询条件经常变,结果预聚合的字段根本用不上,查询还是全表扫描。
选引擎没有银弹。我的做法是:先用 MergeTree 跑原型,观察查询模式。等业务稳定了,再根据热点查询创建物化视图或迁移到专用引擎。这样既保证了灵活性,又能在需要时获得极致性能。