初始订单表有 O1001、O1002 两条记录。送入同步程序的批次则包含 O1001、O1003、O1004:第一条需要改价,后两条需要新增,批次外的 O1002 必须保留。这个小样本用于核对 MERGE 的匹配条件,也用于检查 OUTPUT 返回给应用的订单号和金额。
测试日期为 2026 年 11 月 14 日。金仓使用活动指定的 V9R4C19 SQL Server 兼容版,对照库使用 SQL Server 2022。第一轮同步完成后,继续提交相同批次,再分别执行删除、事务回滚和重复源键用例。最后单独生成 10,000 行数据,核对批量写入后的每一条金额。
1. 实验环境与数据库初始化
| 项目 | 实际配置 |
|---|---|
| 服务器 | Intel Xeon 6981E-C,8 vCPU,约 16GB 内存 |
| 操作系统 | Ubuntu 22.04.5 LTS,x86_64 |
| 容器资源 | 每个数据库限制为 4 CPU、6GB 内存 |
| KingbaseES | V009R004C019,镜像构建版本 B0004 |
| 金仓兼容模式 | sqlserver |
| 金仓字符集及排序环境 | UTF8,en_US.UTF-8 |
| SQL Server | 2022 CU17,16.0.4175.1,Developer Edition |
| 实验数据库 | compat_lab |

金仓镜像来自官方软件下载中心,文件名为 KingbaseES_V009R004C019B0004_x86_64_Docker.tar。导入后得到镜像 kingbase_v009r004c019b0004_single_x86:v1。部署过程遵循该版本的 Docker 安装说明。
bash
sudo docker load -i KingbaseES_V009R004C019B0004_x86_64_Docker.tar
mkdir -p data/kes
chmod 755 data data/kes
sudo docker run -d --name kes-lab \
--restart unless-stopped --cpus 4 --memory 6g --shm-size 1g \
-p 127.0.0.1:54321:54321 \
--env-file kes.env \
-v "$PWD/data/kes:/home/kingbase/userdata" \
kingbase_v009r004c019b0004_single_x86:v1
kes.env 设置 DB_USER=system、DB_MODE=sqlserver、ENCODING=UTF8,数据库密码通过 DB_PASSWORD 单独配置。环境文件只供本机读取,不随文章分发。SQL Server 使用 Developer 版;两端数据分别持久化,服务端口只绑定宿主机回环地址。
初始化时有一个具体限制:在金仓的 test 库执行 CREATE DATABASE,返回 can not create database in current database。改为连接 master 库后,CREATE DATABASE compat_lab 执行成功。后续 SQL 均在独立的 compat_lab 库运行。
bash
sudo docker exec kes-lab ksql -U system -d master \
-c 'CREATE DATABASE compat_lab;'
sudo docker exec kes-lab ksql -U system -d compat_lab \
-c 'SELECT version(); SHOW database_mode;'
金仓查询返回 KingbaseES V009R004C019 和 sqlserver,授权剩余有效期为 180 天。安装日志曾出现 sudo 的 PAM 会话提示,随后数据库完成初始化并启动;下文验证的是 SQL 行为。

截图使用本地实验控制台采集。控制台通过 SSH 调用服务器中的 ksql 或 sqlcmd,右侧显示实际标准输出和错误输出;截图保持浏览器原始画面。完整 SQL 和原始日志与文章一并提供。
SQL Server 对照镜像固定为 mcr.microsoft.com/mssql/server:2022-CU17-ubuntu-22.04。查询得到版本 16.0.4175.1,实验库兼容级别为 160,排序规则为 SQL_Latin1_General_CP1_CI_AS。

2. 用业务订单号匹配,保留自增主键
目标表 orders 同时保存自增主键 order_id 和唯一业务键 order_no。同步条件使用业务订单号,避免把数据库生成的编号当成外部系统的稳定标识。
sql
CREATE TABLE orders (
order_id INT IDENTITY(1,1) PRIMARY KEY,
order_no VARCHAR(20) NOT NULL UNIQUE,
customer NVARCHAR(40) NOT NULL,
amount DECIMAL(12,2) NOT NULL,
status VARCHAR(12) NOT NULL,
note NVARCHAR(80) NULL
);
初始目标表中,O1001 的金额为 100.00,O1002 为 20.10。暂存表 order_stage 放入三条记录:O1001 改为 129.90,并更新为 PAID;O1003 新增金额 0.01,备注为 NULL;O1004 新增金额 999999.99,备注为中文。O1002 不出现在本批次中,应当保留。
金额使用 DECIMAL(12,2),中文常量加 N 前缀。暂存表暂不设置订单号唯一约束,以便后续注入重复源键。

