VersionedCollapsingMergeTree
此引擎
- 允许快速写入持续变化的对象状态。
- 在后台删除旧的对象状态。这大大减少了存储量。
有关详细信息,请参见折叠部分。
该引擎继承自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
sign
- 包含行类型的列的名称:1
是“状态”行,-1
是“取消”行。
The column data type should be `Int8`.
version
version
- 包含对象状态版本的列的名称。
The column data type should be `UInt*`.
查询子句
创建 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)
除 sign
和 version
之外的所有参数与 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
在合并数据部分时执行此操作。
要了解为什么每次更改需要两行,请参见算法。
使用说明
- 写入数据的程序应记住对象的状态以便能够取消它。“取消”字符串应包含主键字段的副本以及“状态”字符串的版本和相反的
Sign
。它增加了存储的初始大小,但允许快速写入数据。 - 列中的长增长数组由于写入负载而降低了引擎的效率。数据越简单,效率越高。
SELECT
结果很大程度上取决于对象更改历史记录的一致性。在准备要插入的数据时要准确。使用不一致的数据(例如,会话深度等非负指标的负值)可能会得到不可预测的结果。
算法
当 ClickHouse 合并数据部分时,它会删除每对具有相同主键和版本以及不同 Sign
的行。行的顺序无关紧要。
当 ClickHouse 插入数据时,它会按主键对行进行排序。如果 Version
列不在主键中,ClickHouse 会将其隐式添加到主键作为最后一个字段,并将其用于排序。
选择数据
ClickHouse 不保证所有具有相同主键的行都位于相同的结果数据部分中,甚至不在同一台物理服务器上。这对于写入数据和随后合并数据部分都是正确的。此外,ClickHouse 使用多个线程处理 SELECT
查询,并且无法预测结果中行的顺序。这意味着如果需要从 VersionedCollapsingMergeTree
表中获取完全“折叠”的数据,则需要聚合。
要完成折叠,请编写带有 GROUP BY
子句和考虑符号的聚合函数的查询。例如,要计算数量,请使用 sum(Sign)
而不是 count()
。要计算某物的总和,请使用 sum(Sign * x)
而不是 sum(x)
,并添加 HAVING sum(Sign) > 0
。
可以以此方式计算聚合 count
、sum
和 avg
。如果对象至少有一个未折叠的状态,则可以计算聚合 uniq
。无法计算聚合 min
和 max
,因为 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 │
└─────────────────────┴───────────┴──────────┴──────┴─────────┘
这是一种非常低效的选择数据的方式。不要将其用于大型表。