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

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包含对象状态版本的列名。Int*, UInt*, Date, Date32, DateTimeDateTime64

查询子句

创建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 │
└─────────────────────┴───────────┴──────────┴──────┴─────────┘

这是一种选择数据的非常低效的方法。请勿在大表上使用它。