SQL Server脚本导出导入与C#批处理迁移实战指南
发布时间:2026/10/25 17:07:00 锦皓数字建站

简介面向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 Liststring(); var current new Liststring(); 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 EXISTSTrue建表、建索引、建存储过程都带判断我一般分两次生成第一次“仅架构”文件名带_schema第二次“仅数据”文件名带_data。不要一次生成“架构和数据”一个大文件后面排错时你根本不知道卡在建表还是卡在插数据。生成完先打开文件看头部有没有USE [源库名]如果有目标库执行前要么替换成目标库名要么在连接里指定目标库并删掉这行。资源包里如果有现成脚本也按这个标准检查一遍。3.2 把脚本改成可重复执行表、索引、存储过程各一套模板SSMS 勾了“检查对象是否存在”后生成脚本通常自带判断但有些对象判断不完整尤其是索引和约束。我一般会手工补一层保证脚本跑第二遍不报错。下面三个模板是迁移里最常用的。-- 表先判断对象是否存在再建表 IF NOT EXISTS ( SELECT 1 FROM sys.objects WHERE object_id OBJECT_ID(N[dbo].[Orders]) AND type NU ) 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 NIX_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 ALTERSQL Server 2016 SP1 及以上或先 DROP 再 CREATE IF OBJECT_ID(N[dbo].[usp_GetOrder], NP) IS NULL EXEC(NCREATE PROCEDURE [dbo].[usp_GetOrder] AS BEGIN SET NOCOUNT ON; SELECT 1; END); ELSE EXEC(NALTER 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 脚本怎么拆 GOC# 里执行.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 Liststring(); 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, IEnumerablestring 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-16C# 读取时显式指定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 typeU源库与目标库一致索引数量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没跑。从那以后我每次导入完都先跑一遍对象数量、行数和校验和再检查自增种子宁可多花十分钟也不让业务当黑匣子。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。