跳到主要内容
跳到主要内容
编辑此页

VersionedCollapsingMergeTree

此引擎

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

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

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

创建表

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
...
) ENGINE = VersionedCollapsingMergeTree(sign, version)
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]

有关查询参数的描述,请参阅查询描述

引擎参数

VersionedCollapsingMergeTree(sign, version)
参数描述类型
sign具有行类型的列名:1 是“状态”行,-1 是“取消”行。Int8
version具有对象状态版本的列名。UInt*DateDate32DateTimeDateTime64

查询子句

创建 VersionedCollapsingMergeTree 表时,与创建 MergeTree 表时一样,需要相同的子句

创建表的已弃用方法
注意

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

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
...
) ENGINE [=] VersionedCollapsingMergeTree(date-column [, samp#table_engines_versionedcollapsingmergetreeling_expression], (primary, key), index_granularity, sign, version)

除了 signversion 之外的所有参数都与 MergeTree 中的含义相同。

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

    列数据类型 — Int8

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

    列数据类型应为 UInt*

折叠

数据

考虑这样一种情况,您需要保存某个对象的持续变化的数据。为一个对象设置一行并在发生更改时更新该行是合理的。但是,对于 DBMS 来说,更新操作是昂贵且缓慢的,因为它需要重写存储中的数据。如果您需要快速写入数据,则更新是不可接受的,但您可以按如下方式顺序写入对象的更改。

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

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

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ 1 │ 1 |
└─────────────────────┴───────────┴──────────┴──────┴─────────┘

稍后,我们注册用户活动的更改,并使用以下两行写入它。

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ -1 │ 1 |
│ 4324182021466249494 │ 6 │ 185 │ 1 │ 2 |
└─────────────────────┴───────────┴──────────┴──────┴─────────┘

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

第二行包含当前状态。

因为我们只需要用户活动的最新状态,所以行

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ 1 │ 1 |
│ 4324182021466249494 │ 5 │ 146 │ -1 │ 1 |
└─────────────────────┴───────────┴──────────┴──────┴─────────┘

可以被删除,从而折叠对象的无效(旧)状态。VersionedCollapsingMergeTree 在合并数据部分时执行此操作。

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

使用注意事项

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

算法

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

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

选择数据

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

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

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

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

使用示例

示例数据

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ 1 │ 1 |
│ 4324182021466249494 │ 5 │ 146 │ -1 │ 1 |
│ 4324182021466249494 │ 6 │ 185 │ 1 │ 2 |
└─────────────────────┴───────────┴──────────┴──────┴─────────┘

创建表

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)
INSERT INTO UAct VALUES (4324182021466249494, 5, 146, -1, 1),(4324182021466249494, 6, 185, 1, 2)

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

获取数据

SELECT * FROM UAct
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ 1 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┴─────────┘
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ -1 │ 1 │
│ 4324182021466249494 │ 6 │ 185 │ 1 │ 2 │
└─────────────────────┴───────────┴──────────┴──────┴─────────┘

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

这就是为什么我们需要聚合

SELECT
UserID,
sum(PageViews * Sign) AS PageViews,
sum(Duration * Sign) AS Duration,
Version
FROM UAct
GROUP BY UserID, Version
HAVING sum(Sign) > 0
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Version─┐
│ 4324182021466249494 │ 6 │ 185 │ 2 │
└─────────────────────┴───────────┴──────────┴─────────┘

如果我们不需要聚合并且想要强制折叠,我们可以对 FROM 子句使用 FINAL 修饰符。

SELECT * FROM UAct FINAL
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 6 │ 185 │ 1 │ 2 │
└─────────────────────┴───────────┴──────────┴──────┴─────────┘

这是一种非常低效的数据选择方式。不要在大型表中使用它。