CoalescingMergeTree 引擎通过追加写的方式,为 clickhouse 带来了真正的列级更新能力。作为 MergeTree 家族的新成员,它能够在后台合并时,把稀疏的局部记录逐步聚合成完整行,非常适合以下场景:
需要高效保留每个实体的最完整版本
可接受后台合并时才落盘整合的延迟
只想填补缺失字段,而非像 ReplacingMergeTree 那样整行覆盖
CoalescingMergeTree 首发于 25.6 版本,25.7.2.54 已具备完整更新能力。
一、实现原理
1.1 八股文版
与其它 MergeTree 引擎一样,需要先通过 ORDER BY 声明排序键(主键)。CoalescingMergeTree 会把相同排序键的多条记录做合并,规则:每列保留最新的非 NULL 值,最终在磁盘上得到一行“完整”数据。

其中 1- 4 为数据 insert 过程,对于同一个 vin 每次插入局部数据
初始化 vin 的电池电量和防火墙信息
更新 GPS 数据(本质上是插入追加操作)
更新速度和温度数据
更新电池电量
其中的 5 - 6 为 MergeTree 的局部合并, 7 则将若干个 part 合并成一个 active part 并进行落盘,其中的合并逻辑为:取每列最新的非 null 数据。tip: 这里的最新指的是行插入的顺序,并非依据排序列或者人为指定的列。
1.2 解剖版:用 SQL 讲清楚
如果上面仍显抽象,下面用最简 sql 演示其本质。
首先是对“最新”的解释:可以理解成 clickhouse 为每一行都分配一个默认值为 now() 的虚拟插入时间。
建表(演示用,非 CoalescingMergeTree)
create table demo ( id UInt8 comment '主键 ID', name Nullable(String) comment '姓名', age Nullable(UInt8) comment '年龄', _insert_time DateTime64 materialized now() comment '插入时间' ) engine MergeTree order by id; |
追加式更新
insert into table demo(id, name, age) values (1, '张三', 20); -- 初始化 id: 1 数据 insert into table demo(id, age) values (1, 21); -- 更新年龄 insert into table demo(id, name) values (1, '张一三'); -- 更新姓名 |
明细数据

CoalescingMergeTree 的合并逻辑等价于
select id, argMax(name, _insert_time) as name, argMax(age, _insert_time) as age from demo group by id; |
若把该语句封装成物化视图,即可模拟 CoalescingMergeTree;而 CoalescingMergeTree 只是把这个过程内嵌到了 MergeTree 的合并阶段。
二、实战
CoalescingMergeTree 特别适合局部更新场景,例如官网给出的 Tesla 在车联网上的应用。我们知道 clickhouse 在超大宽表的查询上性能极高,但超大宽表中每个数据域的就绪时间往往是不同的,企业中为了让 clickhouse 中宽表的可用时间尽可能长采用局部分批更新策略,使用 CoalescingMergeTree 将会变得更加方便
2.1 构建环境
宽表:
user_profile_wide(CoalescingMergeTree)分域表:
user_profile_basic / behavior / business(MergeTree)
create table user_profile_wide
(
-- 主键 & 分区键
user_id UInt64,
-- 1️⃣ 基础属性域(低频更新,每日)
gender Nullable(Enum8('M' = 1, 'F' = 2)),
age Nullable(UInt8),
city_level Nullable(UInt8),
register_channel Nullable(String),
-- 2️⃣ 行为偏好域(中频更新,小时级)
last30d_pv Nullable(UInt32),
last30d_uv Nullable(UInt32),
fav_category Nullable(String),
last_login_time Nullable(DateTime64),
-- 3️⃣ 业务价值域(高频更新,分钟级)
ltv_7d Nullable(Float64),
ltv_30d Nullable(Float64),
churn_prob Nullable(Float32),
VIP_level Nullable(UInt8)
)
engine = CoalescingMergeTree
order by (user_id); |
为了方便进行局部数据更新,以及对不同主题域 ETL 任务的拆分,每个域的数据在 clickhouse 中都有一张表
create table user_profile_basic
(
-- 主键 & 分区键
user_id UInt64,
-- 1️⃣ 基础属性域
gender Nullable(Enum8('M' = 1, 'F' = 2)),
age Nullable(UInt8),
city_level Nullable(UInt8),
register_channel Nullable(String)
) engine = MergeTree
order by (user_id);
create table user_profile_behavior
(
-- 主键 & 分区键
user_id UInt64,
-- 2️⃣ 行为偏好域
last30d_pv Nullable(UInt32),
last30d_uv Nullable(UInt32),
fav_category Nullable(String),
last_login_time Nullable(DateTime64)
) engine = MergeTree
order by (user_id);
create table user_profile_business
(
-- 主键 & 分区键
user_id UInt64,
-- 3️⃣ 业务价值域
ltv_7d Nullable(Float64),
ltv_30d Nullable(Float64),
churn_prob Nullable(Float32),
vip_level Nullable(UInt8)
) engine = MergeTree
order by (user_id); |
2.2 构建数据管道
每个分域表只负责追加写入,宽表通过物化视图自动合并
create materialized view mv_user_profile_basic to user_profile_wide as select user_id, gender, age, city_level, register_channel from user_profile_basic; create materialized view mv_user_profile_behavior to user_profile_wide as select user_id, last30d_pv, last30d_uv, fav_category, last_login_time from user_profile_behavior; create materialized view mv_user_profile_business to user_profile_wide as select user_id, ltv_7d, ltv_30d, churn_prob, vip_level from user_profile_business; |
至此!一个借助 CoalescingMergeTree 实现的大宽表局部更新数据管道就构建好了
2.3 演示更新
分三批将三个主题域的数据写入主题表中,每一批写入之后都可以查询一下宽表
insert into user_profile_basic values (1001, 1, 23, 1, 'ios'); insert into user_profile_behavior values (1001, 123003, 21, '橘子', now64()); insert into user_profile_business values (1001, 10, 20, 1.2, '1'); |
因为更新操作是发生在 MergeTree 的合并过程中,通常在查询时需要加上 final 关键字保证数据的一致性
select * from user_profile_wide final; |
后续如果想要更新某个字段的值只需要向对应字段插入数据即可
insert into tblxxx(id, colxxx) values(1001, valuexxx); |
三、注意事项
因为 CoalescingMergeTree 判断字段不更新的逻辑是 null,因此 DDL 中除了排序键外所有的字段必须要是 Nullable 修饰的数据类型,如果所有字段类型都不使用 Nullable 修饰则与 ReplacingMergeTree 功能一致,即:整行替换
那么与 ReplacingMergeTree 的区别如下:
| 维度 | CoalescingMergeTree | ReplacingMergeTree |
|---|---|---|
| 设计目的 | 逐列合并,减少行数,节省存储 | 多记录去重,保留最新一行 |
| 合并粒度 | 列级 | 行级 |
| null 值处理 | 表示未更新,合并时被非 null 覆盖 | 与普通值一样不做特殊处理 |
| 是否丢列 | 不会丢失任何已写入的列值 | 未出现在新行里的列值会丢失 |
其次,一定要注意数据乱序对 CoalescingMergeTree 的影响特别是在高频数据场景中,因为 CoalescingMergeTree 的局部更新逻辑依赖物理插入顺序,建议:
同一实体短时间内避免多批次并行写入
统一 ETL 作业,显式控制写入顺序
标签: CoalescingMergeTree 引擎 SQL