3. 一条 MERGE 同时验证写入和返回结果
sql
MERGE INTO orders AS t
USING order_stage AS s ON t.order_no=s.order_no
WHEN MATCHED THEN
UPDATE SET customer=s.customer,amount=s.amount,
status=s.status,note=s.note
WHEN NOT MATCHED THEN
INSERT(order_no,customer,amount,status,note)
VALUES(s.order_no,s.customer,s.amount,s.status,s.note)
OUTPUT $action AS action,inserted.order_no,inserted.amount;
金仓返回 3 条操作结果:O1001 为 UPDATE,O1003、O1004 为 INSERT。查询目标表可见 4 条订单,O1002 仍为 20.10。O1004 的中文备注正常显示;NULL 的语义另通过条件判断核验,避免把客户端显示的空白误认为空字符串。

对 OUTPUT 的校验分两步进行:先核对操作类型及返回金额,再查询目标表核对最终数据。单看"执行成功"无法排除字段映射错误;单看目标表又无法证明应用收到的返回值正确。这里的三条输出无需依赖行顺序,按业务订单号逐条比较即可。
SQL Server 执行相同文件,同样返回一条 UPDATE、两条 INSERT。sqlcmd 将 0.01 显示为 .01,而 ksql 显示为 0.01;这是本次客户端输出的格式差异,比较金额时按数值核对。

4. 重复执行后,订单数不变但仍然发生更新
原样再次执行相同批次,结果中出现 3 条 UPDATE。目标表保持 4 行,金额合计仍为 1000150.00。

这组结果暴露出同步实现中的一个容易忽略的问题。业务键匹配阻止了重复插入,但无条件的 WHEN MATCHED THEN UPDATE 仍会进入更新分支。若应用按照返回行数记录同步工作量,这次执行将被记为处理了 3 条变更,尽管字段值没有变化。
在匹配分支增加字段差异条件后,再执行已同步批次,OUTPUT 返回 0 行。备注允许为 NULL,判断条件需要覆盖"空值变成非空值"和"非空值变成空值"两种情况,仅写 t.note<>s.note 并不完整。
sql
WHEN MATCHED AND (
t.customer<>s.customer OR t.amount<>s.amount
OR t.status<>s.status OR t.note<>s.note
OR (t.note IS NULL AND s.note IS NOT NULL)
OR (t.note IS NOT NULL AND s.note IS NULL)
) THEN
UPDATE SET customer=s.customer,amount=s.amount,
status=s.status,note=s.note
对应复测脚本先恢复前序用例修改过的订单,再执行带条件的 MERGE。因此日志中的 MERGE 3 属于恢复步骤;紧随其后的空结果集才是无变化批次的验证结果,目标表仍为 4 行、1000150.00。

5. OUTPUT 区分修改前后的值
将 O1003 的金额从 0.01 改为 0.02,同时把状态改为 PAID:
sql
UPDATE orders SET status='PAID',amount=0.02
OUTPUT deleted.order_no,deleted.amount AS old_amount,
inserted.amount AS new_amount,inserted.status AS new_status
WHERE order_no='O1003';
返回的 old_amount 为 0.01,new_amount 为 0.02,new_status 为 PAID。随后删除 O1004,OUTPUT deleted.order_no,deleted.amount 返回 O1004 和 999999.99。再次查询目标表,剩余 3 行,金额合计 150.02,能够与更新和删除结果互相核对。

这些结果适合供应用确认本次语句处理了哪些订单。若应用据此向外部系统发送通知,还需要以事务提交成功作为发送依据;语句返回了一行数据,并不能单独证明整个业务事务已经提交。
6. 回滚撤销了记录,没有回收自增编号
为隔离订单表中的历史分配,另建 identity_probe,设置 IDENTITY(1,1)。首次插入后,SCOPE_IDENTITY()、@@IDENTITY 与 IDENT_CURRENT('identity_probe') 在当前无触发器、单会话用例中均返回 1。
第二次插入放在显式事务内,OUTPUT inserted.id 返回 2,随后执行 ROLLBACK TRANSACTION。第三次插入返回 3。最终表中只有 (1, first) 和 (3, after_rollback),没有编号 2 对应的记录。

