AI时代数据库操作对象由人变成了Agent,可应用于试错等场景。瞬时数据分支能力是支撑该场景的重要技术。对于各种试错的结果,需要和分支进行diff,从而帮助将正确的结果merge到主分支。本文介绍openGauss数据分支全量diff能力。
1、命令执行入口
通过neon_local_branch_diff命令执行分支diff,如下图所示,主入口函数是neon_local.rs:main,handle_branch_ops为branch的操作函数入口:

入参:
- source_schema:源 schema
- target_schema:目标 schema
- include_tables:可选,只比对指定表名数组;NULL= 比对全部表
返回字段:
|-------------|---------------------------|
| 字段 | 含义 |
| schema_name | 固定填充 target_schema(函数硬编码) |
| table_name | 表名 |
| diff_type | 差异类型标记 |
| row_data | JSON 序列化的行数据 / 签名信息 |
2、整体流程

主要思想:连接target,在target上创建连接source的数据源服务,即通过postgres_fdw定义好连接source的IP和端口以及数据库名;然后连接source,从source上的pg_class、pg_namespace、pg_attribute中查出所有普通表的结构信息,然后保存到BtreeMap中;在target上提取BtreeMap中信息创建外部表(字段定义相同,以便target从source读取数据)。注意,target上创建的外部的schema在OGGIT_FDW_SCHEMA下,这样即使target上表名一样,也不会冲突。
真正执行diff的是neon_branch_diff函数:
cpp
CREATE OR REPLACE FUNCTION neon_branch_diff(
source_schema name,
target_schema name,
include_tables name[] DEFAULT NULL
) RETURNS TABLE (
schema_name text,
table_name text,
diff_type text,
row_data text
)
LANGUAGE plpgsql
AS $$
DECLARE
tbl record;
src_rel oid;
tgt_rel oid;
src_sig text;
tgt_sig text;
pk_cond text;
val_equal_cond text;
BEGIN
FOR tbl IN
SELECT relname
FROM (
SELECT c.relname
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = source_schema::text
AND c.relkind IN ('r', 'f')
UNION
SELECT c.relname
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = target_schema::text
AND c.relkind = 'r'
) names
WHERE include_tables IS NULL OR relname = ANY(include_tables)
ORDER BY relname
LOOP
SELECT max(c.oid)
INTO src_rel
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = source_schema::text
AND c.relname = tbl.relname
AND c.relkind IN ('r', 'f');
SELECT max(c.oid)
INTO tgt_rel
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = target_schema::text
AND c.relname = tbl.relname
AND c.relkind = 'r';
IF src_rel IS NULL THEN
RETURN QUERY EXECUTE format(
'SELECT %L::text, %L::text, %L::text, row_to_json(t)::text FROM %I.%I t',
target_schema::text,
tbl.relname,
'target_only',
target_schema,
tbl.relname
);
CONTINUE;
END IF;
IF tgt_rel IS NULL THEN
RETURN QUERY EXECUTE format(
'SELECT %L::text, %L::text, %L::text, row_to_json(s)::text FROM %I.%I s',
target_schema::text,
tbl.relname,
'source_only',
source_schema,
tbl.relname
);
CONTINUE;
END IF;
SELECT string_agg(attname || ':' || atttypid || ':' || atttypmod || ':' || attnotnull, ',' ORDER BY attnum)
INTO src_sig
FROM pg_attribute
WHERE attrelid = src_rel AND attnum > 0 AND NOT attisdropped;
SELECT string_agg(attname || ':' || atttypid || ':' || atttypmod || ':' || attnotnull, ',' ORDER BY attnum)
INTO tgt_sig
FROM pg_attribute
WHERE attrelid = tgt_rel AND attnum > 0 AND NOT attisdropped;
IF src_sig IS DISTINCT FROM tgt_sig THEN
schema_name := target_schema::text;
table_name := tbl.relname;
diff_type := 'schema_mismatch';
row_data := 'source=' || COALESCE(src_sig, '') || ' target=' || COALESCE(tgt_sig, '');
RETURN NEXT;
CONTINUE;
END IF;
SELECT string_agg(format('t.%I IS NOT DISTINCT FROM s.%I', a.attname, a.attname), ' AND ' ORDER BY a.attnum)
INTO pk_cond
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = tgt_rel AND i.indisprimary;
IF pk_cond IS NULL THEN
schema_name := target_schema::text;
table_name := tbl.relname;
diff_type := 'no_primary_key';
row_data := NULL;
RETURN NEXT;
CONTINUE;
END IF;
SELECT COALESCE(string_agg(format('t.%I IS NOT DISTINCT FROM s.%I', a.attname, a.attname), ' AND ' ORDER BY a.attnum), 'TRUE')
INTO val_equal_cond
FROM pg_attribute a
WHERE a.attrelid = tgt_rel
AND a.attnum > 0
AND NOT a.attisdropped
AND NOT EXISTS (
SELECT 1
FROM pg_index i
WHERE i.indrelid = tgt_rel
AND i.indisprimary
AND a.attnum = ANY(i.indkey)
);
RETURN QUERY EXECUTE format(
'SELECT %L::text, %L::text, %L::text, row_to_json(s)::text
FROM %I.%I s
WHERE NOT EXISTS (SELECT 1 FROM %I.%I t WHERE %s)',
target_schema::text,
tbl.relname,
'source_only',
source_schema,
tbl.relname,
target_schema,
tbl.relname,
pk_cond
);
RETURN QUERY EXECUTE format(
'SELECT %L::text, %L::text, %L::text, row_to_json(t)::text
FROM %I.%I t
WHERE NOT EXISTS (SELECT 1 FROM %I.%I s WHERE %s)',
target_schema::text,
tbl.relname,
'target_only',
target_schema,
tbl.relname,
source_schema,
tbl.relname,
pk_cond
);
RETURN QUERY EXECUTE format(
'SELECT %L::text, %L::text, %L::text,
(''source='' || row_to_json(s)::text || '' target='' || row_to_json(t)::text)
FROM %I.%I t
JOIN %I.%I s ON %s
WHERE NOT (%s)',
target_schema::text,
tbl.relname,
'conflict',
target_schema,
tbl.relname,
source_schema,
tbl.relname,
pk_cond,
val_equal_cond
);
END LOOP;
END;
$$;
流程 简要说明 :
1、收集待比对表集合:合并 source、target 两边的表名,取并集
source 侧:普通表r + 外部表f(postgres_fdw 外部表也纳入比对)
target 侧:仅普通表r,不包含外部表
过滤:include_tables非空时,只保留数组内的表
2、循环遍历每张表:
查找 source 表 oid、target 表 oid
分支 1:源表不存在 → target_only(target 独有行)
分支 2:目标表不存在 → source_only(source 独有行)
分支 3:两边都存在 → 检查表结构签名;签名不一致输出schema_mismatch
分支 4:结构一致 → 提取主键;无主键直接返回 no_primary_key,跳过数据比对
分支 5:有主键 → 三段 SQL 查找差异:source 独有行 /target 独有行 / 主键相同但非主键字段值冲突行
逐段核心逻辑拆解 :
2. 1表集合查询
SELECT relname
FROM (
-- source:普通表r + 外部表f
SELECT c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = source_schema::text AND c.relkind IN ('r', 'f')
UNION
-- target:仅普通表r
SELECT c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = target_schema::text AND c.relkind = 'r'
) names
WHERE include_tables IS NULL OR relname = ANY(include_tables)
重点:UNION自动去重,拿到两边所有出现过的表名。source 支持外部表(FDW 表),target 只认本地物理表。
2. 2表存在性判断
src_rel IS NULL:source 无此表,只有 target 有 → diff_type='target_only',输出 target 全部行
tgt_rel IS NULL:target 无此表,只有 source 有 → diff_type='source_only',输出 source 全部行
2. 3表结构签名比对(schema_mismatch)
SELECT string_agg(attname || ':' || atttypid || ':' || atttypmod || ':' || attnotnull, ',' ORDER BY attnum) INTO src_sig
FROM pg_attribute
WHERE attrelid = src_rel AND attnum > 0 AND NOT attisdropped;
签名构成:"字段名:类型OID:类型修饰符:是否非空",按字段序号拼接字符串
只要字段数量、字段顺序、类型、长度、NOT NULL 约束任意一处不一致 → schema_mismatch
注意:不校验主键、索引、注释、默认值,只校验字段元信息。
2. 4提取主键条件 pk_cond
SELECT string_agg(format('t.%I IS NOT DISTINCT FROM s.%I', a.attname, a.attname), ' AND ' ORDER BY a.attnum)
INTO pk_cond
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = tgt_rel AND i.indisprimary;
取出 target 表主键字段,生成主键匹配表达式:t.pk1 IS NOT DISTINCT FROM s.pk1 AND t.pk2 IS NOT DISTINCT FROM s.pk2
IS NOT DISTINCT FROM:NULL 安全等值判断(=无法匹配 NULL,这是这个函数的亮点)
如果表没有主键:输出diff_type='no_primary_key',跳过该行数据比对
2. 5非主键字段等值条件 val_equal_cond
SELECT COALESCE(string_agg(format('t.%I IS NOT DISTINCT FROM s.%I', a.attname, a.attname), ' AND ' ORDER BY a.attnum), 'TRUE')
INTO val_equal_cond
FROM pg_attribute a
WHERE a.attrelid = tgt_rel
AND a.attnum > 0 AND NOT a.attisdropped
AND NOT EXISTS ( -- 排除主键字段
SELECT 1 FROM pg_index i WHERE i.indrelid = tgt_rel AND i.indisprimary AND a.attnum = ANY(i.indkey)
);
生成所有非主键字段的 NULL 安全等值条件。如果表只有主键没有其他字段,val_equal_cond=TRUE。
2. 6三段动态 SQL,查找三类差异
① source_only:source 存在,target 找不到对应主键行
SELECT ... FROM source_schema.tbl s
WHERE NOT EXISTS (SELECT 1 FROM target_schema.tbl t WHERE pk条件)
② target_only:target 存在,source 找不到对应主键行
SELECT ... FROM target_schema.tbl t
WHERE NOT EXISTS (SELECT 1 FROM source_schema.tbl s WHERE pk条件)
③ conflict:主键相同,但非主键字段内容不一致
FROM target t JOIN source s ON pk条件
WHERE NOT (非主键全部相等条件)
返回:source=json行 target=json行,方便对比同一主键两边不同的值。
3、 diff_type 枚举汇总
|-------------------|----------------------------|----------------------------------|
| diff_type | 含义 | row_data |
| source_only | 该行仅在 source 存在,target 无该主键 | source 行 json |
| target_only | 该行仅在 target 存在,source 无该主键 | target 行 json |
| schema_mismatch | 表结构(字段 / 类型 / 非空)不一致 | source 签名串 + target 签名串 |
| no_primary_key | target 表缺少主键,无法逐行比对 | NULL |
| conflict | 主键相同,但其他字段值不一致 | source 行 json + target 行 json 拼接 |
优点:
1)NULL 安全:使用IS NOT DISTINCT FROM,解决普通=无法比较 NULL 的坑
2)支持 source 侧 FDW 外部表(relkind='f'),刚好适配你前面 postgres_fdw 场景
3)支持指定表白名单include_tables,不用全库比对
4)自动识别表结构变更,提前终止该表的数据比对,避免字段不匹配导致动态 SQL 报错
输出 JSON 行,方便后续导出、解析差异内容
限制:
1)主键取自 target 表,拿 target 表的主键定义去匹配 source 表。如果 source/target 主键定义不一样,比对逻辑直接错乱。
2)性能极差 ,大数据量表不能用,底层是NOT EXISTS嵌套循环,大表会全表扫描,没有分页、没有分片,千万级以上表直接卡死。适合小表、元数据表、分支快照小规模校验。
3)不校验:索引、约束、默认值、序列、注释、权限、分区信息;仅检查字段类型 + NOT NULL
4)没有事务快照隔离:并发写入时,两边表数据在比对过程中持续变更,会产生虚假差异
5)row_to_json对大文本、二进制、特殊类型有转义限制;json 不保留类型,仅文本展示
函数硬编码schema_name返回值永远是 target_schema,语义容易混淆
6)只支持 target 为本地物理表,target 侧不支持 FDW 外部表(WHERE 条件 c.relkind = 'r')
7)没有过滤系统表;没有跳过临时表