跳到主要内容
跳到主要内容

VersionedCollapsingMergeTree

该引擎:

  • 允许快速写入状态不断变化的对象。
  • 在后台删除旧对象状态。这显著减少了存储量。

有关详细信息,请参见Collapsing部分。

该引擎继承自MergeTree,并向合并数据部分的算法中添加了合并行的逻辑。VersionedCollapsingMergeTree的目的与CollapsingMergeTree相同,但使用了不同的合并算法,允许在多个线程中以任意顺序插入数据。特别是,Version列有助于正确合并行,即使它们以错误的顺序插入。相比之下,CollapsingMergeTree只允许严格连续的插入。

创建表

有关查询参数的说明,请参见查询描述

引擎参数

参数描述类型
sign行类型列的名称:1是“状态”行,-1是“取消”行。Int8
version对象状态的版本列的名称。Int*, UInt*, Date, Date32, DateTimeDateTime64

查询子句

创建VersionedCollapsingMergeTree表时,所需的子句与创建MergeTree表时相同。

创建表的弃用方法
备注

在新项目中请勿使用此方法。如果可能,请将旧项目切换到上述描述的方法。

signversion外,所有参数在MergeTree中的含义相同。

  • sign — 行类型列的名称:1是“状态”行,-1是“取消”行。

    列数据类型 — Int8

  • version — 对象状态的版本列的名称。

    列数据类型应为UInt*

合并

数据

考虑需要为某个对象保存不断变化的数据的情况。为一个对象保留一行并在发生变化时更新该行是合理的。然而,对于数据库管理系统来说,更新操作是昂贵且缓慢的,因为它需要在存储中重写数据。如果需要快速写入数据,则更新不可接受,但您可以按顺序写入对象的更改,如下所示。

在写入行时使用Sign列。如果Sign = 1,则表示该行是对象的状态(我们称之为“状态行”)。如果Sign = -1,则表示取消具有相同属性的对象状态(我们称之为“取消行”)。还要使用Version列,该列应为每个对象状态标识一个单独的编号。

例如,我们想计算用户在某个网站上访问了多少页面以及他们在那里待了多长时间。在某个时间点上,我们写入以下用户活动状态的行:

在稍后的某个时刻,我们注册用户活动的变化并用以下两行写入它。

第一行取消了对象(用户)的先前状态。它应复制取消状态的所有字段,除了Sign

第二行包含当前状态。

因为我们只需要用户活动的最后状态,可以删除

来合并对象的无效(旧)状态。VersionedCollapsingMergeTree在合并数据部分时执行此操作。

要了解为什么每次更改需要两行,请参见算法

使用注意事项

  1. 写入数据的程序应该记住对象的状态,以便能够取消它。“取消”字符串应包含主键字段和“状态”字符串版本的副本,以及相反的Sign。这会增加存储的初始大小,但允许快速写入数据。
  2. 列中较长的增长数组会因写入负载而降低引擎的效率。数据越简单,效率越高。
  3. SELECT结果在很大程度上取决于对象状态变化历史的一致性。准备插入数据时要准确。使用不一致的数据可能会得到不可预测的结果,例如非负指标的负值,如会话深度。

算法

当ClickHouse合并数据部分时,它删除每对具有相同主键和版本但不同Sign的行。行的顺序无关紧要。

当ClickHouse插入数据时,它按主键对行进行排序。如果Version列不包含在主键中,ClickHouse会隐式地将其添加到主键中,作为最后一个字段并用于排序。

选择数据

ClickHouse不保证所有具有相同主键的行将在相同的结果数据部分中,甚至在同一物理服务器上。这对于写入数据和随后合并数据部分都是如此。此外,ClickHouse使用多个线程处理SELECT查询,并且无法预测结果中行的顺序。这意味着如果需要从VersionedCollapsingMergeTree表中获取完全“合并”的数据,则需要进行聚合。

要完成合并,请编写带有GROUP BY子句和考虑sign的聚合函数的查询。例如,要计算数量,请使用sum(Sign)而不是count()。要计算某些内容的总和,请使用sum(Sign * x)而不是sum(x),并添加HAVING sum(Sign) > 0

聚合函数countsumavg可以通过这种方式计算。如果对象至少有一个未合并的状态,则可以计算聚合函数uniq。聚合函数minmax无法计算,因为VersionedCollapsingMergeTree不保存合并状态的值的历史记录。

如果需要提取带有“合并”但不需要聚合的数据(例如,检查是否存在符合某些条件的行的最新值),可以在FROM子句中使用FINAL修饰符。这种方法效率低下,不应与大表一起使用。

使用示例

示例数据:

创建表:

插入数据:

我们使用两个INSERT查询创建两个不同的数据部分。如果我们使用单个查询插入数据,ClickHouse将创建一个数据部分,并且永远不会进行任何合并。

获取数据:

我们在这里看到什么,合并的部分在哪里? 我们使用两个INSERT查询创建了两个数据部分。SELECT查询在两个线程中执行,结果是行的随机顺序。 合并没有发生,因为数据部分还没有合并。ClickHouse在无法预测的时间点合并数据部分。

这就是我们需要聚合的原因:

如果我们不需要聚合并希望强制合并,可以在FROM子句中使用FINAL修饰符。

这是一种非常低效的选择数据的方法。不要将其用于大表。