ClickHouse 的 MergeTree 系列引擎是支撑海量数据分析的核心存储结构。当业务不再满足于只做追加式明细存储,而是希望减少重复数据、提前汇总指标时,就会在 ReplacingMergeTree、SummingMergeTree 与 AggregatingMergeTree 之间做权衡。它们并非相互替代,而是分别解决了不同维度的问题。

一、ReplacingMergeTree:按版本保留最新记录
ReplacingMergeTree 的设计目标是处理“相同主键对应多版本”的场景。比如在电商订单系统中,一个订单会因支付、发货、退款产生多条状态变更记录。如果直接写入普通 MergeTree,查询时会出现重复订单。ReplacingMergeTree 要求在建表时指定一个版本列(或时间戳列),后台在合并分区数据块时,会对相同排序键的记录只保留版本最大的一条。
需要注意的是,这种去重发生在后台 merge 阶段,而不是写入时。因此,在 merge 还没有执行的时间窗口内,用户仍然可能查到多条相同主键的记录。通常需要在查询层使用 FINAL 修饰符,或者借助视图做最新态补全。下面给出一个典型建表语句:
CREATE TABLE order_state
(
order_id UInt64,
status String,
update_time DateTime,
amount Decimal(10,2)
)
ENGINE = ReplacingMergeTree(update_time)
PARTITION BY toYYYYMM(update_time)
ORDER BY order_id;
上述表中,order_id 是排序键,update_time 作为版本列。当后台合并时,相同 order_id 的多条数据仅保留 update_time 最大的行。如果业务要求查询强一致的最新态,可以写 SELECT * FROM order_state FINAL,但 FINAL 会在查询时做额外归并,有一定性能开销。
从适用边界看,ReplacingMergeTree 不适合高频更新同一主键且要求毫秒级可见的场景,因为 merge 节奏由系统控制。它更擅长缓慢变化维、配置表同步、以及允许最终一致性的状态类数据。
二、SummingMergeTree:数值列的自动累加
当分析需求以“按维度求和”为主,例如统计每个城市的日销售额,而原始写入是逐笔交易时,SummingMergeTree 可以在后台合并时将指定数值列自动相加,从而减少查询时的实时计算量。建表时需通过 ORDER BY 定义维度列,并明确哪些列需要被求和。
与 ReplacingMergeTree 类似,求和也只在 merge 时发生。未合并的数据块中,相同维度仍然有多行。查询时如果直接 SUM(amount),ClickHouse 会对已合并的聚合行和未合并的明细行一起求和,最终结果正确,但扫描行数可能多于预期。示例如下:
CREATE TABLE city_sales
(
city String,
day Date,
sales Decimal(12,2),
cnt UInt32
)
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(day)
ORDER BY (city, day);
在上面的结构中,city 与 day 组成排序键,sales 和 cnt 会在合并时分别累加。如果某天北京写入了三笔记录,后台 merge 后可能变成一行 (北京, 当天, 总销售额, 总笔数)。这能明显提升大时间范围汇总的效率。
但 SummingMergeTree 不会处理非数值列。如果排序键之外还有其它字符串字段,合并时系统会随机保留其中一行的值,因此不能依赖它做去重或最新值替换。它专用于“只关心合计、不关心明细差异”的指标的预计算。
三、AggregatingMergeTree:聚合中间状态的物化
AggregatingMergeTree 是三者中表达能力最强的一类。它不直接存最终结果,而是存储聚合函数的中间状态,例如 uniqState、sumState、avgState。配合物化视图,可以在数据写入明细表时,自动将聚合状态落到聚合表,查询时使用对应的 State 与 Merge 函数还原。
这种方式特别适合多维分析平台:前端可以按任意维度组合查询,而后台只需维护一份聚合表。因为中间状态可继续合并,所以即使数据分多批写入,最终聚合结果依然准确。典型用法如下:
CREATE TABLE uv_detail
(
day Date,
site String,
user_id UInt64
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(day)
ORDER BY (day, site, user_id);
CREATE TABLE uv_agg
(
day Date,
site String,
uv AggregateFunction(uniq, UInt64)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(day)
ORDER BY (day, site);
CREATE MATERIALIZED VIEW uv_mv
TO uv_agg
AS SELECT
day,
site,
uniqState(user_id) AS uv
FROM uv_detail
GROUP BY day, site;
写入 uv_detail 后,物化视图会把每批数据的 uniqState 写入 uv_agg。查询时通过 uniqMerge(uv) 得到去重用户数。由于 AggregateFunction 类型支持再次合并,后台 merge 不会破坏精度。
相比在查询时直接 uniq(user_id),AggregatingMergeTree 把大量计算前置到写入和 merge 阶段,使大屏类高并发查询更加平稳。代价是物化视图逻辑较复杂,且聚合维度相对固定。
四、选型对比与建议
为了更直观地理解三者差异,可以从数据语义、合并行为和查询特点三个角度对比:
| 引擎类型 | 核心语义 | 合并时动作 | 典型场景 |
|---|---|---|---|
| ReplacingMergeTree | 保留主键最新版本 | 同排序键留版本最大行 | 订单状态、配置同步 |
| SummingMergeTree | 数值列求和 | 同排序键数值累加 | 销售额、点击量汇总 |
| AggregatingMergeTree | 存聚合中间状态 | 同排序键状态合并 | UV、多维指标预计算 |
实践中,一个数仓往往组合使用:原始层用 MergeTree 存明细,中间层用 SummingMergeTree 或 AggregatingMergeTree 做轻度汇总,维度表用 ReplacingMergeTree 保证最新。切忌把 ReplacingMergeTree 当作实时更新数据库使用,也避免用 SummingMergeTree 承载需要保留明细的文本字段。
另外,所有衍生引擎都依赖后台 merge,因此分区粒度、写入批大小和 merge 线程配置都会影响去重与聚合的可见延迟。建议在测试环境观察 system.parts 中活跃数据块数量,再决定分区策略。
五、常见误区提醒
不少团队误以为使用了 ReplacingMergeTree 就无需在查询时处理重复,结果在仪表盘上看到翻倍指标。根本原因是忽略了 FINAL 与未合并 parts 的存在。若查询频率高且对重复零容忍,应在服务层封装带 FINAL 的查询,或定期 optimize 分区(仅适合小表)。
还有人用 SummingMergeTree 存储用户维度信息,期望合并后留下想要的描述字段,实际却得到随机值,引发数据质量投诉。牢记:SummingMergeTree 只保证数值列语义,非数值列合并行为未定义。正确做法是将维度属性放维度表,用 ReplacingMergeTree 管理,事实表只保留外键与度量。
总体来看,选型的关键不是“哪个更高级”,而是“数据在业务里如何被消费”。先画出读写路径与查询模式,再让引擎特性去匹配,才能发挥 ClickHouse 的极致性能。
ClickHouseMergeTreeReplacingMergeTree修改时间:2026-08-05 05:12:39