据此,应用不应把自增编号连续性作为记录完整性的校验依据。此处只验证了当前会话的直接插入,不能据此认为三种函数在触发器或跨会话场景中等价。读取新增行的编号时,OUTPUT inserted.id 已经能够把编号与本次写入结果直接关联。
7. 重复源键必须在合并前处理
向暂存表额外加入一条 O1001,金额设为 88.88。此时两条源记录同时匹配目标表中的同一订单。再次执行 MERGE,金仓返回:
text
ERROR: unable to get a stable set of rows in the source tables
失败后查询目标表,O1001 仍为 129.90,O1003 保持前一用例更新后的 0.02,O1004 没有因该语句而重新插入。用以下语句能够定位到 O1001 的两条源记录:
sql
SELECT order_no,COUNT(*) AS source_count
FROM order_stage
GROUP BY order_no HAVING COUNT(*)>1;

SQL Server 在相同重复源键用例中返回错误 8672。该次 sqlcmd 调用把整份文件作为一个批次提交,报错后未返回后续 SELECT 的结果;因此另外提交只读核验文件,确认目标表仍为三条原记录,源表中 O1001 的计数为 2。ksql 逐条执行时继续返回了后续查询结果。这里必须区分客户端的批次提交方式与数据库对 MERGE 的处理,不能仅根据日志行数推断兼容性差异。


目标表上的唯一约束无法替代源数据检查。对同步业务而言,重复记录需要明确处理规则:拒绝整个批次,或者依据可靠的业务版本号选出应保留的记录。没有版本字段时,直接去重会丢失判断依据。本实验只移除专门注入的重复记录,随后恢复并复测,不据此构造生产去重规则。
8. 10,000 条批量数据核对
批量用例使用独立的 bulk_stage 和 bulk_target。主键从 1 到 10000,金额按编号除以 100 生成,避免随机数据使复测结果变化。生成脚本每次写入 1000 行,共 10 批。
第一次 MERGE 后,目标表记录数为 10000,最小编号 1、最大编号 10000,金额合计 500050.00。随后只把源表前 100 行各增加 1.00,再次 MERGE 处理 100 行;目标表仍为 10000 行,总额增至 500150.00。
最后通过 FULL OUTER JOIN 同时检查缺行、多行和金额不一致,mismatch_count 返回 0。总额校验可以发现整体偏差,逐行关联则避免正负差额抵消后"总数正确、明细错误"的情况。

该用例提供批处理正确性的证据。两库共享同一台主机,实验未按性能基准控制缓存、重复轮次和系统负载,因此不据此比较数据库吞吐量。
9. 对照结果与适用范围
两端按照相同的建表、造数和业务 SQL 执行。返回结果按订单号匹配,金额按 DECIMAL 数值核对;错误文本单独记录。
| 验证项 | KingbaseES V9R4C19 | SQL Server 2022 CU17 |
|---|---|---|
| 首轮 MERGE | 1 条更新,2 条新增 | 1 条更新,2 条新增 |
| 原样重复批次 | 3 条 UPDATE,目标表 4 行 | 3 条 UPDATE,目标表 4 行 |
| 带差异条件的重复批次 | 0 条变更 | 0 条变更 |
| OUTPUT 修改前后金额 | 0.01 → 0.02 | 0.01 → 0.02 |
| 删除 O1004 后 | 3 行,合计 150.02 | 3 行,合计 150.02 |
| 回滚后的自增主键 | 最终保留 1、3 | 最终保留 1、3 |
| 重复源键 | 语句失败,原目标数据保留 | 错误 8672,原目标数据保留 |
| 10,000 行首次合并 | 合计 500050.00 | 合计 500050.00 |
| 修改 100 行后 | 合计 500150.00,逐行差异 0 | 合计 500150.00,逐行差异 0 |


在这些用例中,MERGE、OUTPUT 和 IDENTITY 的关键业务结果一致。