
简介面向需要批量处理 SQL Server 数据库脚本的开发者与运维人员这份用 C# 编写的导出导入工具包提供接近 SQL Server 2014 Management Studio「生成脚本」功能的实现思路可用于数据库结构备份、表与视图等对象脚本生成、跨环境迁移前的脚本整理也适合作为数据库生成脚本与备份类小工具的参考工程。压缩包共 66 个文件、约 2.16MB包含 C# 源码、项目解决方案与配置文件以及若干 SQL 脚本、编译后的程序集和可执行文件另有资源文件、缓存与调试产物便于直接运行或二次开发。已有约 1490 人学习下载。读者可从中了解窗体界面与数据库访问辅助类的相关组织方式参考脚本导出导入的流程设计、连接配置、参数传递与异常处理思路并借助现成工程快速搭建自己的数据库脚本生成工具。1. 从一台只装了 Express 引擎的机器说起上周把一套老系统的数据往新机器搬交付物就是一个 SQLSERVER脚本导出导入.zip本以为解压、跑脚本、结束结果第一遍就失败源库是 SQL Server 2016目标机上装的是 SQL Server 2022 Express导出时没改「服务器上的脚本版本」导进去直接报语法错误带中文的列全变成了问号。数据库生成脚本、导出导入这件事看着是右键菜单里点两下真正卡住人的从来是版本、编码、权限这些环境细节。这篇记录适合要把一整个库搬到另一个实例、或者要在 C# 程序里批量跑 SQL Server 脚本的人照着走能少返工几次。2. 版本对照SQL Server 2019/2022、SSMS 20 与 sqlcmd 18 怎么配装之前先把四件东西的角色分清楚后面所有问题几乎都能归到这四件里引擎负责存数据和执行脚本SSMS 负责生成脚本和查看问题sqlcmd 负责批量跑脚本驱动负责让 C# 这类程序连上去。很多人以为装个 SSMS 就齐了结果换台机器只有引擎没有客户端工具脚本生成不了或者只有旧版 sqlcmd编码参数不认。2.1 一张表决定装哪个版本版本搭配的核心矛盾只有一个方向脚本可以往同版本或更高版本导不能往低版本导。生成脚本向导会把「目标服务器版本」的语法写死在脚本里2022 生成的脚本里可能有 2019 不认的写法拿到 2016 上更是直接崩。组件建议版本用途主要限制SQL Server 引擎2019 或 2022 的 Developer / Express存放源库、回放脚本高版本脚本不能导入低版本引擎SSMS19.x 或 20.x生成脚本向导、对象浏览版本必须不低于引擎否则连不上或不显示新对象sqlcmdODBC 版 18推荐或旧版 17命令行批量执行脚本18 默认开启加密连接连接驱动ODBC Driver 18 for SQL Server、Microsoft.Data.SqlClient 5.x工具与 C# 程序连接新版默认EncryptTrue自签证书会连不上Engine 的版本号和兼容级别是两回事。SQL Server 2022 可以建一个compatibility_level 130的库来跑老语法但生成脚本时向导仍然按引擎版本选语法所以目标机是 2016 的话TargetServerVersion必须手动降下来。这一点在向导里藏得比较深在「高级」里默认值是你当前的引擎版本。SSMS 20 之后安装包不再附带引擎是纯客户端装完不会给你一个实例。有些人装完发现「服务里没有 MSSQLSERVER」就是这一步理解错了还得单独回去装引擎。我一般先装引擎、再装 SSMS最后补 sqlcmd 和 ODBC 驱动顺序反了会出现 SSMS 装了但连不上本机实例的尴尬。2.2 安装顺序与三条验证命令装完不要急着导数据先跑三条命令确认环境是通的。顺序是确认服务在跑、确认能连上、确认版本与兼容级别符合预期。# 1. 确认本机有哪些 SQL Server 服务状态是不是 Running Get-Service | Where-Object { $_.Name -like MSSQL* } | Select-Object Name, DisplayName, Status # 2. 用 Windows 身份连一次看引擎版本和补丁级别 sqlcmd -S localhost -E -Q SELECT VERSION AS ver, SERVERPROPERTY(ProductLevel) AS lvl-S localhost指定实例命名实例要写成localhost\SQLEXPRESS这种形式-E表示用当前 Windows 账号做信任连接不走账号密码-Q是执行完这条语句就退出适合一次性检查。如果这一步报「找不到网络路径」问题在服务没起或者实例名写错跟脚本没关系。-- 3. 看各库的兼容级别1302016140201715020191602022 SELECT name, compatibility_level, collation_name FROM sys.databases WHERE database_id 4;兼容级别决定了STRING_AGG、TRIM、APPROX_COUNT_DISTINCT这些语法能不能用也影响查询优化器的行为。回放脚本前把这一列和目标库对齐能省掉一大半「语法错误」的排查时间。排序规则collation也别忽略源库是Chinese_PRC_CI_AS、目标库是默认的SQL_Latin1_General_CP1_CI_AS时跨库 JOIN 会直接报排序规则冲突这个坑后面还会提。确认完这三条再装 sqlcmd 的 ODBC 版本。装完在命令行敲一次sqlcmd -?能打出版本号就说明 PATH 生效了如果提示「不是内部或外部命令」把C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\180\Tools\Binn加进环境变量重开终端。这里有个血泪经验改完环境变量一定要新开窗口旧窗口的 PATH 是启动时快照改完没反应是常态。3. 导出脚本生成脚本向导与 SMO 批量导出的参数取舍导出这一步决定了后面导入顺不顺。向导适合一次性、看得到的场景SMO 适合要重复跑、要写进流水线的场景。两条路的参数含义是一样的只是向导用勾选框SMO 用属性。3.1 生成脚本向导里必须改的五个选项右键数据库 → 任务 → 生成脚本前面几步都是「下一步」真正的功夫全在「设置脚本编写选项」里的「高级」按钮。默认值是按「仅限架构」和当前引擎版本来的直接点完成导出的东西往往不是你想要的。第一个必改项是「编写脚本的数据类型」三个值仅限架构、仅限数据、架构和数据。迁移整库选第三个只想灌数据到已有表选第二个。选「仅限数据」时要注意如果表上有 IDENTITY 列向导生成的 INSERT 会带SET IDENTITY_INSERT [dbo].[T] ON否则自增值对不上。第二个是「服务器上的脚本版本」把它降到目标引擎版本。这一步在源库版本高于目标库时是强制的否则脚本里会出现目标不认识的语法。第三个是「编写 DROP 和 CREATE」目标是空库时选 FALSE。选 TRUE 会在每个对象前加IF EXISTS ... DROP回放到已有数据的库上会静默删表这个失误代价很大。第四个是「要编写的对象」默认全选包含系统对象之外的所有东西但统计信息statistics建议关掉它会让脚本体积膨胀好几倍回放时还会因为索引还没建完而失败。第五个是输出方式单个文件、每个对象一个文件、还是输出到新建查询窗口。目标是流水线就选「每个对象一个文件」配合文件名的编号前缀排序手工排查就选单文件搜索方便。-- 向导生成的文件开头大致长这样这三行决定了回放行为 USE [SourceDb] GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[Order]( [Id] [int] IDENTITY(1,1) NOT NULL, [Remark] [nvarchar](200) NULL, CONSTRAINT [PK_Order] PRIMARY KEY CLUSTERED ([Id] ASC) ) ON [PRIMARY] GOUSE [SourceDb]是硬编码的库名回放到别的库必须先替换这一点在下一章展开。SET ANSI_NULLS ON和SET QUOTED_IDENTIFIER ON是创建对象时的会话设置会被记录在对象元数据里回放时如果客户端没开这两个选项索引视图和计算列会创建失败。所以后面用 sqlcmd 跑脚本时要加-I把 QUOTED_IDENTIFIER 打开。[nvarchar](200)这种带 N 前缀的类型是 Unicode 的但如果目标库排序规则不是 Unicode 兼容的插入中文仍然会丢字符。稳妥做法是源库和目标库的 collation 保持一致或者在导出前确认所有中文列都是nvarchar而不是varchar。3.2 用 SMO 批量导出把参数写进脚本要反复跑、要进 CI、要按 schema 分批导向导就不够用了。PowerShell 加载 SqlServer 模块后调 SMO参数和向导是同一套只是换成属性名。Import-Module SqlServer -ErrorAction Stop $serverName localhost $dbName SourceDb $outFile D:\dbexport\SourceDb_all.sql $srv New-Object Microsoft.SqlServer.Management.Smo.Server $serverName $db $srv.Databases[$dbName] $opt New-Object Microsoft.SqlServer.Management.Smo.ScriptingOptions $opt.ScriptSchema $true # 导出架构 $opt.ScriptData $true # 同时导出数据 $opt.ScriptDrops $false # 不生成 DROP 语句 $opt.WithDependencies $false # 依赖自己排别让 SMO 猜 $opt.Indexes $true $opt.Triggers $true $opt.ToFileOnly $true $opt.FileName $outFile $opt.AppendToFile $false # 目标引擎版本1302016, 1402017, 1502019, 1602022 $opt.TargetServerVersion [Microsoft.SqlServer.Management.Smo.SqlServerVersion]::Version130 $scr New-Object Microsoft.SqlServer.Management.Smo.Scripter $srv $scr.Options $opt # 排除系统对象按 schema 过滤时改这里的 Where 条件 $scr.Script($db.Tables | Where-Object { -not $_.IsSystemObject })TargetServerVersion对应向导里的「服务器上的脚本版本」是这段代码里最关键的一行。WithDependencies设成 true 会让 SMO 自己算依赖顺序听起来省事实际经常把表排在它引用的自定义类型前面我一般关掉按「类型 → 表 → 数据 → 索引/约束 → 视图 → 存储过程」自己排。ToFileOnly配合FileName是输出到文件AppendToFile $false保证第一次写入覆盖而不是追加否则跑两遍会得到两份重复的 CREATE TABLE。数据量很大时 SMO 生成脚本会比较慢因为它是逐行拼 INSERT几百万行以上的表更推荐 bcp 单独导数据文件。# 大表走 bcp字符模式 UTF-8 代码页避免中文乱码 bcp SourceDb.dbo.[Order] out D:\dbexport\Order.dat -S localhost -T -c -C 65001 -t | -r \n-c是字符模式对应-n原生模式跨版本更安全但不可读-T是信任连接-C 65001指定代码页为 UTF-8-t和-r分别是列分隔符和行分隔符导入时必须用完全一样的参数否则字段会错位。bcp 的好处是快坏处是它只搬数据不搬约束表结构还得靠脚本。4. 导入回放换实例、换库名后按依赖顺序把脚本跑干净导出只是拿到材料回放才是真正决定成败的一步。脚本从源库带过来一堆硬编码直接跑十有八九在第一条USE上就失败。4.1 建库、建登录脚本开头的三处必须先改第一处是USE [SourceDb]回放到新库要全部替换成目标库名。第二处是文件路径如果脚本里带了CREATE DATABASE ... ON (FILENAME ND:\data\SourceDb.mdf)那条路径在你机器上多半不存在。第三处是登录和用户脚本里如果只有CREATE USER而没有CREATE LOGIN用户会变成没有登录名的孤儿用户。-- 目标实例上先手工建库和登录别指望脚本自己搞定 CREATE DATABASE [TargetDb]; GO CREATE LOGIN [appuser] WITH PASSWORD N换成你的密码, CHECK_POLICY ON; GO USE [TargetDb]; GO CREATE USER [appuser] FOR LOGIN [appuser]; GO ALTER ROLE db_datareader ADD MEMBER [appuser]; ALTER ROLE db_datawriter ADD MEMBER [appuser]; GOCHECK_POLICY ON会强制密码复杂度用在正式环境本地验证图省事可以设 OFF。CREATE USER ... FOR LOGIN是按登录名映射如果是从别的实例还原过来的库用户已经存在但 SID 对不上这时用ALTER USER [appuser] WITH LOGIN [appuser]重新绑定比删了重建安全。批量替换库名用 PowerShell 一条命令就够注意先备份原文件再替换正则写错会把表名里的同名子串一起换掉。# 只替换整词 USE [SourceDb]避免误伤表名里的 SourceDb Get-ChildItem D:\dbexport\*.sql | ForEach-Object { (Get-Content $_.FullName -Raw -Encoding UTF8) -replace USE \[SourceDb\], USE [TargetDb] | Set-Content $_.FullName -Encoding UTF8 -NoNewline }-Raw把整个文件读成一个字符串不加的话按行处理跨行的替换会失效-Encoding UTF8必须显式指定Windows PowerShell 5.1 的默认编码是 ANSI读进来再写出去中文注释就成乱码了。这一步是很多人导入后中文变问号的真正原因比数据库排序规则还常见。4.2 执行顺序、GO、编码与退出码脚本之间的依赖顺序不能靠文件名字母序碰运气。按这个顺序排自定义类型和用户定义函数 → 表结构 → 数据 → 主键外键和索引 → 视图 → 存储过程 → 触发器 → 权限。外键放在数据之后建能避开「插入顺序违反约束」触发器放最后否则导数据过程中会被反复触发。GO不是 T-SQL 关键字是客户端工具认的批处理分隔符它不会被发给引擎。这件事在 C# 里自己拼脚本时会踩到后面单独说。#!/usr/bin/env bash set -euo pipefail # 任一脚本失败立即中断别让后面的脚本在脏数据上继续跑 SQLCMDsqlcmd -S localhost -d TargetDb -E -b -r1 -I -f 65001 -t 0 for f in $(ls /data/scripts/*.sql | sort); do echo 执行 $f $SQLCMD -i $f done-b是关键出错时返回非零退出码配合set -e才能中断不加的话 sqlcmd 会继续跑完所有脚本最后给你一堆错误日志却不知道哪一条才是根因。-r1把错误信息重定向到 stderr日志分流更清楚。-I打开 QUOTED_IDENTIFIER前面说过索引视图和计算列没它建不出来。-f 65001指定输入输出编码脚本里带中文时必须加。-t 0表示不设查询超时大表的 INSERT 跑十几分钟很正常默认超时会把任务打断在半路。还有一种情况是脚本本身太大几 GB 的单文件 sqlcmd 也吃不消。这时按对象拆成多个文件或者用-a调大包大小默认 4096 字节网络往返次数能降下来。4.3 C# 端把脚本跑起来连接字符串与 ExecuteNonQuery 的坑程序里跑脚本的场景很常见比如初始化一个租户库、或者部署时自动建表。C# 这边最容易踩两个坑连接字符串的加密默认值以及GO的分批。using System.IO; using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; // TrustServerCertificatetrue 只用于内网自签证书环境 const string cs Serverlocalhost;DatabaseTargetDb;Integrated Securitytrue; EncryptTrue;TrustServerCertificateTrue;Connect Timeout30;; var script File.ReadAllText(D:\dbexport\SourceDb_all.sql); // GO 是客户端分隔符必须自己切批单行独立的 GO 才算数 var batches Regex.Split(script, ^\s*GO\s*$, RegexOptions.Multiline | RegexOptions.IgnoreCase); using var conn new SqlConnection(cs); conn.Open(); foreach (var batch in batches) { if (string.IsNullOrWhiteSpace(batch)) continue; using var cmd new SqlCommand(batch, conn) { CommandTimeout 0 }; cmd.ExecuteNonQuery(); }EncryptTrue是 Microsoft.Data.SqlClient 4.0 之后的默认值从旧版 System.Data.SqlClient 迁过来的代码常常因为这个报「证书链不受信任」。测试环境加TrustServerCertificateTrue图快正式环境应该换成可信证书否则等于把加密关掉。CommandTimeout 0表示不限时建大索引、灌大数据时必须这么设默认 30 秒会半路抛异常留下一个半成品表。正则里的^\s*GO\s*$只匹配独占一行的 GO能避开字符串字面量里的 GO 被误切。如果脚本超过几十 MB一次性ReadAllText内存压力大改成分块读、按行判断批边界更稳。5. 避坑排查五条从现场记下来的翻车记录5.1 中文全变问号现象导完之后nvarchar列里的中文变成?英文和数字正常。原因有两层。脚本文件本身被存成了 ANSIGBK编码里面的中文字面量在读取时就已经错了或者目标库的 collation 是 Latin1 系varchar列存不下中文。前者更常见因为 PowerShell 的Set-Content默认不写 BOM编辑器打开时按本地编码猜。解决生成脚本时统一用 UTF-8 保存回放时sqlcmd -f 65001确认目标列的ORDER里没有从nvarchar被降级成varchar跨库操作前先对齐 collation或者临时在 JOIN 上加COLLATE DATABASE_DEFAULT。5.2 外键导致建表或插数据失败现象脚本跑到一半报「ALTER TABLE 语句与 FOREIGN KEY 约束冲突」或者表还没建完就报「对象不存在」。原因生成脚本时勾了「脚本所有对象」向导按字母序输出OrderDetail排到了Order前面或者外键和表结构混在一起数据还没插就先把约束建了。解决导出时按 schema 分组人为把表结构、数据、约束拆成三个文件用 SMO 时把WithDependencies关掉自己排已经生成的脚本可以用sp_rename之外的思路——先禁用外键ALTER TABLE ... NOCHECK CONSTRAINT ALL导完再WITH CHECK重新启用。5.3 连接直接报证书错误现象sqlcmd 或 C# 程序连不上错误信息里带「SSL Provider: 证书链是由不受信任的机构颁发的」。原因ODBC Driver 18 和 SqlClient 4.0 默认把Encrypt设成 yes而开发机的实例用的是安装时自签的证书。解决客户端连接串加TrustServerCertificatetrue或者 sqlcmd 加-C参数正式环境让实例挂可信证书别长期用绕过的方式。这个坑最容易被误判成账号权限问题实际上连登录都没走到。5.4 高版本生成的脚本在低版本引擎上跑不动现象CREATE TABLE之后的某条语句报「关键字 X 附近有语法错误」或者「无法识别的内置函数名称」。原因生成脚本时TargetServerVersion没降2022 的语法写进了给 2016 的脚本里或者用了 2019 才有的ALTER TABLE ... WITH (ONLINE ON)。解决导出前把目标版本明确告诉向导已经生成的脚本用SELECT VERSION对一遍把新语法替换成等价写法实在要跨大版本考虑中间过一道 2019 的实例做跳板。兼容级别只能让引擎容忍老语法不能让老引擎认识新语法。5.5 大表导出超时或内存被吃满现象SMO 生成脚本跑到 70% 卡住或者 PowerShell 进程内存一路涨到几个 G 然后崩掉。原因SMO 的ScriptData $true是把所有行拼成 INSERT 语句放在内存里百万行级别的表直接把字符串撑爆sqlcmd 执行超大单文件也类似。解决大表单独走 bcp 或SqlBulkCopy结构用 SMO 导数据用 bcp 搬或者分页导出用ORDER BY Id OFFSET ... FETCH每次拉十万行拼一个文件。别指望一条命令搞定全库物理上就不成立。6. 验证跑通从空库到数据一致的三个自检动作脚本跑完不等于迁移成功最常见的错觉是「没报错就是对的」。上线前我固定做三个比对几分钟的事能挡住九成的隐性错误。第一步比结构。用INFORMATION_SCHEMA统计源库和目标库的列数量差一列都说明某个对象没建出来。-- 在源库和目标库各跑一次把结果贴到两个窗口对比 SELECT s.name AS schema_name, t.name AS table_name, COUNT(c.column_id) AS col_count FROM sys.tables t JOIN sys.schemas s ON s.schema_id t.schema_id JOIN sys.columns c ON c.object_id t.object_id WHERE t.is_ms_shipped 0 GROUP BY s.name, t.name ORDER BY s.name, t.name;第二步比行数。sys.partitions的rows对堆表和聚集索引是准确的比COUNT(*)快得多几十亿行也能秒出。SELECT s.name AS schema_name, t.name AS table_name, SUM(p.rows) AS row_count FROM sys.tables t JOIN sys.schemas s ON s.schema_id t.schema_id JOIN sys.partitions p ON p.object_id t.object_id AND p.index_id IN (0, 1) GROUP BY s.name, t.name ORDER BY s.name, t.name;第三步比内容。行数一致但数据错位的情况确实会发生比如 bcp 的字段分隔符和文本里出现的竖线撞了。对关键表用CHECKSUM_AGG抽一遍两边值一致基本可以放心。自检项命令判定标准结构与列数INFORMATION_SCHEMA.COLUMNS统计源库目标库逐表相等行数sys.partitions求和差异为 0或有明确说明内容校验CHECKSUM_AGG(BINARY_CHECKSUM(*))抽样表的值完全一致对象数量sys.objects按 type 分组存储过程、视图、触发器数量一致-- 关键表做一次内容校验源库和目标库分别执行 SELECT CHECKSUM_AGG(BINARY_CHECKSUM(*)) AS chk FROM dbo.[Order] WITH (NOLOCK);BINARY_CHECKSUM(*)对每个列值算二进制校验和CHECKSUM_AGG再把这些值聚合起来任何一个字符不同结果就会变。缺点是nvarchar的排序规则敏感、大字段text、image会被忽略所以它只能当抽样手段不能替代全量核对。真要做严格对账还是得按主键区间分批跑EXCEPT。三个比对跑完再把应用连上去做一次真实读写一次 INSERT、一次带中文的 UPDATE、一次跨表 JOIN才算真的通了。我吃过一次亏脚本导入一切正常结果应用一连就报权限不足原因是我建了用户却忘了把db_executor之外的 schema 权限给全存储过程在dbo下能跑在别的 schema 下全部失败。从那以后我每次回放完都强制走一遍「新建一个只有读写权限的账号用它把核心存储过程挨个执行一次」不再只看管理员视角的结果。希望这些记录能帮到你少在环境上返工。本文还有配套的精品资源点击获取