openGauss branch diff能力解析

AI时代数据库操作对象由人变成了Agent,可应用于试错等场景。瞬时数据分支能力是支撑该场景的重要技术。对于各种试错的结果,需要和分支进行diff,从而帮助将正确的结果merge到主分支。本文介绍openGauss数据分支全量diff能力。

1、命令执行入口

通过neon_local_branch_diff命令执行分支diff,如下图所示,主入口函数是neon_local.rs:main,handle_branch_ops为branch的操作函数入口:

入参:

  1. source_schema:源 schema
  2. target_schema:目标 schema
  3. 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)没有过滤系统表;没有跳过临时表

相关推荐
郭小李1 小时前
spring中@Transactional注解对事务的支持
数据库·spring·oracle
运维行者_1 小时前
供应链安全成最大风险源,企业如何构建可控的运维体系?
运维·开发语言·网络·数据库·安全·云计算·流量监控
高铭杰1 小时前
Postgresql源码(160)新的共享内存注册机制,为动态调整shared_buffers打基础
数据库·postgresql
数据库小学妹1 小时前
AI Agent连数据库怎么控权限?只读账号的4个缺口
数据库·人工智能·安全·数据库权限·审计·ai数据库·只读账号
瑞克-桑切斯2 小时前
6、MySQL 总结
数据库
凤凰院凶涛QAQ2 小时前
《MySQL内核拆解:从SQL到InnoDB》(三) : 数据库设计 & 聚合查询 & 分组查询
数据库·sql·mysql
Decay__3 小时前
01_MySQL 体系架构全解 —— 连接层、SQL 层、存储引擎层与 InnoDB 深入
运维·数据库·sql·mysql·架构
Linux_cy3 小时前
nginx 502 排查手册:九种原因分类、定位命令与检查顺序
linux·运维·服务器·网络·数据库·nginx·ssh
在多学一点3 小时前
MySQL_DDL的基本命令
数据库·mysql·adb