CoalescingMergeTree:列级合并,一行成景

白发任 笔记 59

CoalescingMergeTree 引擎通过追加写的方式,为 clickhouse 带来了真正的列级更新能力。作为 MergeTree 家族的新成员,它能够在后台合并时,把稀疏的局部记录逐步聚合成完整行,非常适合以下场景:

  • 需要高效保留每个实体的最完整版本

  • 可接受后台合并时才落盘整合的延迟

  • 只想填补缺失字段,而非像 ReplacingMergeTree 那样整行覆盖

CoalescingMergeTree 首发于 25.6 版本,25.7.2.54 已具备完整更新能力。

一、实现原理

1.1 八股文版

与其它 MergeTree 引擎一样,需要先通过 ORDER BY 声明排序键(主键)。CoalescingMergeTree 会把相同排序键的多条记录做合并,规则:每列保留最新的非 NULL 值,最终在磁盘上得到一行“完整”数据。

CoalescingMergeTree:列级合并,一行成景-第1张图片-多梦笔记

其中 1- 4 为数据 insert 过程,对于同一个 vin 每次插入局部数据

  1. 初始化 vin 的电池电量和防火墙信息

  2. 更新 GPS 数据(本质上是插入追加操作)

  3. 更新速度和温度数据

  4. 更新电池电量
    其中的 5 - 6 为 MergeTree 的局部合并, 7 则将若干个 part 合并成一个 active part 并进行落盘,其中的合并逻辑为:取每列最新的非 null 数据。

    tip: 这里的最新指的是行插入的顺序,并非依据排序列或者人为指定的列。

1.2 解剖版:用 SQL 讲清楚

如果上面仍显抽象,下面用最简 sql 演示其本质。
首先是对“最新”的解释:可以理解成 clickhouse 为每一行都分配一个默认值为 now() 的虚拟插入时间。
建表(演示用,非 CoalescingMergeTree)


SQL
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;


追加式更新


SQL
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:列级合并,一行成景-第2张图片-多梦笔记

CoalescingMergeTree 的合并逻辑等价于


SQL
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)


SQL
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 中都有一张表


SQL
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 构建数据管道

每个分域表只负责追加写入,宽表通过物化视图自动合并


SQL
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 演示更新

分三批将三个主题域的数据写入主题表中,每一批写入之后都可以查询一下宽表


SQL
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 关键字保证数据的一致性


SQL
select * from user_profile_wide final;


后续如果想要更新某个字段的值只需要向对应字段插入数据即可


SQL
insert into tblxxx(id, colxxx) values(1001, valuexxx);


三、注意事项

因为 CoalescingMergeTree 判断字段不更新的逻辑是 null,因此 DDL 中除了排序键外所有的字段必须要是 Nullable 修饰的数据类型,如果所有字段类型都不使用 Nullable 修饰则与 ReplacingMergeTree 功能一致,即:整行替换
那么与 ReplacingMergeTree 的区别如下:

维度CoalescingMergeTreeReplacingMergeTree
设计目的逐列合并,减少行数,节省存储多记录去重,保留最新一行
合并粒度列级行级
null 值处理表示未更新,合并时被非 null 覆盖与普通值一样不做特殊处理
是否丢列不会丢失任何已写入的列值未出现在新行里的列值会丢失

其次,一定要注意数据乱序对 CoalescingMergeTree 的影响特别是在高频数据场景中,因为 CoalescingMergeTree 的局部更新逻辑依赖物理插入顺序,建议:

  • 同一实体短时间内避免多批次并行写入

  • 统一 ETL 作业,显式控制写入顺序


分享到:

标签: CoalescingMergeTree 引擎 SQL