TT Lab
开始
学习 学习路径 课程

ClickHouse — 从内部理解列式分析数据库

SummingMergeTree 与 AggregatingMergeTree — 由合并延续聚合

在 TT Lab 中继续学习

一句话总结

这两种引擎的做法是:排序键相同的行在合并时被折叠成一行,同时把值合并起来。可以用求和压缩的值(条数、合计),SummingMergeTree 直接相加;无法用求和压缩的值(不同用户数、平均值),AggregatingMergeTree 存放的不是数字,而是聚合的中间状态。合并什么时候结束是不确定的,所以查询始终要用 GROUP BY 加 sum()、-Merge 来收尾。

为什么需要它

假设仪表板要显示“各站点的每日访问量”。如果原始事件每天有几亿行,每次打开页面都把原始数据扫一遍,就是浪费。答案明明每天只有几百行就够了。所以要预先汇总。问题是数据还在不断进来。今天上午的汇总放进去了,下午的数据来了,就得找到汇总行去修改——可是正如前面的模块所见,MergeTree 的数据片段不能修改。

如果说 ReplacingMergeTree 是“追加新版本,合并时丢弃旧的”,那么这两种引擎就是“追加新的片段,合并时相加”。下午的汇总只要再多写一行,键相同的行就会在合并中合成一行。写入仍然只有追加。

工作原理

api.example 一天的数据通过三次 INSERT 进入。在 SummingMergeTree 中,访问量 2194、2198、2213 三行,经过合并或 sum() 变成 6605。在 AggregatingMergeTree 中,三行存放的是用户集合这种状态,如果各自计算结束再相加,结果是 5322 人,会把重叠的人算两次,而用 uniqExactMerge 合并之后再计算结束,得到的是与原始数据相同的 3671 人。这个状态还可以用 uniqExactMergeState 按日期再次合并

SummingMergeTree——参考文档中的规则很简短。把排序键相同的行,替换成一行把数值类型列的值相加的行。如果不在引擎参数中写出要相加的列,就会把排序键之外的所有数值列都相加。要相加的列全部变成 0 的行会被删除。不相加的列,会从已有的值中随便留下一个。

这个“所有数值列”就是陷阱。在实验服务器上,向 (k, a Int32, mx UInt32) 表分别写入 (1, 5, 10) 和 (1, −5, 20),合并之后得到 (1, 0, 30)。本来当作最大值写入的 mx 被相加成了 30,而且即使 a 变成了 0,mx 不为 0,所以这一行仍然留着。如果需要最大值,要用 AggregatingMergeTree 的 SimpleAggregateFunction(max, UInt32) 列(同样的两行被合并成了 20)。

把实验数据(100 万行,5 个站点 × 30 天 = 150 个键)按时间段分三次写入,并停止合并,汇总表就是 450 行。文档之所以说“求和可能不完整,查询时要用 sum() 和 GROUP BY”,就是因为这种状态。SELECT * 会为每个键返回三行,而用 GROUP BY site, day 重新相加,无论是否合并,结果都与原始数据完全一致。

AggregatingMergeTree——不同用户数是无法相加的。因为上午来的 1,772 人和下午来的 1,771 人之间,有相同的人。所以存放的不是数字,而是状态。用组合器文档的说法,-State 返回的不是结果值,而是“聚合的中间状态(uniq 的话是哈希表)”,-Merge 则把各个状态合并,得出结果值。

users AggregateFunction(uniqExact, UInt64)      -- 열 타입: 어떤 함수의 상태인가
INSERT ... SELECT uniqExactState(user_id) ...    -- 넣을 때 -State
SELECT uniqExactMerge(users) ... GROUP BY ...    -- 읽을 때 -Merge

-If 用来附加条件(countIfState(is_bot = 0) 的类型是 AggregateFunction(countIf, UInt8))。平均值也以状态保存——avgState 里有总和与个数,合并之后再相除,与原始平均值完全一致。状态不是给人读的值,所以如果以 JSON 取出,会打印出二进制字节。

最大的优点是状态可以再次合并。-MergeState 会把状态合并之后,返回的不是结果值,而是状态。可以把各站点的每日状态折叠成每日状态,在没有原始数据的情况下,求出只保留数字就不可能得到的“合并各站点的每日访客”。

在现场相遇的样子

最常见的 bug 是把每行各自得到的结果数字相加。在实验中,把合并之前的 450 个状态行分别用 finalizeAggregation 算出结果再相加,是 807,524;按键合并之后再算出结果并相加,是 552,634。如果仪表板上的“每日用户合计”大得异常,就要怀疑这个错误。平均值也一样——平均值的平均值不是平均值。

第二种是 SummingMergeTree 里混进了不该汇总的数值列(最大值、标识符、比率)。文档建议把完整的原始数据放在 MergeTree 里,SummingMergeTree 只用于汇总。这是为了即使排序键选错了,也能从原始数据重新生成。

第三种是由谁来填充这张表。实验中是手动运行三次 INSERT ... SELECT,而在现场,通常与物化视图配对,每当源数据收到 INSERT,就自动写入汇总。这就是下一个模块。还有一点——如果一个 INSERT 里同一个键出现多次,写入的那一刻就会被合并(optimize_on_insert)。把 100 万行 hits = 1 一次写入,立刻就变成了 150 行。

下一项实验要做什么

向源表 agg.hits 写入 100 万行,把(site, day)汇总按时间段分三次写入 SummingMergeTree,确认合并之前的行数。核对 GROUP BY + sum() 查询在合并之前是否也与源数据一致,再亲手合并。接着把不同用户数、平均值、只统计真人的访问量用 -State 存入 AggregatingMergeTree,用 -Merge 计算结束,并记录把每行各自算出的结果相加会错多少。最后用 -MergeState 把每日状态按日期再次合并。