- 在任何时刻,你的表中都可能仍然存在重复项 (具有相同排序键的行)
- 重复行的实际删除发生在 parts 合并期间
- 你的查询需要考虑到可能存在重复项
去重方案
-
ReplacingMergeTree表引擎:使用这种表引擎时,具有相同排序键的重复行会在合并过程中被移除。ReplacingMergeTree非常适合模拟 upsert 行为 (即希望查询返回最后插入的那一行) 。 -
行折叠:
CollapsingMergeTree和VersionedCollapsingMergeTree表引擎采用这样一种逻辑:先将现有行“抵消”,再插入新行。与ReplacingMergeTree相比,它们实现起来更复杂,但查询和聚合写起来会更简单,因为无需关心数据是否已经完成合并。这两种表引擎在需要频繁更新数据时尤其有用。
使用 ReplacingMergeTree 实现 Upsert
views 列表示某条评论的查看次数。假设我们在文章发布时插入一行新数据,并在该值增加时每天通过 upsert 写入一行包含总查看次数的新数据:
views 列,请插入一条主键相同的新行 (注意 views 列的新值) :
SELECT 查询中使用 FINAL 关键字,这会对查询结果进行逻辑合并:
如果数据量较小,使用
FINAL 还算可行。但如果处理的是大量数据,
FINAL 可能就不是最佳选择了。下面我们来看看一种更好的方法,
用于找出某一列的最新值。避免使用 FINAL
views 列:
FINAL 时才会在查询阶段进行合并) 。
FINAL,不如利用一些业务逻辑——我们知道 views 列始终是递增的,因此按所需列分组后,可以使用 max 函数选出值最大的那一行:
FINAL 关键字。
我们的 Deleting and Updating Data 培训模块 对这个示例做了更深入的讲解,包括如何将 version 列与 ReplacingMergeTree 搭配使用。
使用 CollapsingMergeTree 频繁更新列
ALTER TABLE..UPDATE,直接在现有数据旁边插入新数据反而可能更高效。我们可以添加一列,用来标记数据是已过时还是最新……实际上,已经有一种表引擎很好地实现了这种行为,尤其是它还会自动删除过时数据。下面来看看它的工作方式。
假设我们通过一个外部系统跟踪 Hacker News 评论的浏览次数,并且每隔几小时就将数据推送到 ClickHouse。我们希望旧行被删除,而新行则表示每条 Hacker News 评论的最新状态。我们可以使用 CollapsingMergeTree 来实现这种行为。
下面定义一个表来存储浏览次数:
hackernews_views 表中有一个名为 sign 的 Int8 列,也就是 sign 列。sign 列的名称可以任意指定,但数据类型必须是 Int8。另外请注意,这个列名会传递给 CollapsingMergeTree 表的构造函数。
CollapsingMergeTree 表的 sign 列是什么?它表示该行的 状态,并且 sign 列的值只能是 1 或 -1。其工作方式如下:
- 如果两行具有相同的主键 (如果排序顺序与主键不同,则按排序顺序) ,但 sign 列的值不同,那么最后插入的、值为 +1 的那一行会成为状态行,其他行则会相互抵消
- 相互抵消的行会在 merge 期间被删除
- 没有匹配对的行会被保留
hackernews_views 表添加一行。由于这是该主键下唯一的一行,我们将其状态设为 1:
views 列。你需要插入两行:一行用于冲销现有行,另一行包含该行的新状态:
(123, 'ricardo'):
FINAL 后会返回当前的状态行:
FINAL。
在我们的示例中,传入
views 列的值实际上并不是必需的,也不必与旧行中 views 的当前值一致。事实上,只提供主键和一个 -1 就能抵消一行:多个线程的实时更新
CollapsingMergeTree 表,行会通过 sign 列相互抵消,而一行的状态由最后插入的那一行决定。但是,如果你从不同线程插入行,而这些行可能会乱序写入,就会出现问题。在这种情况下,依赖“最后”一行是行不通的。
这时 VersionedCollapsingMergeTree 就派上用场了——它会像 CollapsingMergeTree 一样折叠行,但它保留的不是最后插入的行,而是你指定的版本列中值最大的那一行。
我们来看一个示例。假设我们要跟踪 Hacker News 评论的浏览次数,并且数据会频繁更新。我们希望报表使用最新值,而不必强制执行或等待合并。我们从一个类似于 CollapsedMergeTree 的表开始,不同之处在于我们增加了一列,用来存储该行状态的版本:
VersionsedCollapsingMergeTree 作为引擎,并传入了sign 列和版本列。下面是该表的工作方式:
- 它会删除每一对主键和版本相同、但 sign 不同的行
- 行被插入的顺序并不重要
- 请注意,如果版本列不是主键的一部分,ClickHouse 会将其隐式添加到主键中,作为最后一个字段
hackernews_views_vcmt 表中添加一些行:
VersionedCollapsingMergeTree 表就非常实用。
为什么我的行没有被去重?
INSERT 语句中使用了非幂等函数或表达式。比如,如果插入的行包含列定义 createdAt DateTime64(3) DEFAULT now(),那么这些行必然各不相同,因为每一行的 createdAt 列都会有一个唯一的默认值。由于每次插入的行都会生成唯一的校验和,MergeTree / ReplicatedMergeTree 表引擎无法判断这些行应被去重。
在这种情况下,你可以为每个批次的行指定自己的 insert_deduplication_token,以确保对同一批次进行多次插入时,不会导致相同的行被重复插入。有关如何使用此设置的更多信息,请参阅 insert_deduplication_token 文档。