目录

ClickHouse 不让删数据?版本折叠合并树是怎么更新删除的

ClickHouse 查询快,但删除和更新一直是它的软肋。

VersionedCollapsingMergeTree 是官方给出的一个折中方案:不真删数据,而是再插一条「抵消行」,让后台合并时把新旧记录成对消掉。

我之前在一个用户状态表上踩过它的坑,SELECT * 出来的数据对不上,折腾了挺久才搞明白它的脾气。

这篇就把机制讲清楚。

它到底解决什么问题?

一句话:把删除和更新,变成纯插入操作。

ClickHouse 底层是列式存储,按数据块批量写。

它擅长追加和查询,但原地改一行、删一行的代价很高。

VersionedCollapsingMergeTree 的思路是不碰旧数据。

要删一条记录,就再写一条「除符号外完全相同」的抵消行。

要改一条记录,就先抵消旧的,再追加一条新的。

真正的物理删除交给后台合并进程慢慢做。

sign 和 version 不是自动生成的

这是很多人第一个误区。

signversion 是你自己在表里定义的两个普通列,引擎不会凭空给你加。

建表时通过引擎参数告诉 ClickHouse,哪一列当符号、哪一列当版本:

ENGINE = VersionedCollapsingMergeTree(sign, version)

两列的含义和类型如下:

作用 取值 / 类型
sign 标记行的类型 1 是状态行(state),-1 是取消行(cancel);类型 Int8
version 标记对象的状态版本 整数 / 日期 / 时间类型,每个状态用不同数字

写入规则记三条就够:

  • 写一个新状态,sign = 1
  • 取消旧状态,写一条 sign = -1 的取消行,这行要复制旧状态除 sign 外的所有字段,包括相同的 version
  • 更新后的新状态,sign = 1,version 递增

折叠规则:相同主键 + 相同 version + 符号相反

核心就一句话。

后台合并数据分片时,会把「主键相同、version 相同、sign 相反」的成对行删掉。

行的物理顺序无所谓,这点很关键。

看一组官方示例数据:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │         5 │      146 │    1 │       1 │  旧状态
│ 4324182021466249494 │         5 │      146 │   -1 │       1 │  取消旧状态
│ 4324182021466249494 │         6 │      185 │    1 │       2 │  新状态
└─────────────────────┴───────────┴──────────┴──────┴─────────┘

version=1 那两行符号相反,合并时被抵消。

version=2 的状态行保留下来,成为当前值。

那个 order by 不只是排序

原来我以为 ORDER BY 就是排个序,其实它定义的是表的主键,也就是折叠时「谁和谁算同一个对象」的依据。

ORDER BY 写了哪些列,折叠就按这些列加上 version 来配对。

还有个隐藏行为容易忽略:如果 version 列没写进主键,ClickHouse 会偷偷把它追加到主键最后一位参与排序。

所以 ORDER BY 选错列,折叠逻辑就全乱了。

和 CollapsingMergeTree 差在哪?

差别就在 version 这一列。

CollapsingMergeTree 折叠依赖插入顺序,状态行必须严格排在取消行前面,多线程或乱序插入就会折叠失败。

VersionedCollapsingMergeTree 靠 version 配对,插入顺序怎么乱都能正确折叠。

对比项 VersionedCollapsingMergeTree CollapsingMergeTree
折叠依据 version 列配对 严格的插入顺序
插入要求 多线程、任意顺序都行 必须按顺序写
乱序容错 仍能正确折叠 折叠失败

简单说,你的写入链路是异步的、并发的,就用带 version 的版本。

最大的坑:SELECT * 查出来的数据是错的

这是实战里最容易翻车的地方。

合并是后台异步做的,时机完全由 ClickHouse 决定,你不知道它什么时候合。

在合并发生前,抵消行和旧状态行都还在表里。

而且 ClickHouse 不保证相同主键的行落在同一个 part、甚至同一台机器上。

所以直接 SELECT *,大概率会看到没折叠的脏数据。

正确的查法有两种。

第一种,用 GROUP BY 配合 sign 做聚合,这是推荐做法:

SELECT
    UserID,
    sum(PageViews * Sign) AS PageViews,
    sum(Duration * Sign) AS Duration,
    Version
FROM UAct
GROUP BY UserID, Version
HAVING sum(Sign) > 0

计数用 sum(Sign) 代替 count(),求和用 sum(x * Sign) 代替 sum(x),再用 HAVING sum(Sign) > 0 把已经被抵消掉的对象过滤掉。

要注意 minmax 这类函数它不支持,因为引擎根本不保存历史状态的值。

第二种,加 FINAL

SELECT * FROM UAct FINAL

结果是对的,但 FINAL 会强制在查询时做合并,大表上极慢。

线上大表别用,老老实实 GROUP BY。

完整示例走一遍

建一张用户表:

CREATE TABLE UAct
(
    UserID    UInt64,
    PageViews UInt8,
    Duration  UInt8,
    Sign      Int8,
    Version   UInt8
)
ENGINE = VersionedCollapsingMergeTree(Sign, Version)
ORDER BY UserID;

写入一个初始状态:

INSERT INTO UAct VALUES (4324182021466249494, 5, 146, 1, 1);

后来这个用户数据要从 (5, 146) 改成 (6, 185),那就一次写两条:

INSERT INTO UAct VALUES
    (4324182021466249494, 5, 146, -1, 1),  -- 抵消旧状态,version 仍是 1
    (4324182021466249494, 6, 185,  1, 2);  -- 新状态,version 升到 2

合并之后,version=1 的两行消失,只剩 version=2 的当前值。

合并之前,就靠上面那条 GROUP BY 查询拿到正确结果。

常见问题

VersionedCollapsingMergeTree 适合频繁更新的场景吗?

适合「状态会变、但变更有明确版本」的数据,比如用户画像、订单状态。

它把更新转成插入,写入很快。

但代价是存储会膨胀,而且需要应用层记住旧状态才能构造抵消行。

如果是高频随机单点更新且不能记住旧值,它并不合适。

一次 INSERT 后数据会立刻折叠吗?

不会。

单次 INSERT 只生成一个 part,永远不触发合并。

至少要有两个 part 才可能合并,且合并时机由 ClickHouse 自行决定,不可预测。

所以千万别假设「插完就折叠好了」。

sign 取错值会怎样?

折叠会出错。

state 行写成 -1、cancel 行没复制对 version,都会导致该抵消的没抵消,查询结果出现负数之类的诡异值。

写入链路一定要保证符号和 version 的一致性。

它和 ReplacingMergeTree 怎么选?

ReplacingMergeTree 按主键去重保留最新行,适合「只要最新值、不关心抵消」的场景,用法更简单。

VersionedCollapsingMergeTree 适合需要精确加减、做增量聚合统计的场景。

只想去重就用前者,要算 sum、count 这类指标就用后者。

写在最后

VersionedCollapsingMergeTree 的本质,是用「插入 + 后台抵消」绕开 ClickHouse 不擅长的删改。

理解它只要抓两点:sign 和 version 是自己定义的、折叠按主键加 version 配对;查询永远别裸 SELECT *,用 GROUP BY 或 FINAL。

以上结论我只在自己的测试环境验证过,具体行为还是以你用的 ClickHouse 版本为准。

如果你在用它的过程中遇到折叠不生效、查询数据对不上的问题,欢迎在评论区一起聊聊~~~

参考资料

VersionedCollapsingMergeTree | ClickHouse Docs

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/clickhouse-versioned-collapsing-mergetree/

备用原文链接: https://blog.fiveyoboy.com/articles/clickhouse-versioned-collapsing-mergetree/