跳转到主要内容

描述

CollapsingMergeTree 引擎继承自 MergeTree, 并增加了在合并过程中折叠行的逻辑。 如果一对行在排序键 (ORDER BY) 中的所有字段都相同,只有特殊字段 Sign 不同, 且该字段的值分别为 1-1CollapsingMergeTree 表引擎就会异步删除 (折叠) 这对行。 没有与之配对且 Sign 值相反的行则会被保留。 更多详情,请参阅本文档中的 折叠 部分。
该引擎可以显著减少存储量, 从而提高 SELECT 查询的效率。

参数

Sign 参数外,此表引擎的所有参数 其含义均与 MergeTree 中相同。
  • Sign — 用作表示行类型的列名,其中 1 表示“状态”行,-1 表示“取消行”。类型:Int8

创建表

  • 有关查询参数的说明,请参阅查询描述
  • 创建 CollapsingMergeTree 表时,与创建 MergeTree 表一样,也需要相同的查询子句

折叠

数据

考虑这样一种情况:你需要为某个对象保存持续变化的数据。 看起来合理的做法似乎是为每个对象只保留一行,并在每次发生变化时更新它, 但是更新操作对 DBMS 来说代价高、速度慢,因为它们需要重写存储中的数据。 如果我们需要快速写入数据,执行大量更新就不是可接受的方案, 但我们始终可以按顺序写入某个对象的变更。 为此,我们使用一个特殊的列 Sign
  • 如果 Sign = 1,表示该行是“状态行”:包含表示当前有效状态的各个字段的一行
  • 如果 Sign = -1,表示该行是“取消行”:用于抵消具有相同属性的对象状态的一行
例如,我们想计算用户在某个网站上查看了多少页面,以及他们在这些页面上停留了多长时间。 在某一时刻,我们写入以下这一行来表示用户活动的状态:
在稍后的某个时刻,我们记录下用户活动的变化,并将其写成以下两行:
第一行会抵消该对象 (此处表示一个用户) 的先前状态。 它应复制“被取消”行中除 Sign 之外的所有排序键字段。 上面的第二行表示当前状态。 由于我们只需要用户活动的最终状态,因此可以按如下所示删除原始“状态行”以及我们插入的“取消” 行,从而折叠对象的无效 (旧) 状态:
CollapsingMergeTree 会在数据分区片段合并期间执行这种折叠行为。
关于为什么每次变更都需要两行, 将在 算法 一节中进一步说明。
这种方法的特点
  1. 写入数据的程序必须记住对象的状态,才能将其取消。“取消行”应包含“状态行”中排序键字段的副本,以及相反的 Sign。这会增大初始存储空间占用,但能让数据写入更快。
  2. 列中持续增长的长数组会因写入负载增加而降低引擎效率。数据越简单,效率越高。
  3. SELECT 结果在很大程度上取决于对象变更历史的一致性。准备插入数据时务必谨慎;数据不一致时,结果可能不可预测。例如,像会话深度这类非负指标可能会出现负值。

算法

当 ClickHouse 合并数据 parts 时, 具有相同排序键 (ORDER BY) 的每组连续行都会被归并为不超过两行, 即 Sign = 1 的“状态行”和 Sign = -1 的“取消行”。 换句话说,在 ClickHouse 中,这些条目会发生折叠。 对于每个结果数据分区片段,ClickHouse 会保存: 此外,当“状态行”比“取消行”至少多两行, 或者“取消行”比“状态行”至少多两行时,合并会继续进行。 不过,ClickHouse 会将这种情况视为逻辑错误,并将其记录到服务器日志中。 如果同一份数据被多次 insert,就可能出现此错误。 因此,折叠不应改变统计信息的计算结果。 这些变更会逐步折叠,因此最终几乎每个对象都只会保留最后一个状态。 之所以需要 Sign 列,是因为合并算法无法保证 具有相同排序键的所有行都会出现在同一个结果数据分区片段中,甚至不一定在同一台物理服务器上。 ClickHouse 使用多个线程处理 SELECT 查询,因此无法预测结果中各行的顺序。 如果需要从 CollapsingMergeTree 表中获取完全“折叠”后的数据,则必须进行聚合。 要完成最终折叠,请编写带有 GROUP BY 子句和会考虑符号的聚合函数的查询。 例如,要计算数量,请使用 sum(Sign) 而不是 count()。 要计算某个值的总和,请使用 sum(Sign * x) 并结合 HAVING sum(Sign) > 0,而不是使用 sum(x), 如下方示例所示。 聚合 countsumavg 可以用这种方式计算。 如果某个对象至少有一个未折叠的状态,则聚合 uniq 也可以计算。 而聚合 minmax 则无法计算, 因为 CollapsingMergeTree 不会保存已折叠状态的历史记录。
如果你需要在不进行聚合的情况下提取数据 (例如,检查最新值满足特定条件的行是否存在) , 你可以在 FROM 子句中使用 FINAL 修饰符。它会在返回结果前先合并数据。 对于 CollapsingMergeTree,每个键只会返回最新的状态行。

示例

用法示例

给定以下示例数据:
我们来使用 CollapsingMergeTree 创建一个 UAct 表:
接下来,我们将插入一些数据:
我们使用两条 INSERT 查询来创建两个不同的数据分区片段。
如果通过单条查询插入数据,ClickHouse 只会创建一个数据分区片段,并且之后将不会再执行任何合并。
我们可以使用以下方式查询数据:
让我们来看一下上面返回的数据,看看是否发生了 折叠…… 通过两条 INSERT 查询,我们创建了两个数据分区片段。 SELECT 查询在两个线程中执行,因此得到的行顺序是随机的。 不过,折叠 并未发生,因为数据分区片段此时还没有发生 merge, 而 ClickHouse 会在后台的某个不可预知时间对数据分区片段进行 merge,我们无法提前预测。 因此,我们需要进行 聚合, 这可以通过 sum aggregate function 和 HAVING clause 来实现:
如果不需要聚合且希望强制进行折叠,也可以在 FROM 子句中使用 FINAL 修饰符。
这种数据选择方式效率较低,不建议在扫描大量数据 (数百万行) 时使用。

另一种方法的示例

这种方法的思路是,合并时只会考虑键字段。 因此,在 “取消行” 这一行中,我们可以指定负值, 这样在求和时就能抵消该行的前一个版本,而无需使用 Sign 列。 在此示例中,我们将使用下面的样本数据:
对于这种方法,需要将 PageViewsDuration 的数据类型更改为可存储负值。 因此,在使用 collapsingMergeTree 创建 UAct 表时,我们将这些列的数据类型从 UInt8 更改为 Int16
让我们通过向表中插入数据来验证这种方法。 不过,对于示例或小型表,这样做也是可以接受的:
最后修改于 2026年6月12日