跳转到主要内容
由于 ClickHouse 的数据存储结构和复制机制,从 Postgres 复制到 ClickHouse 的更新和删除操作会在 ClickHouse 中产生重复行。本文将介绍其原因,以及在 ClickHouse 中处理重复数据的策略。

数据如何复制?

PostgreSQL 逻辑解码

ClickPipes 使用 Postgres Logical Decoding 来实时捕获 Postgres 中发生的变更。Postgres 中的 Logical Decoding 过程使 ClickPipes 这类客户端能够以便于人类阅读的格式接收这些变更,也就是一系列 INSERT、UPDATE 和 DELETE。

ReplacingMergeTree

ClickPipes 使用 ReplacingMergeTree 引擎将 Postgres 表映射到 ClickHouse。ClickHouse 在仅追加型工作负载下表现最佳,因此不建议频繁执行 UPDATE。这正是 ReplacingMergeTree 特别适用的场景。 使用 ReplacingMergeTree 时,更新会表示为插入该行的一个较新版本 (_peerdb_version) ;而删除则表示为插入该行的一个较新版本,并将 _peerdb_is_deleted 标记为 true。ReplacingMergeTree 引擎会在后台对数据进行去重和合并,并为给定主键 (id) 保留该行的最新版本,从而能够以版本化插入的方式高效处理 UPDATE 和 DELETE。 下面是 ClickPipes 在 ClickHouse 中创建表时执行的 CREATE TABLE 语句示例。

示例说明

下图通过一个基础示例说明了如何使用 ClickPipes 在 PostgreSQL 和 ClickHouse 之间同步 users 表。 步骤 1 展示了 PostgreSQL 中 2 行数据的初始快照,以及 ClickPipes 将这 2 行数据初始加载到 ClickHouse 的过程。可以看到,这两行数据都会原样复制到 ClickHouse。 步骤 2 展示了对 users 表执行的三个操作:插入一行新数据、更新一行现有数据,以及删除另一行数据。 步骤 3 展示了 ClickPipes 如何将 INSERT、UPDATE 和 DELETE 操作以带版本的插入方式复制到 ClickHouse。UPDATE 会表现为 ID 2 对应行的一个新版本,而 DELETE 会表现为 ID 1 的一个新版本,并通过 _is_deleted 标记为 true。因此,与 PostgreSQL 相比,ClickHouse 中会多出三行数据。 因此,运行类似 SELECT count(*) FROM users; 这样的简单查询时,ClickHouse 和 PostgreSQL 中的结果可能会不同。根据 ClickHouse merge 文档,过期的行版本最终会在合并过程中被丢弃。但是,合并发生的时机无法预测,这意味着在合并完成之前,ClickHouse 中的查询可能会返回不一致的结果。 如何确保 ClickHouse 和 PostgreSQL 中的查询结果完全一致?

使用 FINAL 关键字去重

在 ClickHouse 查询中,推荐的数据去重方式是使用 FINAL 修饰符。 这样可确保只返回去重后的行。 下面来看看如何将它应用到三种不同的查询中。 请注意以下查询中的 WHERE 子句,它用于过滤掉已删除的行。
  • 简单计数查询:统计 posts 的数量。
这是你可以运行的最简单查询,用于检查同步是否已顺利完成。这两个查询应返回相同的计数结果。
  • 使用 JOIN 的简单聚合:累计浏览量最高的前 10 位用户。
这是一个针对单表进行聚合的示例。如果这里存在重复数据,会严重影响 sum 函数的结果。

FINAL 设置

你无需在查询中为每个表名都添加 FINAL 修饰符,而是可以使用 FINAL 设置,将其自动应用到查询中的所有表。 此设置既可按查询应用,也可应用于整个会话。

ROW policy

隐藏冗余的 _peerdb_is_deleted = 0 过滤器,一个简单的方法是使用 ROW policy。 下面是一个示例:创建行策略,从而在对表 votes 的所有查询中排除已删除的行。
行策略会应用于一组用户和角色。在此示例中,它会应用于所有用户和角色。也可以将其调整为仅应用于特定用户或角色。

像在 Postgres 中那样查询

将分析型数据集从 PostgreSQL 迁移到 ClickHouse 时,通常需要修改应用程序中的查询,以适应数据处理和查询执行上的差异。 本节将介绍如何在保持原有查询不变的情况下对数据进行去重。

视图

视图 是隐藏查询中 FINAL 关键字的一个好方法,因为它们不存储任何数据,只是在每次访问时从另一张表读取。 下面是一个示例,展示如何在 ClickHouse 中为数据库中的每张表创建包含 FINAL 关键字并过滤已删除行的视图。
然后,我们可以使用与在 PostgreSQL 中相同的查询语句来查询这些视图。

可刷新materialized view

另一种方法是使用可刷新materialized view,它支持按计划执行查询,对行去重,并将结果存储到目标表中。每次按计划刷新时,目标表都会被最新的查询结果替换。 这种方法的主要优势在于,带有 FINAL 关键字的查询只会在刷新时执行一次,因此后续针对目标表的查询无需再使用 FINAL。 不过,它的缺点是目标表中的数据只能保持在最近一次刷新的状态。也就是说,对于许多用例来说,几分钟到几小时的刷新间隔可能已经足够。
然后,您就可以像平常一样查询 deduplicated_posts 表了。
最后修改于 2026年7月1日