跳至主要内容

CollapsingMergeTree

该引擎继承自 MergeTree 并在数据部分合并算法中添加了行折叠的逻辑。

CollapsingMergeTree 异步删除(折叠)成对的行,如果排序键(ORDER BY)中的所有字段都相等,除了特定字段 Sign,它可以具有 1-1 值。没有成对的行将被保留。有关更多详细信息,请参阅本文档的 折叠 部分。

该引擎可能会显着减少存储量并提高 SELECT 查询的效率。

创建表

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

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

CollapsingMergeTree 参数

sign

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

Column data type — `Int8`.

查询子句

在创建 CollapsingMergeTree 表时,与创建 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 [=] CollapsingMergeTree(date-column [, sampling_expression], (primary, key), index_granularity, sign)

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

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

    列数据类型 — Int8

折叠

数据

考虑这样一种情况,您需要为某个对象保存不断变化的数据。为某个对象拥有一行并在任何更改时更新它似乎是合乎逻辑的,但更新操作对于 DBMS 来说既昂贵又缓慢,因为它需要在存储中重写数据。如果您需要快速写入数据,则更新不可接受,但您可以按以下顺序写入对象的更改。

使用特定列 Sign。如果 Sign = 1,则表示该行是对象的“状态”,我们称之为“状态”行。如果 Sign = -1,则表示具有相同属性的对象状态的取消,我们称之为“取消”行。

例如,我们想计算用户在某个网站上检查了多少页面以及他们在那里停留了多长时间。在某个时刻,我们用以下行写入用户活动状态

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

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

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

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

第二行包含当前状态。

由于我们只需要用户活动的最后状态,因此行

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

可以删除折叠无效(旧)对象状态。CollapsingMergeTree 在合并数据部分时会执行此操作。

为什么我们需要在 算法 段落中读取每次更改的 2 行。

这种方法的特殊属性

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

算法

当 ClickHouse 合并数据部分时,每个具有相同排序键(ORDER BY)的连续行的组将减少到不超过两行,一行带有 Sign = 1(“状态”行)和另一行带有 Sign = -1(“取消”行)。换句话说,条目会折叠。

对于每个生成的 data part,ClickHouse 会保存

  1. 如果“状态”行和“取消”行的数量匹配且最后一行是“状态”行,则保存第一个“取消”行和最后一个“状态”行。
  2. 如果“状态”行比“取消”行多,则保存最后一个“状态”行。
  3. 如果“取消”行比“状态”行多,则保存第一个“取消”行。
  4. 在所有其他情况下,都不保存任何行。

此外,当“状态”行比“取消”行至少多 2 行,或者“取消”行比“状态”行至少多 2 行时,合并将继续,但 ClickHouse 将此情况视为逻辑错误并在服务器日志中记录它。如果多次插入相同的数据,则可能会发生此错误。

因此,折叠不应改变计算统计结果。更改逐渐折叠,以便最终仅保留几乎每个对象的最后状态。

需要 Sign,因为合并算法不保证具有相同排序键的所有行都将在同一个生成的数据部分中,甚至不保证在同一个物理服务器上。ClickHouse 使用多个线程处理 SELECT 查询,因此无法预测结果中行的顺序。如果需要从 CollapsingMergeTree 表获取完全“折叠”的数据,则需要聚合。

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

可以这样计算聚合 countsumavg。如果某个对象至少具有一个未折叠的状态,则可以计算聚合 uniq。无法计算聚合 minmax,因为 CollapsingMergeTree 不会保存折叠状态的值历史记录。

如果您需要提取未聚合的数据(例如,检查是否存在其最新值与某些条件匹配的行),则可以对 FROM 子句使用 FINAL 修饰符。这种方法的效率要低得多。

使用示例

示例数据

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

创建表

CREATE TABLE UAct
(
UserID UInt64,
PageViews UInt8,
Duration UInt8,
Sign Int8
)
ENGINE = CollapsingMergeTree(Sign)
ORDER BY UserID

插入数据

INSERT INTO UAct VALUES (4324182021466249494, 5, 146, 1)
INSERT INTO UAct VALUES (4324182021466249494, 5, 146, -1),(4324182021466249494, 6, 185, 1)

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

获取数据

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

我们看到了什么?折叠在哪里?

使用两个 INSERT 查询,我们创建了 2 个数据部分。SELECT 查询是在 2 个线程中执行的,我们得到了行的随机顺序。由于还没有合并数据部分,因此没有发生折叠。ClickHouse 会在未知的时间点合并 data part,我们无法预测。

因此,我们需要聚合

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

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

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

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

另一种方法的示例

示例数据

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

这个想法是合并只考虑键字段。在“取消”行中,我们可以指定等于先前版本的行的负值,以便在不使用 Sign 列的情况下求和。对于这种方法,需要更改 PageViewsDuration 的数据类型以存储 UInt8 的负值 ->Int16。

CREATE TABLE UAct
(
UserID UInt64,
PageViews Int16,
Duration Int16,
Sign Int8
)
ENGINE = CollapsingMergeTree(Sign)
ORDER BY UserID

让我们测试一下这种方法

insert into UAct values(4324182021466249494,  5,  146,  1);
insert into UAct values(4324182021466249494, -5, -146, -1);
insert into UAct values(4324182021466249494, 6, 185, 1);

select * from UAct final; // avoid using final in production (just for a test or small tables)
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │ 6 │ 185 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┘
SELECT
UserID,
sum(PageViews) AS PageViews,
sum(Duration) AS Duration
FROM UAct
GROUP BY UserID
┌──────────────UserID─┬─PageViews─┬─Duration─┐
│ 4324182021466249494 │ 6 │ 185 │
└─────────────────────┴───────────┴──────────┘
select count() FROM UAct
┌─count()─┐
│ 3 │
└─────────┘
optimize table UAct final;

select * FROM UAct
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │ 6 │ 185 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┘