
简介面向金蝶K3 WISE实施、运维与二次开发人员的基础资料同步SQL脚本合集重点解决职员、物料、客户、供应商、仓库、计量单位、部门等主数据在数据库间的同步与维护问题适合需要参考存储过程写法、梳理同步逻辑或排查数据一致性问题的技术人员。压缩包共15个sql文件约30KB内容以基础资料及其类别的同步存储过程、同步辅助表脚本为主覆盖从类别定义到明细数据的处理链路便于按业务对象查找和改写。已有1349人学习说明其在K3基础资料同步场景中具有一定参考价值。读者可从中获得可复用的SQL组织思路、存储过程模板与同步辅助表设计用于搭建或完善自己的数据同步方案并结合实际表结构做适配。1. K3 WISE 基础资料同步sql语句为什么“复制粘贴 INSERT”一上线就翻车做过一次账套对接的人大概都遇到过同一个场景把源账套的客户、供应商、物料导成几百条 INSERT 语句在目标账套跑一遍前两百条一切正常从第两百零一条开始报主键冲突绕过冲突继续跑单据里选不到物料BOM 展开提示单位不存在。问题不在 SQL 语法而在 K3 WISE 的基础资料从来不是一张表——t_Item是骨架客户、供应商、部门、职员、仓库各自还有扩展表挂在后面靠自增的FItemID串起来。基础资料同步sql语句真正要解决的是业务主键的映射、自增列的回填和父子层级的顺序三件事。这篇文章写给我这种被迁移坑过一遍的人讲清楚表结构、给可直接改的脚本、把血泪经验里的坑逐条列出来。2. 摸清底表t_Item 和 t_Organization、t_Supplier 到底谁挂谁动手写同步语句之前先花二十分钟把源账套和目标账套的表结构摸一遍比事后回滚十次都划算。K3 WISE 的版本跨度很大同一张表在不同补丁上字段可能多几个少几个直接照抄网上的脚本最容易死在“目标库没这一列”上。我的习惯是先用INFORMATION_SCHEMA出一份清单再确认自增列最后才动数据。2.1 用三条查询确认字段、自增列和真实行数第一条查询看字段是否齐全重点盯FItemID、FNumber、FParentID、FLevel、FDetail、FDeleted这六个字段在不在。第二条查询确认哪些列是IDENTITY这是后面能不能直接插的关键。第三条查询数行数顺便看看脏数据比例。-- 1) 基础资料相关表的字段清单先确认目标库有没有缺列 SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME IN ( t_Item,t_ItemClass,t_Organization,t_Supplier, t_Department,t_Emp,t_Stock,t_Unit,t_UnitGroup ) ORDER BY TABLE_NAME, ORDINAL_POSITION; -- 2) 哪些列是自增列插入时不能带值除非开 IDENTITY_INSERT SELECT t.name AS TableName, c.name AS ColumnName, c.is_identity AS IsIdentity FROM sys.columns c JOIN sys.tables t ON c.object_id t.object_id WHERE t.name IN (t_Item,t_Organization,t_Supplier,t_Department,t_Emp,t_Stock,t_Unit) AND c.is_identity 1; -- 3) 按核算项目类别统计行数顺便看禁用数据占多少 SELECT FItemClassID, COUNT(*) AS TotalRows, SUM(CASE WHEN FDeleted 1 THEN 1 ELSE 0 END) AS DeletedRows, SUM(CASE WHEN FDetail 1 THEN 1 ELSE 0 END) AS LeafRows FROM t_Item GROUP BY FItemClassID ORDER BY FItemClassID;第一条查询的价值在于提前发现版本差异比如某些版本t_Item有FHelpCode助记码而老库没有脚本里带上这一列就会整批失败。第二条查询告诉你哪些列必须靠SET IDENTITY_INSERT打开或者干脆不指定值交给数据库生成。第三条查询里的FDetail尤其重要——它标记“是否明细”也就是有没有下级搞错了会导致物料分类选不进去或者 BOM 建不了。参数上没什么好调的FItemClassID的取值在不同账套里含义固定客户、供应商、部门、职员、物料各占一个号用查询结果对照业务数据看一眼就能确认。2.2 FItemID 是自增不是业务键FNumber 才是唯一的锚点新手最容易犯的错是把FItemID当成业务主键来做匹配。源账套的物料 ID 1024目标账套的 1024 很可能是一个完全不相干的客户。FItemID只是数据库内部的物理序号真正跨账套稳定的锚点是FNumber编码加上FItemClassID类别两者合起来才唯一。所以同步脚本的骨架一定是以(FItemClassID, FNumber)做匹配先把源端记录落到目标端拿到新的FItemID再用映射表回填所有引用它的字段。这套思路还能顺手解决另一个问题同一个编码在源端是“原材料”分类在目标端可能被人工挪到了“半成品”下。如果没有FItemClassID参与匹配脚本会把它当成新增再插一条结果就是编码重复、单据选料时出现两条同名物料。匹配条件写成FItemClassID FNumber后这种情况会落到“已存在但分类不同”的分支里交给人来判断而不是让脚本替你决定。-- 用业务键找重复同一类别下编码重复或仅大小写/空格不同 SELECT FItemClassID, FNumber, COUNT(*) AS Cnt FROM t_Item GROUP BY FItemClassID, FNumber HAVING COUNT(*) 1; -- 编码带前后空格的脏数据同步前必须清掉 SELECT FItemID, [ FNumber ] AS RawNumber FROM t_Item WHERE FNumber LTRIM(RTRIM(FNumber));第一段查询在源库和目标库各跑一遍源端有重复说明数据本身就有问题先治理再同步目标端有重复说明历史上已经被人手工插过脚本必须做幂等处理。第二段查询看着琐碎但空格问题是同步后“编码明明一样却匹配不上”的头号原因SQL Server 在默认排序规则下比较时会忽略尾部空格但前端界面不会等于脚本认为匹配成功、用户看到两条。我一般会先跑UPDATE把FNumber统一LTRIM(RTRIM())掉再开始同步。2.3 主从表的插入顺序先 t_Item 再扩展表客户和供应商在多数 K3 WISE 账套里的结构是t_Item放编码、名称、层级、禁用标志t_Organization/t_Supplier放税号、联系人、地址、结算方式这些业务属性两张表通过同一个FItemID一对一关联。部门、职员、仓库的结构类似扩展表各自不同。这意味着一条客户资料至少要写两张表而且第二张表的FItemID必须等于第一张表刚生成的值。顺序反了会怎样t_Organization能插进去但界面打开客户列表看不到因为列表是从t_Item左连接的等你在t_Item补插一条FItemID又对不上于是变成两条互不相干的记录一条有税号没名称一条有名称没税号。这种数据最难查用户在界面上反复刷新也看不出所以然。资料类型主表扩展表关联字段客户t_Itemt_OrganizationFItemID供应商t_Itemt_SupplierFItemID部门t_Itemt_DepartmentFItemID职员t_Itemt_EmpFItemID物料t_Itemt_Unit 关联、类别在 t_ItemClassFUnitID / FParentID这张表不用背用 2.1 里的字段查询加一次SELECT TOP 10 *就能验证。真正要记住的是那句老话主表先插、拿 ID、再插扩展表任何一步颠倒后面全是手工返工。3. 用编码当锚点把两个账套的差异集跑出来表结构清楚之后下一步不是急着写 INSERT而是先把“源端有、目标端没有”“两端都有但属性不同”“目标端有、源端没有”这三种状态分出来。这一步做扎实后面的插入脚本只是在执行一个已经确定的清单出问题的概率会低一个数量级。差异集我习惯用一个物理映射表加一个视图来实现映射表负责记 ID 转换关系视图负责展示差异两边职责分开。3.1 建一张映射表把源 FItemID 和目标 FItemID 钉死映射表是整个同步方案的黑匣子出问题第一时间查它。字段设计上至少要有源 ID、目标 ID、业务编码、类别、同步时间主键用(SrcFItemID, FItemClassID)再给FNumber加一个普通索引方便反查。-- 映射表记录源账套 ID 与目标账套 ID 的对应关系 IF OBJECT_ID(dbo.Sync_ItemMap,U) IS NULL BEGIN CREATE TABLE dbo.Sync_ItemMap ( SrcFItemID INT NOT NULL, DstFItemID INT NULL, FItemClassID INT NOT NULL, FNumber NVARCHAR(60) NOT NULL, SyncTime DATETIME NOT NULL CONSTRAINT DF_Sync_ItemMap_Time DEFAULT GETDATE(), SyncBatch INT NOT NULL, -- 批次号便于整批回滚 CONSTRAINT PK_Sync_ItemMap PRIMARY KEY (SrcFItemID, FItemClassID) ); CREATE INDEX IX_Sync_ItemMap_Number ON dbo.Sync_ItemMap(FItemClassID, FNumber); CREATE INDEX IX_Sync_ItemMap_Batch ON dbo.Sync_ItemMap(SyncBatch); END GOSyncBatch这一列是后悔药。同步跑到一半发现分类映射错了按批次号删除映射记录、再按映射表反查目标库刚插入的行一条DELETE就能回退干净不用去比对时间戳。DstFItemID允许为空是有意为之先把源端清单灌进映射表占位插入成功后再回填目标 ID这样中途失败也能看出“哪些已经处理、哪些还没开始”。索引不要省十万级物料下没有IX_Sync_ItemMap_Number回填父子关系那一步会慢到你以为数据库卡死。3.2 用 FULL OUTER JOIN 一次跑出新增、变更、仅目标端三种状态差异集的核心是FULL OUTER JOIN。源表和目标表按FItemClassID FNumber关联左表有右表无是新增右表有左表无是目标端多余两边都有就比名称、父级、是否明细这些关键字段不一致标记为变更。-- 差异视图三种状态一次算出来假设源库通过链接服务器 SrcDB 访问 CREATE OR ALTER VIEW dbo.v_Sync_ItemDiff AS SELECT COALESCE(s.FItemClassID, d.FItemClassID) AS FItemClassID, COALESCE(s.FNumber, d.FNumber) AS FNumber, s.FItemID AS SrcFItemID, d.FItemID AS DstFItemID, s.FName AS SrcName, d.FName AS DstName, s.FParentID AS SrcParentID, d.FParentID AS DstParentID, CASE WHEN d.FItemID IS NULL THEN ADD -- 目标端没有新增 WHEN s.FItemID IS NULL THEN ONLY_DST -- 目标端多余人工确认 WHEN ISNULL(s.FName,) ISNULL(d.FName,) OR ISNULL(s.FDetail,0) ISNULL(d.FDetail,0) OR ISNULL(s.FDeleted,0) ISNULL(d.FDeleted,0) THEN DIFF -- 属性不一致更新 ELSE SAME END AS DiffType FROM SrcDB.dbo.t_Item s FULL OUTER JOIN dbo.t_Item d ON s.FItemClassID d.FItemClassID AND s.FNumber d.FNumber WHERE ISNULL(s.FDeleted,0) 0 OR s.FItemID IS NULL;COALESCE处理的是三种状态里只有一边有值的情况不加的话ONLY_DST那批记录的类别和编码全是 NULL统计时会漏掉。WHERE里排除源端已禁用的资料是因为禁用数据同步过去只会给目标端添乱但如果业务上要求保留历史就把条件去掉改为在插入时带上FDeleted标志。DIFF分支只比了名称、明细标志、禁用标志三个字段实际项目里可以按需加地址、税号、结算方式但别把FSort排序码加进去——排序码在两端含义不同比对结果会全是差异白白增加人工确认量。3.3 层级资料必须按 FLevel 升序插否则父级找不到物料分类、部门、客户分组都是有层级的FParentID指向父级FLevel表示层级深度。全量插入如果按FItemID顺序来子级很可能排在父级前面FParentID就指向了一个还不存在的 ID。解决办法很简单按FLevel升序处理一层插完再插下一层。-- 按层级取待插入清单先浅后深 SELECT s.FItemClassID, s.FNumber, s.FName, s.FLevel, s.FParentID, srcParent.FNumber AS SrcParentNumber FROM SrcDB.dbo.t_Item s LEFT JOIN SrcDB.dbo.t_Item srcParent ON srcParent.FItemID s.FParentID LEFT JOIN dbo.t_Item tgt ON tgt.FItemClassID s.FItemClassID AND tgt.FNumber s.FNumber WHERE ISNULL(s.FDeleted,0) 0 AND tgt.FItemID IS NULL ORDER BY s.FItemClassID, s.FLevel, s.FNumber;这里多取了一个SrcParentNumber是整段脚本的关键。目标库的FParentID必须填新生成的 ID而源端的FParentID在目标库毫无意义所以要把父级的编码带过来插入完成后用父级编码到映射表里反查目标 ID。ORDER BY里把FItemClassID放在最前面是因为客户和物料的层级要分开算混在一起排序会让某个类别的最底层记录跑到最前面。4. 生成可回滚的同步脚本MERGE 拿新 ID、映射表回填父级到了真正写数据这一步我一般不用INSERT ... SELECT硬塞。原因是拿不到“源 ID 对应哪个新 ID”只能靠编码再查一遍多一次关联就多一次出错机会。用MERGE加OUTPUT INTO可以在一句话里把源 ID 和新 ID 同时写进映射表这是整套脚本里最舒服的一段。4.1 用 MERGE ... OUTPUT 一次完成插入和 ID 映射MERGE常规用途是同步数据但它的OUTPUT子句有个特性可以同时引用源表列src.xxx和插入后的列inserted.xxx。利用这一点插入的同时就把映射关系落库了。-- 待插入暂存表结构按目标库实际字段裁剪 CREATE TABLE #Stage ( SrcFItemID INT, FItemClassID INT, FNumber NVARCHAR(60), FName NVARCHAR(255), FLevel INT, FDetail BIT, FDeleted BIT, SrcParentID INT ); INSERT INTO #Stage (SrcFItemID, FItemClassID, FNumber, FName, FLevel, FDetail, FDeleted, SrcParentID) SELECT s.FItemID, s.FItemClassID, s.FNumber, s.FName, ISNULL(s.FLevel,1), ISNULL(s.FDetail,0), 0, s.FParentID FROM SrcDB.dbo.t_Item s LEFT JOIN dbo.t_Item t ON t.FItemClassID s.FItemClassID AND t.FNumber s.FNumber WHERE ISNULL(s.FDeleted,0) 0 AND t.FItemID IS NULL AND s.FLevel CurrentLevel; -- 从第一层开始逐层推进 -- 插入并回写映射表OUTPUT 能同时带走源列和新生成的主键 MERGE INTO dbo.t_Item AS tgt USING #Stage AS src ON 1 0 -- 强制所有行走 NOT MATCHED WHEN NOT MATCHED THEN INSERT (FItemClassID, FNumber, FName, FLevel, FDetail, FParentID, FDeleted) VALUES (src.FItemClassID, src.FNumber, src.FName, src.FLevel, src.FDetail, 0, src.FDeleted) OUTPUT src.SrcFItemID, inserted.FItemID, inserted.FItemClassID, inserted.FNumber, BatchNo INTO dbo.Sync_ItemMap (SrcFItemID, DstFItemID, FItemClassID, FNumber, SyncBatch);ON 1 0是故意写死的意思是“永远不匹配”所有行都走插入分支。如果不这么写MERGE会拿源表和目标表做连接碰上编码相同但需要更新的行会走UPDATE分支而这次的清单本来就是筛出来的纯新增加连接反而慢。OUTPUT里的列顺序要和INTO后面的字段列表一一对应BatchNo是外部传进来的批号变量用来支持整批回滚。FParentID暂时填 0等这一层插完再统一回填FLevel用ISNULL兜底成 1是因为老账套里有些记录的层级字段是空的不兜底会让后面的排序逻辑失效。提示MERGE在并发场景下有已知的锁问题同步脚本必须保证同一时间只有一个进程在跑最简单的办法是开头sp_getapplock拿一个应用锁跑完释放。4.2 用映射表把 FParentID 从源 ID 翻译成目标 ID插入完成后FParentID还是 0。回填的逻辑是拿暂存表里的SrcParentID去映射表查源 ID 对应的目标 ID。第一层记录的SrcParentID是 0 或者指向根节点回填时过滤掉即可。-- 回填父级靠映射表把源端的 FParentID 翻译成目标端的 FItemID UPDATE t SET t.FParentID ISNULL(pm.DstFItemID, 0) FROM dbo.t_Item t JOIN dbo.Sync_ItemMap m ON m.DstFItemID t.FItemID JOIN #Stage s ON s.SrcFItemID m.SrcFItemID AND s.FItemClassID m.FItemClassID LEFT JOIN dbo.Sync_ItemMap pm ON pm.SrcFItemID s.SrcParentID AND pm.FItemClassID s.FItemClassID WHERE s.SrcParentID 0; -- 父级在目标端找不到的说明父级被禁用或漏同步单独捞出来人工处理 SELECT s.FNumber, s.FName, s.SrcParentID FROM #Stage s LEFT JOIN dbo.Sync_ItemMap pm ON pm.SrcFItemID s.SrcParentID AND pm.FItemClassID s.FItemClassID WHERE s.SrcParentID 0 AND pm.DstFItemID IS NULL;回填用ISNULL(pm.DstFItemID, 0)把找不到父级的记录先归到根节点下避免FParentID变成 NULL 导致界面报错。但这么做只是止血第二段查询必须跑把这类记录捞出来看看到底是父级被禁用、还是同步时漏了。我遇到过一次源端某个部门的父级是“总部”而“总部”在目标账套里被改名成了“集团总部”编码也改过映射表里根本没有对应记录结果一整棵部门树全部挂到了根节点下界面上看起来层级全平了。这类问题的共同点是映射表里查不到就说明业务键没对上必须回到 3.2 的差异视图重新确认而不是靠ISNULL掩盖。4.3 批大小、事务和失败重跑的设计百万级物料不可能一个事务跑完日志会把磁盘撑爆。我一般按每批 2000 到 5000 行提交一次批次号写进映射表。行数太少提交开销大太多回滚时间长2000 到 5000 是实测下来比较舒服的区间具体看单行宽度。-- 分批循环骨架在存储过程里用 WHILE 实现 DECLARE BatchSize INT 3000, Rows INT 1, BatchNo INT 1001; WHILE Rows 0 BEGIN BEGIN TRY BEGIN TRAN; -- 取一批待插入数据灌入 #Stage ... SET Rows ROWCOUNT; IF Rows 0 BEGIN COMMIT TRAN; BREAK; END -- MERGE 插入 OUTPUT 写映射表 ... -- 回填 FParentID ... COMMIT TRAN; SET BatchNo BatchNo 1; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRAN; INSERT INTO dbo.Sync_ErrorLog(BatchNo, ErrMsg, ErrTime) VALUES (BatchNo, ERROR_MESSAGE(), GETDATE()); THROW; -- 抛出去让调度系统知道失败了 END CATCH END三个参数值得调BatchSize控制单次提交行数BatchNo从某个固定值起跳便于回溯THROW不能省——吞掉异常的脚本是最危险的日志里干干净净数据却少了一半。重跑的时候不需要清空整张表靠#Stage里的LEFT JOIN dbo.t_Item ... WHERE t.FItemID IS NULL自动跳过已插入的记录这就保证了脚本的幂等性。唯一要注意的是映射表里可能有上次失败留下的半截记录重跑前按SyncBatch清一次更保险。5. 同步后的校验与排查三条 SQL 揪出断链和孤儿脚本能跑通不等于数据对。每次同步完我会固定跑三条校验再人工看几眼列表界面的层级展开。这三条校验覆盖了孤儿记录、父子断链和编码重复能拦下八成以上的问题。5.1 三条校验 SQL 和对应的修复思路-- 校验一孤儿记录FParentID 指向了一个不存在的 FItemID SELECT c.FItemID, c.FNumber, c.FName, c.FParentID FROM dbo.t_Item c LEFT JOIN dbo.t_Item p ON p.FItemID c.FParentID WHERE c.FParentID 0 AND p.FItemID IS NULL; -- 校验二明细标志与子项数量矛盾应为明细却没有子项或不该有子项却有 SELECT p.FItemID, p.FNumber, p.FName, p.FDetail, COUNT(c.FItemID) AS ChildCount FROM dbo.t_Item p LEFT JOIN dbo.t_Item c ON c.FParentID p.FItemID AND ISNULL(c.FDeleted,0) 0 WHERE ISNULL(p.FDeleted,0) 0 GROUP BY p.FItemID, p.FNumber, p.FName, p.FDetail HAVING (p.FDetail 1 AND COUNT(c.FItemID) 0) OR (p.FDetail 0 AND COUNT(c.FItemID) 0); -- 校验三客户/供应商在扩展表里找不到对应记录 SELECT i.FItemID, i.FNumber, i.FName FROM dbo.t_Item i LEFT JOIN dbo.t_Organization o ON o.FItemID i.FItemID WHERE i.FItemClassID 1 AND o.FItemID IS NULL; -- 类别号按实际账套调整第一条是硬伤必须清零否则单据引用该资料时直接报错。第二条属于“看起来不报错但业务用不了”的软问题FDetail 1却有子项意味着这个节点既是明细又是分组物料分类选到它时会提示“非明细分类不能使用”。第三条针对客户和供应商扩展表缺记录时资料能显示但税号、联系人全是空开票环节才发现。三条都跑完我会再补一条计数比对源端有效记录数减目标端新增数应该等于 0。5.2 五条踩坑记录现象、原因、解决踩坑一脚本报“IDENTITY_INSERT 已打开”或“必须显式指定自增列”。现象是插入语句直接失败。原因是有些表FItemID是自增有些表比如t_Stock的FStockID在部分版本上不是脚本一律按自增处理就错了。解决是先用 2.1 的第二条查询确认自增列要么走MERGE不指定值要么SET IDENTITY_INSERT 表名 ON并在同一会话内OFF两个会话同时开会互斥。踩坑二同步完成后物料在单据里选不到。现象是物料列表能打开但新建单据时的物料F7里搜不到。原因多半是FDetail标志错了——物料本身应该是明细FDetail 1如果插成 0 就会被当成非明细过滤掉。还有一种可能是FItemClassID填成了别的类别号。解决是拿一条源端正常物料对照所有字段逐列比一遍别只看名称和编码。踩坑三计量单位同步过去后BOM 展开报“单位不存在”。现象是物料能选建 BOM 时报错。原因是物料的FUnitID指向t_Unit而t_Unit又挂在t_UnitGroup下同步物料时只映射了FItemID没映射FUnitID新账套里那个单位 ID 对应的是别的单位或者根本不存在。解决是把单位组和单位作为独立的基础资料先同步建好单位映射表再同步物料时用映射表翻译FUnitID。踩坑四同一批数据跑两遍目标端出现重复记录。现象是第二次跑完物料数量翻倍。原因是匹配条件只用了FNumber没带FItemClassID或者编码两端一个有空格一个没有。解决是匹配条件必须双字段且同步前统一LTRIM(RTRIM(FNumber))脚本入口处加一次重复检查有重复直接退出不要“尽力而为”地插。踩坑五事务日志爆满同步跑到一半数据库不可用。现象是脚本执行到几十万行时整个实例变慢日志文件疯长。原因是单事务行数太大或者数据库是完整恢复模式但没做日志备份。解决是把批大小压到 3000 以内、每批提交同步前手动做一次日志备份同步期间临时切成大容量日志恢复模式同步完切回来。6. 增量同步用校验和代替全量比对把每次耗时压到分钟级全量比对在物料上万之后会越来越慢FULL OUTER JOIN两边各扫一遍十几万行跑几分钟很正常。真正上线跑的应该是增量只处理“上次同步之后变化过的记录”。难点在于 K3 WISE 的很多基础资料表没有可靠的修改时间字段FChangeTime不是每张表都有有也不一定在每次修改时更新。我的做法是给关键字段算一个校验和落到本地一张快照表里下次同步只比对校验和变化的行。-- 校验和快照表记录每行关键字段的 HASHBYTES 结果 IF OBJECT_ID(dbo.Sync_ItemHash,U) IS NULL CREATE TABLE dbo.Sync_ItemHash ( FItemClassID INT NOT NULL, FNumber NVARCHAR(60) NOT NULL, RowHash VARBINARY(32) NOT NULL, LastSeen DATETIME NOT NULL, CONSTRAINT PK_Sync_ItemHash PRIMARY KEY (FItemClassID, FNumber) ); -- 算出当前源端的行哈希和快照表比对只捞变化的行 WITH Cur AS ( SELECT FItemClassID, FNumber, HASHBYTES(SHA2_256, CONCAT(ISNULL(FName,), |, ISNULL(FDetail,0), |, ISNULL(FDeleted,0), |, ISNULL(FParentID,0), |, ISNULL(FUnitID,0))) AS RowHash FROM SrcDB.dbo.t_Item ) SELECT c.FItemClassID, c.FNumber FROM Cur c LEFT JOIN dbo.Sync_ItemHash h ON h.FItemClassID c.FItemClassID AND h.FNumber c.FNumber WHERE h.RowHash IS NULL OR h.RowHash c.RowHash;CONCAT里的分隔符不能省不加的话(AB,C)和(A,BC)会算出同一个哈希值这种碰撞概率低但一旦发生就是灵异事件级别的排查难度。ISNULL是为了让 NULL 和空串产生相同结果否则源端FName从 NULL 改成空串也会被判定为变化白白多跑一次。快照表用(FItemClassID, FNumber)做主键而不是FItemID是为了跨账套通用——同一个物料在源端和目标端的 ID 本来就不一样。增量模式下的三种处理策略我一般按资料类型分开用变化类型客户/供应商物料部门/职员新增主表扩展表插入主表插入单位走映射主表扩展表插入属性修改只更新扩展表更新名称、单位、禁用标志只更新扩展表删除/禁用只置 FDeleted1不物理删同左同左物理删除在任何同步方案里都是下策。历史上引用过这条资料的单据还在删掉主表记录会让历史单据打开时报“资料不存在”财务报表直接对不上。统一改成FDeleted 1前端列表默认过滤掉历史数据照常能查这是我在第一次迁移踩了这个坑之后定下的规矩。最后收一个习惯每次同步跑完我会把源端和目标端的有效记录数、层级深度分布、扩展表覆盖率三个数字记到一张日志表里和上一次的值并排看。数字对不上就先停别急着跑下一批。有次物料总数只差 3 条看着像是分页问题追下去发现是源端有 3 条物料的编码里带了不可见字符LTRIM也去不掉最后用REPLACE(FNumber, NCHAR(9), )才清干净。基础资料同步这事脚本只占三成剩下七成是校验和较真。希望帮到你。本文还有配套的精品资源点击获取