去重策略
去重指的是移除数据集中的重复行的过程。在 OLTP 数据库中,这很容易实现,因为每行都有唯一的键——但代价是插入速度较慢。每一行插入都需要先搜索,如果找到,则需要替换。
ClickHouse 在数据插入速度方面是为速度而设计的。存储文件是不可变的,并且 ClickHouse 在插入行之前不会检查是否存在主键——因此去重需要付出更多的努力。这也意味着去重不是立即发生的——它是最终一致的,这会产生一些副作用
- 在任何时候,您的表仍然可能存在重复项(具有相同排序键的行)
- 实际的重复行删除发生在 parts 合并期间
- 您的查询需要考虑到可能存在重复项
| ClickHouse 提供关于去重以及许多其他主题的免费培训。 删除和更新数据培训模块 是一个好的起点。 |
去重选项
ClickHouse 中使用以下表引擎实现去重
-
ReplacingMergeTree表引擎:使用此表引擎,在合并期间会删除具有相同排序键的重复行。ReplacingMergeTree是模拟 upsert 行为(您希望查询返回最后插入的行)的一个不错的选择。 -
折叠行:
CollapsingMergeTree和VersionedCollapsingMergeTree表引擎使用一种逻辑,即“取消”现有行并插入新行。与ReplacingMergeTree相比,它们实现起来更复杂,但您的查询和聚合可以更简单地编写,而无需担心数据是否已合并。当您需要频繁更新数据时,这两个表引擎很有用。
我们将在下面介绍这两种技术。有关更多详细信息,请查看我们的免费按需 删除和更新数据培训模块。
使用 ReplacingMergeTree 进行 Upsert
让我们看一个简单的例子,其中一个表包含 Hacker News 评论,其中 views 列表示评论被查看的次数。假设文章发布时插入一行,并在每天更新一次总浏览量(如果值增加)
让我们插入两行
要更新 views 列,插入具有相同主键的新行(请注意 views 列的新值)
表现在有 4 行
输出中上面的单独框演示了幕后的两个 parts——此数据尚未合并,因此重复行尚未删除。让我们在 SELECT 查询中使用 FINAL 关键字,这将导致查询结果的逻辑合并
结果只有 2 行,并且插入的最后一行是返回的行。
如果您的数据量很小,使用 FINAL 还可以。如果您处理大量数据,使用 FINAL 可能不是最佳选择。让我们讨论一个更好的选项来查找列的最新值。
避免 FINAL
让我们再次更新 views 列,用于两个唯一的行
表现在有 6 行,因为尚未发生实际合并(仅在使用了 FINAL 时的查询时间合并)
与其使用 FINAL,不如使用一些业务逻辑——我们知道 views 列始终在增加,因此我们可以使用 max 函数在所需列上分组后选择具有最大值的行
如上所示的分组实际上可能更有效(就查询性能而言),而不是使用 FINAL 关键字。
我们的 删除和更新数据培训模块 扩展了此示例,包括如何使用 version 列与 ReplacingMergeTree。
使用 CollapsingMergeTree 频繁更新列
更新列涉及删除现有行并用新值替换它。如您所见,ClickHouse 中这种类型的更改是最终一致的——在合并期间发生。如果您有很多行要更新,实际上避免 ALTER TABLE..UPDATE 而只是将新数据与现有数据一起插入可能更有效。我们可以添加一列,指示数据是陈旧还是新的……实际上,有一个表引擎已经很好地实现了这种行为,特别是考虑到它可以自动删除陈旧数据。
假设我们使用外部系统跟踪 Hacker News 评论的浏览次数,并且每隔几个小时,我们将数据推送到 ClickHouse。我们希望删除旧行,并且新行代表每个 Hacker News 评论的新状态。我们可以使用 CollapsingMergeTree 来实现此行为。
让我们定义一个表来存储浏览次数
请注意,hackernews_views 表有一个名为 sign 的 Int8 列,称为 sign 列。sign 列的名称是任意的,但 Int8 数据类型是必需的,并且请注意将列名传递给 CollapsingMergeTree 表的构造函数。
CollapsingMergeTree 表的 sign 列是什么?它代表行的状态,sign 列只能是 1 或 -1。以下是如何工作的
- 如果两行具有相同的主键(或者如果主键与排序键不同,则具有相同的排序顺序),但 sign 列的值不同,则最后插入的带有 +1 的行将成为状态行,而其他行将相互取消
- 相互取消的行在合并期间会被删除
- 没有匹配对的行将被保留
让我们将一行添加到 hackernews_views 表中。由于它是此主键的唯一行,我们将其状态设置为 1
现在假设我们想更改 views 列。您插入两行:一行取消现有行,另一行包含行的新状态
表现在有 3 行,主键为 (123, 'ricardo')
请注意,添加 FINAL 会返回当前状态行
但是,当然,不建议对大型表使用 FINAL。
在我们的示例中传递给 views 列的值实际上并不需要,也不必与旧行的 views 的当前值匹配。事实上,您可以用主键和 -1 来取消一行
来自多个线程的实时更新
使用 CollapsingMergeTree 表,行使用 sign 列相互取消,行的状态由最后插入的行确定。但是,如果您从不同的线程插入行,而行的插入顺序被打乱,这可能会有问题。在这种情况下,使用“最后”一行不起作用。
这就是 VersionedCollapsingMergeTree 有用的地方——它像 CollapsingMergeTree 一样折叠行,但不是保留最后插入的行,而是保留具有您指定的版本列的最高值的行。
让我们看一个例子。假设我们想跟踪 Hacker News 评论的浏览次数,并且数据经常更新。我们希望报告使用最新的值,而无需强制或等待合并。我们从一个类似于 CollapsedMergeTree 的表开始,除了我们添加一列来存储行状态的版本
请注意,该表使用 VersionsedCollapsingMergeTree 作为引擎,并传递了 sign 列 和 version 列。以下是表的工作方式
- 它删除具有相同主键和版本以及不同 sign 的每一对行
- 插入行的顺序无关紧要
- 请注意,如果版本列不是主键的一部分,ClickHouse 会将其隐式地作为最后一个字段添加到主键中
您在编写查询时使用相同的逻辑——按主键分组,并使用巧妙的逻辑来避免已被取消但尚未删除的行。让我们向 hackernews_views_vcmt 表添加一些行
现在我们更新两行并删除其中一行。要取消一行,请确保包含先前的版本号(因为它也是主键的一部分)
我们将运行与之前相同的查询,该查询巧妙地根据 sign 列加减值
结果是两行
让我们强制进行表合并
结果中应该只有两行
当您想在从多个客户端和/或线程插入行时实现去重时,VersionedCollapsingMergeTree 表非常有用。
为什么我的行没有被去重?
插入的行未被去重的一个原因是,您在 INSERT 语句中使用了一个非幂等函数或表达式。例如,如果您插入具有列 createdAt DateTime64(3) DEFAULT now() 的行,您的行保证是唯一的,因为每行都会为 createdAt 列生成一个唯一的默认值。MergeTree / ReplicatedMergeTree 表引擎不会知道要对行进行去重,因为每个插入的行都会生成唯一的校验和。
在这种情况下,您可以为每一批行指定自己的 insert_deduplication_token,以确保同一批行的多次插入不会导致同一行被重新插入。请参阅 关于 insert_deduplication_token 的文档,了解有关如何使用此设置的更多详细信息。