简介:SQLSERVER脚本导出导入.zip 是一份面向 SQL Server 开发与运维人员的 C# 工具源码包,重点解决数据库对象脚本批量导出、导入执行以及备份迁移场景中的重复操作问题,功能目标与 SQL Server Management Studio 2014 的“生成脚本”相近。压缩包共 66 个文件,约 2.16MB,包含 10 个 cs 源码、8 个 sql 脚本以及 sln/csproj 工程文件,另有 dll、exe、config、resx 等运行与配置资源,便于直接查看项目结构或编译调试。已有 1490 人学习下载,说明其在数据库脚本化处理方向具有一定参考价值。读者可从中了解 WinForms 界面组织、SqlHelper 数据访问封装、脚本导出与导入执行的实现流程,并据此做二次开发、功能裁剪或迁移工具原型验证。
1. SQL Server 脚本导出导入:一个 zip 解决老库搬家的三件麻烦事
「SQL Server 脚本导出导入」这件事,真到要搬库那天才知道没有那么容易:手里只有一台快下线的实例和一个 .bak,目标机器版本更低、还要求不长时间停服;或者只想从生产库抽三张表给测试环境,既不能带上账号信息,也不能让自增主键冲突。这份 zip 收的就是这类场景的全套材料——生成脚本、导出导入、备份还原,外加一份 C# 里调用 SMO 与 SqlBulkCopy 的样例代码。它不适合拿来学 T-SQL 语法,适合手上正好有一个库要挪、要拆、要归档的人。下面按「拆包看结构 → 向导怎么勾 → 代码怎么写 → 坑在哪 → 怎么验」走一遍,每一步都给可抄的参数。
2. 拆包看结构:脚本、备份、C# 代码各自管哪一段
很多人拿到这类包的第一反应是把 .sql 全拖进 SSMS 一起按 F5,然后看着一屏红色报错发呆。问题不在脚本,在于没分清包里几类文件各自负责什么。脚本文件是「可读的、可进版本库的、跨版本相对安全的」搬运方式;备份文件是「完整但基本绑死版本」的快照;C# 那部分是「把手工点向导的动作固化成程序」的胶水层。三者不是替代关系,是覆盖不同场景。
2.1 包里的 .sql 脚本由哪几段拼成
一份由「生成脚本」向导产出的完整脚本,结构上基本是固定的五段:库级设置(USE [库名]、ANSI_NULLS、QUOTED_IDENTIFIER)、对象创建(CREATE TABLE、主键、默认值、检查约束)、索引与触发器、外键与其余约束、数据段(成批的INSERT)。这个顺序不是随便排的——建表必须先于外键,外键必须先于数据,否则父子表之间必然冲突。
所以读脚本时有个很实用的习惯:先看文件头有没有USE [目标库]。如果源库名和目标库名不一样,这三行就是第一个炸弹——脚本会老老实实往原库名里灌,跑到最后你发现数据没进目标库,还以为是权限问题。常见做法是把USE那行改成:setvar变量,或者干脆删掉,用sqlcmd -d指定目标库。
另外注意脚本里的GO。GO不是 T-SQL 语句,是客户端工具(SSMS、sqlcmd)的批处理分隔符。也就是说,任何通过 SqlCommand 直接执行脚本的 C# 代码,遇到GO都会报「'GO' 附近有语法错误」——这是新手最常遇到的一个错误,后面第 4 章会给拆分方案。
2.2 脚本、备份、分离附加三种方式怎么选
这三种方式的边界比想象中清晰,选错了会浪费掉整个下午:
| 方式 | 适用场景 | 主要限制 | 大致耗时量级 |
|---|---|---|---|
| 生成脚本(架构+数据) | 跨大版本、只迁部分对象、要进 Git 审阅 | 大表生成慢,脚本体积膨胀快 | 10 GB 以内可控,再往上明显变慢 |
| 备份 + 还原(.bak) | 整库搬迁、同版本或向上升级 | 不能还原到更低版本;需要目标盘空间翻倍 | 与数据量线性相关,通常最快 |
| 分离 + 附加(.mdf/.ldf) | 同机换实例、机房内快速搬 | 必须停机、文件权限要对、版本仍受限 | 分钟级,但停机窗口是硬成本 |
判断逻辑很简单:目标实例版本低于源库,只能走脚本(而且要在生成时把「为服务器版本编写脚本」调低);目标库只保留部分表,也只能走脚本;整库且版本不降,直接备份还原,别费劲生成几万个 INSERT。真正麻烦的是中间那档——库不大但表和约束特别多,脚本生成能跑但导入顺序老出错,这时候用 C# 按依赖顺序排一遍比手工调靠谱。
提示:脚本方式最大的价值不是搬数据,而是「可审查」。DDL 进版本库之后,谁在哪个环境加了个索引一目了然,这是 .bak 永远做不到的。
2.3 C# 那部分:SMO 与 SqlBulkCopy 是两条腿
包里 C# 代码的思路是两条腿走路:DDL 和中小表走 SMO 的Scripter,大表走SqlBulkCopy。为什么不全部用 SMO?因为ScriptData = true生成的本质还是INSERT语句,几百万行的时候它会在内存里拼出一个巨大的StringCollection,机器就开始明显卡顿。而SqlBulkCopy走的是 TDS 的批量插入协议,配合TableLock能跑到接近bcp的速度。
为什么又不全部用SqlBulkCopy?因为它只搬数据,不管表结构、约束、索引、触发器。你得先有表,才能灌数据。所以实际组合是:SMO 导出架构(ScriptSchema = true, ScriptData = false)→ sqlcmd 执行架构脚本 → SqlBulkCopy 灌数据 → 再单独脚本化索引和外键。这个顺序比「一次性生成全部」稳得多,失败时也能定位到具体是哪一段。
还需要提前说清一个版本问题:Microsoft.SqlServer.Management.Smo这一套程序集跟 SQL Server 版本是绑的,装了新版 SMSS 之后 GAC 里可能存在多个版本,编译时会报程序集冲突。工程里通常的做法是只用 NuGet 上的Microsoft.SqlServer.SqlManagementObjects包,不要再引用 GAC 里的 DLL,否则部署到没有装 SSMS 的机器上会直接找不到程序集。
3. SSMS 生成脚本向导:六个必须改的选项和批处理大小的取法
向导本身很好找:对象资源管理器里右键数据库 → 任务 → 生成脚本。真正让人出错的是后面那个「高级」按钮——它默认给的选项是「只导架构」,一个数据都不带,跑完你以为成功了,实际上表是空的。这一章把必须动的选项逐个说清,并给出批处理大小这种纯经验参数的取法。
3.1 从整库到单表:入口与对象选择的差别
向导第一步是「选择对象」。如果是从数据库节点右键进来的,默认选中「编写整个数据库及所有数据库对象的脚本」,适合整库搬迁;如果只是想抽几张表,选「选择特定数据库对象」,然后展开「表」逐个勾选。这里有个细节:勾了表之后,表上的索引、约束不会自动跟着走,需要靠后面的高级选项里的索引、外键、检查约束等开关控制。
第二步是输出方式,可以输出到「新建查询窗口」「文件」「剪贴板」,还能「另存为脚本文件」并指定路径。整库搬迁建议直接输出到文件,因为新建查询窗口在几万行 INSERT 的时候会卡住编辑器,甚至因为超时把脚本截断——截断的脚本还特别难发现,末尾少了几个GO而已。
第三步是「高级」里的选项。很多人到这里直接点下一步,然后就得到了一个只有CREATE TABLE没有INSERT的脚本,回头还怪工具不好用。
3.2 高级脚本编写选项里必须改的六项
| 选项名 | 默认值 | 建议值 | 不改的后果 |
|---|---|---|---|
| 要编写脚本的数据的类型 | 仅限架构 | 架构和数据 | 表是空的,一行数据没有 |
| 编写创建数据库的脚本 | False | 视需要 True | 目标实例上没有库,脚本第一句就报错 |
| 脚本 USE DATABASE | True | 目标库名不同时改 False | 数据灌回了源库 |
| 检查对象是否存在 | False | True | 目标库已有同名表时直接报错中断 |
| 编写扩展属性的脚本 | True | 保持 True | 列注释、MS_Description 全丢 |
| 为服务器版本编写脚本 | 当前实例版本 | 目标实例版本或更低 | 用了新语法,低版本实例直接语法错误 |
「检查对象是否存在」这一项值得单独说:打开之后,脚本会变成IF NOT EXISTS (SELECT * FROM sys.objects WHERE ...) BEGIN ... END的形式。它的好处是可重复执行,坏处是——它只判断不存在才建,不会帮你删掉旧的再重建。如果目标表已经存在且结构不一样,脚本会静默跳过建表,然后INSERT报列数不匹配。所以真要覆盖式导入,还是得自己先写一段DROP,或者把表名换成新名字。
「为服务器版本编写脚本」是跨版本搬迁的关键选项。它影响的是脚本里能否出现较新的语法(比如CREATE TABLE里的某些新特性、STRING_AGG、DATETIME2的精度写法等)。把目标实例版本设成实际版本,比设成「最新」安全得多;设成比目标还低的版本,通常会生成兼容性更好的写法,代价是丢掉一些新特性。
3.3 只导数据不导架构,以及批处理大小怎么定
只导数据的做法是:要编写脚本的数据的类型选「仅限数据」,同时把「检查对象是否存在」关掉。这样生成的脚本只有INSERT,适合往一张已经建好、结构完全一致的表里追加数据。注意「仅限数据」这个选项在向导的某些界面版本里藏在子菜单下,找不到就往「高级」里翻,名称是Types of data to script。
批处理大小(Batch size)控制的是每多少行 INSERT 之后输出一个GO。默认值很小(1 左右),意思是每行 INSERT 一个批。这个默认值会带来两个问题:一是脚本文件体积巨大,因为每行都带GO;二是执行时每条语句都是一次独立往返,几百万行能跑到怀疑人生。
我的取法是按行宽估算:单行数据 200 字节以内,Batch size给 500~1000;单行超过 1 KB(有大nvarchar(max)、varbinary字段),降到 100~200,否则单条INSERT拼出来的字符串太长,可能顶到批处理大小上限直接报错。宁小勿大,因为一条失败要重跑整批,而批大小 1000 时一批失败只影响 1000 行。
还有一个隐蔽点:INSERT语句里字符串前缀。生成出来的脚本对nvarchar字段会写成N'中文',如果目标列是varchar且库排序规则是中文的,通常没问题;但如果脚本文件本身保存成了 ANSI 编码,中文字符在写入文件那一刻就已经丢了。所以导出到文件后,第一件事是确认文件编码——带 BOM 的 UTF-8 或者 UTF-16 LE 都行,纯 ANSI 一定出问题。
4. C# 侧落地:SMO 生成脚本、SqlBulkCopy 灌数据、sqlcmd 兜底
手工点向导适合一次性的活儿,一旦要每周同步一次测试库,就得把它代码化。这一章给三段能直接跑的代码:SMO 生成脚本、SqlBulkCopy 批量导入、命令行 sqlcmd/bcp 兜底。三段之间有明确的分工,不要混着用。
4.1 用 SMO 生成脚本:ScriptingOptions 逐项配置
using System.Collections.Specialized; using System.Data.SqlClient; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo; // TrustServerCertificate=True 不能省:新版驱动默认要求加密握手,本地自签证书会直接连接失败 string connStr = "Server=127.0.0.1,1433;Database=DemoDb;User Id=sa;Password=***;TrustServerCertificate=True;"; var server = new Server(new ServerConnection(new SqlConnection(connStr))); Database db = server.Databases["DemoDb"]; var options = new ScriptingOptions { ScriptSchema = true, // 导出 CREATE TABLE / 约束 ScriptData = false, // 数据走 SqlBulkCopy,这里先关掉 ScriptDrops = false, Indexes = true, Triggers = true, ForeignKeys = true, CheckConstraints = true, ScriptDefaults = true, ExtendedProperties = true, // 列注释,丢了很难补 WithDependencies = true, // 让 SMO 记录对象间依赖,供后面排序 EnforceScriptingOptions = true, // 关键:不设 true,前面若干开关会被忽略 NoCommandTerminator = false, // 保留 GO IncludeHeaders = true, }; var scripter = new Scripter(server) { Options = options }; var urns = db.Tables.Cast<Table>() .Where(t => !t.IsSystemObject) .Select(t => t.Urn) .ToArray(); var tree = scripter.DiscoverDependencies(urns, true); var ordered = scripter.WalkDependencies(tree); var output = new StringCollection(); foreach (var node in ordered) { var obj = server.GetSmoObject(node.Urn) as IScriptable; if (obj == null) continue; foreach (string line in obj.Script(options)) output.Add(line); } File.WriteAllLines(@"D:\export\DemoDb_schema.sql", output.Cast<string>(), Encoding.UTF8);这段代码的核心是EnforceScriptingOptions = true。SMO 有一个不太显眼的行为:ScriptingOptions里某些开关只有在强制模式下才生效,不设这个值,你会发现Indexes = true写了跟没写一样,导出来的脚本里索引全没了。这个坑排查起来特别费时间,因为它不报错,只是安静地少东西。
DiscoverDependencies和WalkDependencies这两步负责排序。第二个参数true表示「只包含依赖闭包内的对象」,如果不传或传false,返回的列表顺序可能不满足外键约束的要求。WalkDependencies返回的DependencyTreeNode是按依赖排好的,父表在前、子表在后,直接按顺序脚本化就能避免第 5 章要讲的顺序问题。
ScriptData = false是有意为之:SMO 生成数据脚本时会把所有行拼进内存,几十万行就开始靠内存支撑了。数据的活儿交给 4.3 的SqlBulkCopy,分工清晰,失败也能各自重试。
4.2 执行顺序:架构脚本怎么喂给数据库
上一步产出的DemoDb_schema.sql里是有GO的,不能用SqlCommand.ExecuteNonQuery直接执行。两种处理方式:
using System.Data.SqlClient; using System.Text.RegularExpressions; string script = File.ReadAllText(@"D:\export\DemoDb_schema.sql"); // 按行首的 GO 拆分,注意 GO 后面可能跟次数(GO 3),先统一成单次 string[] batches = Regex.Split(script, @"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase); using var conn = new SqlConnection("Server=127.0.0.1,1433;Database=DemoDb;User Id=sa;Password=***;TrustServerCertificate=True;"); conn.Open(); foreach (string batch in batches) { if (string.IsNullOrWhiteSpace(batch)) continue; using var cmd = new SqlCommand(batch, conn) { CommandTimeout = 0 }; // 0 = 不超时,建大索引时必设 cmd.ExecuteNonQuery(); }CommandTimeout = 0这一句在建大表聚簇索引的时候是必需的,默认 30 秒,一个几 GB 的表建索引轻松超过,然后你会收到「超时时间已到」——但那句 DDL 其实在服务端还在跑,重试会撞上「对象已存在」。这类半途状态是最难收拾的,所以宁可设成不超时,靠人工看进度。
另一种更省事的做法是直接调sqlcmd,它对GO、变量、错误码都处理好了:
# -b:遇错返回非零退出码,脚本化调用必加,否则失败也返回 0 # -I:开启 QUOTED_IDENTIFIER,带过滤索引和计算列的表必须有 # -f 65001:输入文件按 UTF-8 解析,不加的话中文会变问号 sqlcmd -S 127.0.0.1,1433 -U sa -P "***" -d DemoDb -b -I -f 65001 -i "D:\export\DemoDb_schema.sql"-b这个参数值得强调。不加它,sqlcmd即使中间报错也返回 0,你在 CI 里看到「构建成功」,实际库是空的。加上之后,第一个错误就会中断并返回非零,脚本化流程才能正确失败。
4.3 SqlBulkCopy 导入:批次、事务、超时三个参数
数据导入用SqlBulkCopy,关键在于三个参数:BatchSize、BulkCopyTimeout、以及构造时传进去的事务。
using System.Data.SqlClient; var src = new SqlConnection("Server=10.0.0.11;Database=DemoDb;User Id=reader;Password=***;TrustServerCertificate=True;"); var dst = new SqlConnection("Server=127.0.0.1,1433;Database=DemoDb;User Id=sa;Password=***;TrustServerCertificate=True;"); src.Open(); dst.Open(); var readCmd = new SqlCommand("SELECT * FROM dbo.OrderDetail WHERE CreateTime >= @t", src); readCmd.Parameters.AddWithValue("@t", new DateTime(2024, 1, 1)); using var reader = readCmd.ExecuteReader(); using var tran = dst.BeginTransaction(); var bulk = new SqlBulkCopy(dst, SqlBulkCopyOptions.TableLock | SqlBulkCopyOptions.KeepIdentity, tran) { DestinationTableName = "[dbo].[OrderDetail]", BatchSize = 50000, // 每 5 万行提交一次到服务端 BulkCopyTimeout = 0, // 0 表示不超时,大表必设 EnableStreaming = true, // 边读边发,不在客户端缓存整表 }; bulk.NotifyAfter = 10000; // 每 1 万行触发一次进度回调 bulk.SqlRowsCopied += (s, e) => Console.WriteLine($"已写入 {e.RowsCopied} 行"); // 源列名和目标列名不一致时必须显式映射,否则按序号对,错位了还不报错 bulk.ColumnMappings.Add("OrderId", "OrderId"); bulk.ColumnMappings.Add("ProductId", "ProductId"); bulk.ColumnMappings.Add("Amount", "Amount"); try { bulk.WriteToServer(reader); tran.Commit(); } catch { tran.Rollback(); throw; }参数说明逐个来。BatchSize = 50000是「每次往服务端提交多少行」,不是「缓存多少行」——设太小网络往返次数多,设太大单次事务日志暴涨,50 万行以上的表建议 2 万到 5 万之间试跑一次看日志增长速度。BulkCopyTimeout = 0关掉超时,因为在目标端有索引和触发器的情况下,50 万行可能跑十几分钟。EnableStreaming = true让它在读取端边读边发,避免客户端内存占用。
SqlBulkCopyOptions的两个值要留意:TableLock拿表级锁,导入期间别人查不了这张表,但速度能快好几倍,做数据迁移时通常可接受;KeepIdentity保留源表的自增值,不加的话目标表会重新生成标识——如果下游有依赖这些 ID 的关联表,不加就是数据错乱。与之相对,如果你希望重新生成 ID,就别加KeepIdentity,但要先保证没有外键指向它。
ColumnMappings那三行看起来啰嗦,其实是保命用的。不显式映射时,SqlBulkCopy按列的序号对应,源表多一个列、或者列顺序变了,数据就整体错位,而且它不报错——你会发现Amount里存的是商品名。这种静默错误只能靠第 6 章的对账发现。
4.4 bcp 兜底:几千万行时的命令行方案
当数据量到了一定程度,SqlBulkCopy也开始吃力,就该上bcp了。它是原生工具,走的是同一套批量协议,但没有 .NET 那一层开销:
# 导出:-c 字符模式,-t 列分隔符,-r 行分隔符,-b 每批行数 bcp "DemoDb.dbo.OrderDetail" out "D:\export\OrderDetail.dat" \ -S 10.0.0.11,1433 -U reader -P "***" -c -t "|" -r "\n" -b 100000 # 导入:-h "TABLOCK" 拿表锁提速,-F 1 表示从第一行开始 bcp "DemoDb.dbo.OrderDetail" in "D:\export\OrderDetail.dat" \ -S 127.0.0.1,1433 -U sa -P "***" -c -t "|" -r "\n" -b 100000 -h "TABLOCK" -e "D:\export\err.log"-e指定的错误日志文件不要省。bcp遇到类型转换失败的行默认是跳过并继续的,跑完它只告诉你「复制了 N 行」,跳过了多少要看错误日志。不加-e的时候这些信息就打屏,量大了直接滚没。字符模式下还要注意分隔符冲突——如果某个nvarchar字段内容里本身带|,导入就错列,这时候要么换一个数据里绝对不会出现的分隔符(比如\x1f),要么改回原生模式-n。
5. 避坑排查:中文乱码、外键顺序、标识列这五处最容易翻车
这一章的五条都是实际操作中遇到过的,按「现象 → 原因 → 解决」写,遇到对应症状直接对号入座就行。
5.1 中文变问号,以及排序规则冲突
现象一:导入完成后,所有nvarchar字段的内容变成一排?,英文和数字正常。
原因:三个环节任一断链都会这样——脚本文件被存成了 ANSI 编码(中文在写入时已经丢失);INSERT语句里的字符串没加N前缀('中文'被当成varchar解析);或者用sqlcmd执行时没指定输入编码,它按当前代码页解析文件。
解决:脚本统一存 UTF-8 with BOM 或 UTF-16 LE;字符串字面量前面必须有N;sqlcmd加-f 65001。三步都做到,基本不会再出现。生成脚本向导在导出时默认会带N前缀,但如果中途用编辑器另存过,BOM 就可能被剥掉——所以改完脚本第一件事是用十六进制查看器看前三个字节是不是EF BB BF。
现象二:跨库 JOIN 或往临时表插数据时报Cannot resolve the collation conflict between "Chinese_PRC_CI_AS" and "SQL_Latin1_General_CP1_CI_AS"。
原因:源库和目标库的排序规则不一致,JOIN 的两边列排序规则不同,SQL Server 没法直接比较。这在「从老实例搬到新实例」时非常常见,因为新建库时很多人直接用了实例默认排序规则。
解决:临时办法是在比较或 JOIN 的列后面加COLLATE DATABASE_DEFAULT,让它对齐当前库;根治办法是建库时显式指定COLLATE Chinese_PRC_CI_AS(或与源库一致),别依赖实例默认值。注意COLLATE加在 WHERE 条件上会导致索引失效,大表上要谨慎。
5.2 外键冲突与标识列报错
现象一:INSERT 语句与 FOREIGN KEY 约束 "FK_Order_OrderDetail" 冲突,或者INSERT 语句与 CHECK 约束冲突。
原因:脚本按表名的字母序执行,子表排在父表前面;或者数据本身在源库就是「脏」的(历史遗留的孤儿行,源库里外键是NOCHECK状态)。
解决:短期方案是把整段数据导入包在「先禁用、后启用」里——导入前对目标库执行EXEC sp_MSforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL',导完再ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL。注意WITH CHECK不能省,只写CHECK的话约束虽然启用了,但不会校验历史行,等于没有真正恢复。长期方案是用 4.1 的WalkDependencies排序,从源头保证顺序。
现象二:当 IDENTITY_INSERT 设置为 OFF 时,不能为表 'OrderDetail' 中的标识列插入显式值。
原因:生成的脚本里INSERT包含了自增列的值,但脚本没有打开IDENTITY_INSERT。向导生成的脚本通常会自动加SET IDENTITY_INSERT [dbo].[表] ON,但如果手工拼脚本或者用了ScriptData的某些选项组合,这段可能缺失。
解决:在数据段前后手工补SET IDENTITY_INSERT [dbo].[表名] ON;/OFF;,一个会话同一时刻只能对一张表开启,多表要成对切换。走SqlBulkCopy的话对应的是SqlBulkCopyOptions.KeepIdentity。
5.3 大表脚本生成卡死与静默截断
现象:向导跑到一半没了动静,或者「新建查询窗口」里的脚本明显比预期短,末尾缺了内容,但没有报错。
原因:生成脚本是先把所有INSERT拼进内存再输出的,大表加上宽行,几十万行就能让 SSMS 占用好几 GB;查询窗口有显示行数上限,超出的部分不显示但仍然在内存里,看起来就像被截断了。
解决:超过 10 GB 或单表超过 500 万行的场景,不要用向导生成数据脚本,改走bcp导出 +bcp导入,或者 4.3 的SqlBulkCopy分批。如果必须用脚本,就按主键区间分批:WHERE Id BETWEEN 1 AND 500000,一轮一轮生成,每轮一个独立文件,最后拼起来执行。这样还有个附带好处:某一批失败时可以只重跑那一批。
6. 导入之后怎么验:行数对账与校验值比对
导入完成不等于迁移成功,我自己有过一次惨痛经历:脚本跑完没有任何报错,业务方用了两天才发现某张表的金额列整体错位,原因是ColumnMappings没写、源表中间加过一列。从那以后我每次迁移都强制走一遍对账,用时不到两分钟,能挡住大部分静默错误。
第一层是行数对账,最简单也最有效:
-- 源库和目标库各跑一遍,逐表比对行数 SELECT 'OrderDetail' AS TableName, COUNT_BIG(*) AS RowCnt FROM dbo.OrderDetail UNION ALL SELECT 'OrderMain', COUNT_BIG(*) FROM dbo.OrderMain UNION ALL SELECT 'Product', COUNT_BIG(*) FROM dbo.Product;行数对不上就是硬错误,不用往下查了,先看是不是有批处理提前中断、或者源表在导出期间还在写入。行数一致也不代表数据一致——这就是第二层校验值要解决的:
-- CHECKSUM_AGG 对整表算一个聚合校验值,两边不一致就说明有行内容不同 SELECT CHECKSUM_AGG(BINARY_CHECKSUM(*)) AS Chk, COUNT_BIG(*) AS Cnt FROM dbo.OrderDetail;BINARY_CHECKSUM(*)会跳过text、ntext、image、xml这几类列,也不适合对浮点列做严格比对(float的二进制表示在两侧可能因计算路径不同而微小差异)。所以更稳的做法是只对关键列算校验:BINARY_CHECKSUM(OrderId, ProductId, Amount, CreateTime),字段类型是定长数值或datetime2的时候结果可靠。
真出现校验值不一致、行数却相同的场景,下一步是定位到具体行。做法是两边各自算一个行级校验并落到临时表,再全外连接找差异:
-- 两边各建一次 #chk,然后找只在一侧出现的行或校验值不同的行 SELECT ISNULL(a.OrderId, b.OrderId) AS OrderId FROM #chk_src a FULL OUTER JOIN #chk_dst b ON a.OrderId = b.OrderId WHERE a.OrderId IS NULL OR b.OrderId IS NULL OR a.Chk <> b.Chk;这套流程我在几个项目里都跑过,记录进一张对账表,把「表名、源行数、目标行数、源校验、目标校验、比对时间」几列留下来,下次迁移直接看上一轮的基线,能快速判断这次是新增数据还是真的丢了数据。
最后一个习惯:把导出的架构脚本按库名/日期放进版本库,数据脚本不进(体积太大),只留生成它的那套 C# 代码和参数。这样过三个月再回来看,能准确复现当时是怎么导的,而不是对着一堆export_final_v2.sql猜。这份包里的脚本和 C# 样例我建议按第 3 章的选项先跑通一遍最小流程,再按第 4 章换成代码,别一上来就对着几百兆的库试。希望帮到你。
本文还有配套的精品资源,点击获取