简介:面向C#开发者与SQL Server运维人员的脚本导出导入工具源码包,功能参照SQL Server 2014 Management Studio的“生成脚本”,可用于数据库对象批量导出、结构与数据迁移,也适合二次开发并集成到自有管理平台。压缩包共66个文件、约2.16MB,以C#源码、dll与exe程序、SQL脚本和config配置为主,另含解决方案、资源及调试缓存文件,既能阅读实现,也可直接运行验证。已有1490人学习下载。读者可获得可运行的导出导入实现、数据库脚本生成思路、数据访问封装与WinForm界面组织方式,并据此排查连接配置、批量生成和备份迁移中的常见问题。
1. 一次迁移卡在“对象已存在”说起:SQL Server 脚本导出导入到底解决什么
上周帮一个做内部管理系统的团队做数据库迁移:开发在源库右键生成了整套 SQL Server 脚本导出导入文件,交给运维在目标库执行。前半段建表顺利,跑到存储过程时开始报“数据库中已存在名为 ‘usp_GetOrder’ 的对象”,再往后数据脚本又因为外键顺序插不进去,最后只进去一半数据。问题不在脚本本身,而在“生成脚本”时那几个勾选项和导入顺序没人当回事。这份SQLSERVER脚本导出导入.zip就是围绕数据库生成脚本、数据库导出导入、数据库备份和 C# 调用 SQL Server 脚本这几件事整理的一套可落地材料。适合正在做测试环境搭建、跨环境迁移、备份恢复演练,或者想把手工导入流程固化成 C# 小工具的从业者。下面按“包里有什么、怎么生成、怎么搬数据、怎么用 C# 跑、哪里容易翻车、上线前怎么验”走一遍。
2. 资源包拆解:脚本、备份和 C# 调用各自负责哪一段
拿到压缩包先别急着双击运行,先看目录里几类文件的分工。常见的组织方式是按动作分文件夹:生成脚本类、数据导出类、C# 调用示例类、备份还原类。不同资料包命名不一样,但文件类型跑不出这几样:.sql脚本、.bak备份、.fmt或.bcp格式文件、.cs示例代码。先把每一类对应到它擅长的场景,后面执行时才不会拿备份去干脚本的活,也不会拿脚本去替代整体备份。
2.1 先看目录:四类文件对应四个动作
我一般先列一遍扩展名,再决定先跑哪条路。.sql是结构和数据的文本表达,适合跨版本、跨环境、需要进 Git 审计的场景;.bak是数据库整体备份,适合同版本、快速整体恢复;.fmt是 BCP 的格式定义文件,用来解决分隔符和字段顺序问题;.cs是 C# 侧调用脚本的入口,通常负责读文件、拆批、执行、记日志。资源包里如果同时放了这四类,说明它想覆盖“手工导入”和“程序化导入”两条线,不是单纯给几个建表语句。
| 文件类型 | 典型用途 | 适合谁 | 主要限制 |
|---|---|---|---|
.sql生成脚本 | 建库、建表、视图、存储过程、数据插入 | 开发、测试、迁移实施 | 大文件执行慢,依赖顺序要自己排 |
.bak备份 | 整库恢复、灾备演练 | 运维、DBA | 版本和排序规则要匹配,还原后可能孤立用户 |
.fmt/.bcp | 大表数据导出导入 | 数据迁移、批量初始化 | 字段顺序、分隔符、字符集要严格对齐 |
.cs示例 | 自动化执行脚本、批量导入 | 后端开发、工具开发 | 需要自己处理 GO 拆分、事务和超时 |
看目录时还要留意有没有README或说明文件。如果没有,就按“先结构后数据”的顺序试跑:先找只含CREATE的脚本,再找含INSERT的脚本,最后找 C# 示例。不要一上来跑全量脚本,尤其不要在生产库上试。
2.2 生成脚本和备份不是二选一:选型看目标端条件
很多人会问:既然有备份,为什么还要生成脚本?反过来也一样。关键看目标端能不能还原备份。如果源库和目标库版本接近、目标服务器有相同目录权限、你只需要整体恢复,.bak最快,一条RESTORE DATABASE就完事。但如果你要把结构放进代码仓库、只迁移部分表、目标库版本比源库低、或者需要把脚本交给没有备份权限的人执行,生成脚本更合适。
我的习惯是:整库迁移优先备份,跨版本或需要审计优先脚本,大表数据单独走 BCP。资源包把两种方式都放进去,不是让你二选一,而是让你按阶段组合。比如先用生成脚本建好空库和对象,再用 BCP 灌大表数据,最后用备份做一次兜底。这里有个边界:生成脚本里的CREATE DATABASE语句常带源库的文件路径,目标服务器如果没有对应盘符会直接报错。遇到这种情况,我一般把建库语句摘出来,手动改路径,或者只生成对象脚本,不生成数据库脚本。
2.3 C# 调用脚本的入口在哪:先看连接和拆分逻辑
资源包里的 C# 示例通常不会只有一个Program.cs,常见做法是拆成连接配置、脚本读取、批处理执行、日志记录四块。你拿到后先找SqlConnection出现在哪,再看它怎么读.sql文件。如果示例是File.ReadAllText一次读入再Split("GO"),只能算演示级,碰到大脚本会吃内存,碰到字符串里出现“GO”还会误拆。合格一点的写法是按行读,正则匹配独占一行的GO,再逐批执行。
using System; using System.Collections.Generic; using System.Data.SqlClient; using System.IO; using System.Text.RegularExpressions; public static class SqlScriptRunner { // 只匹配独占一行的 GO,可带重复次数,忽略大小写 private static readonly Regex GoRegex = new Regex(@"^\s*GO\s*(\d+)?\s*$", RegexOptions.IgnoreCase | RegexOptions.Compiled); public static void ExecuteScript(string connectionString, string scriptPath) { var batches = new List<string>(); var current = new List<string>(); foreach (var line in File.ReadLines(scriptPath)) { if (GoRegex.IsMatch(line)) { if (current.Count > 0) { batches.Add(string.Join(Environment.NewLine, current)); current.Clear(); } continue; } current.Add(line); } if (current.Count > 0) batches.Add(string.Join(Environment.NewLine, current)); using (var conn = new SqlConnection(connectionString)) { conn.Open(); foreach (var batch in batches) { using (var cmd = new SqlCommand(batch, conn)) { cmd.CommandTimeout = 0; // 大脚本不设超时,靠分批控制 cmd.ExecuteNonQuery(); } } } } }这段代码的重点不是“能跑”,而是两个边界:File.ReadLines按行流式读取,不会把几百 MB 脚本一次性塞进内存;GoRegex只认整行GO,不会把INSERT INTO Log VALUES ('GO')拆坏。CommandTimeout = 0表示不超时,适合一次性执行大脚本,但如果脚本里有长时间锁,最好还是设一个上限并在外层加重试。资源包里的 C# 示例如果用了ReadAllText,你可以按这个思路改掉,后面第 4 章还会展开事务和错误捕获。
3. 用 SSMS 生成建库脚本:四个选项和幂等改造
生成脚本这件事,SSMS 的入口很浅,但“高级”里的选项决定了脚本能不能重复执行、能不能带数据、会不会把目标库写死。我见过最常见的翻车是:生成时没勾“检查对象是否存在”,目标库执行第二遍就报对象已存在;或者勾了“架构和数据”,但数据插入顺序排在主键之前,外键直接冲突。下面按入口、选项、幂等改造、依赖顺序四步走。
3.1 生成脚本的入口和关键选项
在 SSMS 里右键源数据库,任务 -> 生成脚本,进入向导后不要一路下一步。先选“特定数据库对象”,再点“高级”,把下面几项定死。不同 SSMS 版本菜单文字略有差异,但核心选项跑不出这张表。
| 选项 | 推荐值 | 原因 |
|---|---|---|
| 编写脚本的数据类型 | 仅架构 / 架构和数据分两次生成 | 结构和数据分开跑,出错好回滚 |
| 脚本检查对象是否存在 | True | 生成IF NOT EXISTS判断,支持重复执行 |
| 编写脚本的服务器版本 | 与目标库一致或更低 | 避免生成目标库不支持的语法 |
| 为所选数据库中的所有对象编写脚本 | 按需 | 全选容易把无关对象带进去 |
| 编写脚本的数据库引擎类型 | 标准版或企业版按目标定 | 避免分区、压缩等特性不兼容 |
| 包含 IF NOT EXISTS | True | 建表、建索引、建存储过程都带判断 |
我一般分两次生成:第一次“仅架构”,文件名带_schema;第二次“仅数据”,文件名带_data。不要一次生成“架构和数据”一个大文件,后面排错时你根本不知道卡在建表还是卡在插数据。生成完先打开文件看头部有没有USE [源库名],如果有,目标库执行前要么替换成目标库名,要么在连接里指定目标库并删掉这行。资源包里如果有现成脚本,也按这个标准检查一遍。
3.2 把脚本改成可重复执行:表、索引、存储过程各一套模板
SSMS 勾了“检查对象是否存在”后,生成脚本通常自带判断,但有些对象判断不完整,尤其是索引和约束。我一般会手工补一层,保证脚本跑第二遍不报错。下面三个模板是迁移里最常用的。
-- 表:先判断对象是否存在,再建表 IF NOT EXISTS ( SELECT 1 FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Orders]') AND type = N'U' ) BEGIN CREATE TABLE [dbo].[Orders]( [OrderId] INT IDENTITY(1,1) NOT NULL, [CustomerId] INT NOT NULL, [Amount] DECIMAL(18,2) NOT NULL, [CreatedAt] DATETIME2(0) NOT NULL CONSTRAINT [DF_Orders_CreatedAt] DEFAULT (SYSUTCDATETIME()), CONSTRAINT [PK_Orders] PRIMARY KEY CLUSTERED ([OrderId]) ); END GO -- 索引:按索引名判断,避免重复创建 IF NOT EXISTS ( SELECT 1 FROM sys.indexes WHERE name = N'IX_Orders_CustomerId' AND object_id = OBJECT_ID(N'[dbo].[Orders]') ) BEGIN CREATE NONCLUSTERED INDEX [IX_Orders_CustomerId] ON [dbo].[Orders]([CustomerId]); END GO -- 存储过程:用 CREATE OR ALTER(SQL Server 2016 SP1 及以上)或先 DROP 再 CREATE IF OBJECT_ID(N'[dbo].[usp_GetOrder]', N'P') IS NULL EXEC(N'CREATE PROCEDURE [dbo].[usp_GetOrder] AS BEGIN SET NOCOUNT ON; SELECT 1; END'); ELSE EXEC(N'ALTER PROCEDURE [dbo].[usp_GetOrder] AS BEGIN SET NOCOUNT ON; SELECT 1; END'); GO表模板里OBJECT_ID加type = 'U'是为了只认用户表,避免同名的其他对象干扰。索引模板必须同时限定name和object_id,因为不同表可以有同名索引。存储过程模板用了动态 SQL 包CREATE和ALTER,是为了在“不存在”和“已存在”两种情况下都能走通;如果你用的版本支持CREATE OR ALTER,直接写更干净。注意GO不是 T-SQL 语句,是 SSMS 和 sqlcmd 的批处理分隔符,C# 里执行时必须拆掉,后面第 4 章会讲。
3.3 依赖顺序和跨库引用怎么处理
生成脚本不会帮你排好所有依赖。对象之间的引用顺序错了,执行到一半就会报“无效的对象名”。我一般按下面这个顺序分文件,每个文件一个批次,跑完一个再跑下一个:
- 数据库和文件组(如果必须建库)
- 架构(
CREATE SCHEMA) - 表(只建表,不带外键和索引)
- 主键、唯一约束
- 外键、检查约束
- 非聚集索引
- 触发器
- 函数、存储过程、视图
- 数据插入(按外键依赖顺序,先父表后子表)
跨库引用是另一个坑。生成脚本里如果出现[SourceDb].[dbo].[Table]这种三部分命名,目标库如果没有同名数据库,执行会失败。常见做法是把脚本里的源库名替换成目标库名,或者把跨库查询改成同库查询。资源包里如果有 C# 执行工具,可以在执行前做一次文本替换,但别用简单的string.Replace全局替换,容易把数据内容里的库名也改掉。更稳的是只替换USE [开头和FROM [、JOIN [后面的库名,或者干脆在生成脚本时选“不编写 USE 语句”。
4. 数据搬运与 C# 批处理:BCP 参数、GO 拆分和事务边界
结构建好后,数据怎么进去是第二个大头。小表用生成脚本里的INSERT没问题,几百万行的表再用INSERT一条条跑,导入时间能按小时算。资源包里如果带了 BCP 示例和 C# 执行脚本,大概率就是为大表准备的。这一章把 BCP 命令行和 C# 批处理放在一起讲,因为实际迁移里经常是“BCP 灌数据,C# 跑结构脚本和校验脚本”。
4.1 BCP 导出导入的命令行参数
BCP 是 SQL Server 自带的批量复制工具,不走 SSMS 界面,适合脚本化和定时任务。导出命令一般长这样:
# 导出:宽字符模式,竖线分隔,每 10000 行提交一次 bcp "SalesDb.dbo.Orders" out "D:\migrate\Orders.dat" \ -S "tcp:127.0.0.1,1433" \ -U "sa" \ -P "YourPassword" \ -w \ -t "|" \ -r "\n" \ -b 10000 # 导入:带错误文件,最多允许 100 条错误,失败行写到 error.txt bcp "SalesDb.dbo.Orders" in "D:\migrate\Orders.dat" \ -S "tcp:127.0.0.1,1433" \ -U "sa" \ -P "YourPassword" \ -w \ -t "|" \ -r "\n" \ -b 10000 \ -e "D:\migrate\error.txt" \ -m 100参数含义:-w使用 Unicode 字符集,避免中文乱码;-t指定列分隔符,-r指定行分隔符;-b是每批行数,大表调大可以减少日志和网络往返,但别超过 100000,否则单批失败重试成本高;-e把失败行写进错误文件,-m是最大错误数,超过就中止。导入前最好先建一个空表,字段顺序和数据类型与导出表一致,BCP 默认按位置映射,不按列名映射。如果两边字段顺序不同,用-f格式文件显式指定列顺序。
4.2 C# 执行 SQL 脚本怎么拆 GO
C# 里执行.sql文件,第一个拦路虎是GO。SqlCommand一次只能执行一个批处理,把带GO的整段文本丢进去会报语法错误。常见写法有三种:按行正则拆、用 SMO 的ExecuteNonQuery、调用sqlcmd外部进程。资源包里的示例如果够轻量,一般是第一种。
using System; using System.Collections.Generic; using System.Data.SqlClient; using System.IO; using System.Text; using System.Text.RegularExpressions; public static class BatchExecutor { private static readonly Regex GoLine = new Regex(@"^\s*GO\s*(\d+)?\s*$", RegexOptions.IgnoreCase | RegexOptions.Compiled); public static int ExecuteFile(string connStr, string filePath, int batchSize = 50) { var batches = new List<string>(); var buffer = new StringBuilder(); int executed = 0; using (var reader = new StreamReader(filePath, Encoding.UTF8, true)) { string line; while ((line = reader.ReadLine()) != null) { if (GoLine.IsMatch(line)) { if (buffer.Length > 0) { batches.Add(buffer.ToString()); buffer.Clear(); } } else { buffer.AppendLine(line); } } } if (buffer.Length > 0) batches.Add(buffer.ToString()); using (var conn = new SqlConnection(connStr)) { conn.Open(); foreach (var sql in batches) { using (var cmd = new SqlCommand(sql, conn)) { cmd.CommandTimeout = 300; try { cmd.ExecuteNonQuery(); executed++; } catch (SqlException ex) { Console.WriteLine($"批处理第 {executed + 1} 段失败,行号 {ex.LineNumber}:{ex.Message}"); throw; } } } } return executed; } }这里用StreamReader逐行读,Encoding.UTF8加detectEncodingFromByteOrderMarks: true是为了兼容带 BOM 的脚本。GoLine正则允许GO 5这种带次数的写法,虽然SqlCommand不支持次数,但匹配到后可以循环执行五次。SqlException.LineNumber能告诉你失败在批处理内的第几行,但注意它是相对当前批处理的行号,不是整个文件的行号,排查时要结合批处理序号定位。CommandTimeout = 300是给中等脚本留的余量,超大脚本还是建议拆文件分批跑。
4.3 事务、超时和错误捕获
C# 执行脚本时,最容易犯的错是“一个事务包所有批处理”。脚本里本身可能有BEGIN TRAN,外层再包一层会嵌套;更麻烦的是大脚本一个事务跑下来,日志文件暴涨,失败回滚要等很久。我的习惯是:结构脚本按批提交,不额外包事务;数据脚本按业务批次包事务,每批 1000 到 5000 行。下面是一个按批提交的写法。
public static void ExecuteInBatches(string connStr, IEnumerable<string> sqlBatches, int commitEvery = 10) { using (var conn = new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran = conn.BeginTransaction(); int count = 0; try { foreach (var sql in sqlBatches) { using (var cmd = new SqlCommand(sql, conn, tran)) { cmd.CommandTimeout = 300; cmd.ExecuteNonQuery(); } count++; if (count % commitEvery == 0) { tran.Commit(); tran = conn.BeginTransaction(); } } tran.Commit(); } catch { tran.Rollback(); throw; } } }commitEvery控制每多少个批处理提交一次,结构脚本可以设大一点,数据脚本设小一点。异常时回滚当前事务,已经提交的批次不会丢,重新执行时靠第 3 章的幂等脚本跳过已存在对象。注意如果脚本里有CREATE DATABASE或ALTER DATABASE,不能放在事务里,这类语句要单独拎出来先执行。超时方面,CommandTimeout设 0 表示无限等,生产环境不建议,设 300 到 600 秒更稳,失败后看日志再决定是否重跑。
5. 常见问题与排查:脚本导入导出翻车的五个现场
这一章按“现象 -> 原因 -> 解决”记录五个我实际踩过的坑。每个坑都不是孤立的,往往和生成选项、字符集、执行方式有关。先看现象能不能对上,再改配置,别一上来就重装数据库。
5.1 执行第二遍报“对象已存在”
现象:第一次执行脚本成功,第二次执行到CREATE TABLE或CREATE PROCEDURE就报“数据库中已存在名为 ‘XXX’ 的对象”。原因:生成脚本时“脚本检查对象是否存在”没开,或者开了但只对表生效,索引、约束、触发器没带判断。解决:回到 SSMS 生成脚本的高级选项,把“脚本检查对象是否存在”设为 True,重新生成;已经生成的脚本按第 3 章的模板手工补IF NOT EXISTS。存储过程优先用CREATE OR ALTER,没有这个语法就改成“存在则ALTER,不存在则CREATE”的动态 SQL。索引判断要同时限定索引名和object_id,只判断索引名会漏掉不同表同名索引的情况。
5.2 中文变成问号或乱码
现象:脚本里的中文注释、表数据插入后变成???或乱码,英文和数字正常。原因:脚本文件保存编码和数据库连接编码不一致。SSMS 默认可能把脚本存成 ANSI 或 UTF-8 无 BOM,而 C# 用Encoding.Default读,或者 BCP 用了-c而不是-w。解决:脚本文件统一存成“UTF-8 带签名”或 UTF-16;C# 读取时显式指定Encoding.UTF8并开启 BOM 检测;BCP 导出导入加-w,不要用-c。如果目标库排序规则和源库不同,NVARCHAR字段一般没事,VARCHAR字段可能出现比较或排序差异,必要时在查询里加COLLATE。
5.3 大脚本执行到一半超时或内存爆掉
现象:SSMS 打开几百 MB 的.sql文件直接卡死;C# 用File.ReadAllText读大文件抛OutOfMemoryException;执行到一半报“超时时间已到”。原因:一次性把整个文件读进内存,或者一个SqlCommand执行了过多语句,锁等待超过默认 30 秒。解决:C# 改成File.ReadLines流式读取;按GO拆批,每批执行完释放;CommandTimeout调到 300 秒以上;特别大的数据不要走INSERT脚本,改用 BCP 或SqlBulkCopy。SSMS 里执行大脚本可以改用sqlcmd -i file.sql -b,它对大文件更友好,-b让失败时返回非零退出码,方便脚本判断。
5.4 备份还原后应用登录失败
现象:.bak还原到目标服务器后,应用连接报“登录失败,用户 ‘appuser’ 登录失败”,但数据库里能看到这个用户。原因:还原把数据库用户带过来了,但服务器登录名的 SID 和数据库用户里的 SID 对不上,形成孤立用户。解决:在目标库执行ALTER USER [appuser] WITH LOGIN = [appuser];重新绑定;如果登录名不存在,先CREATE LOGIN再绑。批量检查可以用sys.database_principals和sys.server_principals对比 SID。资源包里如果带还原脚本,注意别把源服务器的登录名硬编码进去,改成参数传入。
5.5 BCP 导入报截断或类型转换失败
现象:BCP 导入时错误文件里出现“第 1 行第 3 列(Amount)出现截断”或“将字符串转换为 datetime 时失败”。原因:目标表字段长度小于源数据、分隔符出现在数据内容里、日期格式和语言设置不匹配。解决:先看-e错误文件的行号和列号,确认是长度问题还是格式问题;长度问题改目标表字段长度或导出时用CONVERT截断;分隔符问题换一个数据里不会出现的字符,或者用-f格式文件指定列顺序和类型;日期问题在 BCP 命令里加-c时指定代码页,或者改用-w加格式文件。导入前先用SELECT TOP 10抽样检查数据,别等全量跑完才发现。
6. 上线前校验:用校验和比对源库与目标库
脚本跑完不等于迁移完成。我每次导入后先做三层校验:对象数量、行数、校验和。对象数量查sys.objects按类型分组;行数查sys.dm_db_partition_stats,避免对大表做全表COUNT(*);校验和用CHECKSUM_AGG(BINARY_CHECKSUM(*)),对整表做一次轻量指纹。三层都对上,才让业务连过去。
| 校验项 | 查询方式 | 通过标准 |
|---|---|---|
| 表数量 | SELECT COUNT(*) FROM sys.objects WHERE type='U' | 源库与目标库一致 |
| 索引数量 | SELECT COUNT(*) FROM sys.indexes WHERE object_id > 100 | 差异为 0 |
| 行数 | sys.dm_db_partition_stats按index_id IN (0,1)汇总 | 每张表行数一致 |
| 数据指纹 | CHECKSUM_AGG(BINARY_CHECKSUM(*)) | 每张表校验和一致 |
-- 行数比对:分别连源库和目标库执行,结果贴到 Excel 对比 SELECT OBJECT_SCHEMA_NAME(p.object_id) AS SchemaName, OBJECT_NAME(p.object_id) AS TableName, SUM(p.row_count) AS RowCnt FROM sys.dm_db_partition_stats p JOIN sys.objects o ON p.object_id = o.object_id WHERE o.type = 'U' AND p.index_id IN (0,1) GROUP BY p.object_id ORDER BY SchemaName, TableName; -- 校验和:按表逐条执行,大表放在业务低峰期 SELECT CHECKSUM_AGG(BINARY_CHECKSUM(*)) AS TableChecksum FROM [dbo].[Orders];BINARY_CHECKSUM(*)对列顺序和数据类型敏感,源库和目标表结构必须一致;如果有TEXT、NTEXT或XML列,校验和可能不包含这些列,需要单独抽样。校验和一致也不代表 100% 没问题,但能兜住大部分字段错位和截断。我一般把这三条查询存成一个verify.sql,每次导入后强制跑一遍,跑完再开业务连接。
说个血泪经验:有次赶时间跳过了校验,结果目标表自增列种子没同步,业务插入直接主键冲突,排查了半小时才发现是DBCC CHECKIDENT没跑。从那以后我每次导入完都先跑一遍对象数量、行数和校验和,再检查自增种子,宁可多花十分钟,也不让业务当黑匣子。希望帮到你。
本文还有配套的精品资源,点击获取