
简介这是一份基于 C# 开发的 SQL Server 数据库脚本导出/导入工具功能与 SQL Server 2014 Management Studio 的“生成脚本”类似适合需要在 .NET 环境下批量备份表结构、数据或迁移数据库的开发者使用也可作为 C# WinForm 数据库编程的入门实例。压缩包共 66 个文件体积仅 2.16MB主要包含 10 个 C# 源码文件、8 个 SQL 脚本、4 个可直接运行的 EXE 及配套 DLL/PDB 调试文件另有解决方案、项目工程、界面资源配置和编译中间文件。其中源码覆盖主程序入口、WinForm 界面和数据库辅助操作类SQL 脚本可作为导出结果的参考或导入模板。已有 1488 人学习/下载。借助该工具用户可以像 SSMS 一样选择库表并生成 CREATE/INSERT 脚本也可将脚本反向导入目标数据库实现不同服务器间的库结构迁移工程目录按源码、编译输出与资源分类组织适合研究数据库脚本生成原理或作为内部数据迁移工具二次开发的基础。1. SQLSERVER脚本导出导入.zip数据搬迁问题的最短路径接手一个老库到新服务器的数据迁移数据库超过 1TBSSMS 的“生成脚本”功能在这个量级下基本是摆设导出的脚本动辄几个 GB 文本执行起来连日志文件都撑不住。这时候我把目光转向脚本方案。SQLSERVER脚本导出导入.zip 就是这类工具包的常见形态把一批 bcp 命令、T-SQL 存储过程、批处理封装在一起按“导出—传输—导入—校验”四步完成数据搬迁。它适合三类人被大表迁移折磨的 DBA、需要在无人值守环境定时备份项目数据的运维以及想摆脱图形界面、把数据流转做到可复现和可版本化的开发者。这篇笔记不聊图形界面只讲脚本方案怎么落地。2. 脚本包背后的三条路径bcp、BULK INSERT 与动态 INSERT 的选型逻辑脚本包的核心不是某一个命令而是三套互补的导出导入路径。拿到任何脚本包先看它用的是哪条路径再判断适合什么数据量。2.1 bcp千万级大表导出的默认答案bcp 是 SQL Server 自带的命令行工具不依赖 SSMS在 Windows 的 cmd、PowerShell 或者 Linux 的 mssql-tools 里都能调用。它的典型用法是bcp 查询语句 queryout 数据文件 -S 服务器 -U 账号 -P 密码导出的是二进制或文本格式的平面文件。选择 bcp 的第一个理由是性能。底层走 OLEDB 或 ODBC导出时是流式写文件不会把整张表拉进内存配合-b参数按批提交日志增长可控。第二个理由是增量能力queryout 可以接任意 WHERE 子句把某个时间段、某种状态的记录先导出来脚本包里的“增量备份”功能基本都是靠这个实现的。bcp SELECT [ID], [Name], [Amount], [CreateTime] FROM [MyDB].[dbo].[Orders] WHERE [CreateTime] 2024-01-01 queryout D:\db_backup\orders_2024.dat -S 192.168.1.10,1433 -U backup_user -P YourPassword -c -t | -r \n -b 5000 -m 100 -e D:\db_backup\orders_2024.err这段命令做了几件事-c表示用字符模式读写-t |指定字段分隔符为竖线-r \n指定行分隔符为换行-b 5000表示每 5000 行提交一次-m 100允许最多 100 条错误-e指定错误日志文件。这里最容易忽略的是-m如果不写bcp 遇到第一条错误就中断如果写太大错误记录会淹没在文件里脚本包一般建议设 50 到 100。2.2 BULK INSERT把数据文件快速装进目标库bcp 负责把数据导出去BULK INSERT 负责把文件装回来这是脚本包里最常见的配对。BULK INSERT 是一条 T-SQL 语句可以直接在 SSMS 或脚本里执行但它有一个硬约束文件路径必须是 SQL Server 服务进程能访问到的路径不是你客户端电脑的本地路径。BULK INSERT [MyDB].[dbo].[Orders] FROM ND:\db_backup\orders_2024.dat WITH ( FIELDTERMINATOR |, ROWTERMINATOR \n, FIRSTROW 1, BATCHSIZE 5000, TABLOCK, CODEPAGE 65001 );FIELDTERMINATOR和ROWTERMINATOR必须和 bcp 导出的参数一致否则导入时列对不上。FIRSTROW 1表示从文件第一行开始读如果文件有标题行要改成2。TABLOCK是性能关键它允许目标表在导入期间使用最小日志记录速度能提升一倍以上代价是导入期间这张表基本处于排他锁状态不适合在线业务。CODEPAGE 65001是把文件当作 UTF-8 读后面避坑章节会专门讲编码问题。2.3 动态 INSERT小配置表的精细化处理bcp 和 BULK INSERT 适合大表但对于几百行、几千行的配置表、字典表用这两个工具反而小题大做。脚本包里通常会另放一个生成 INSERT 语句的存储过程把每行数据转成可读的文本 SQL。这样做的价值在于可审查迁移前能直接看 SQL 内容确认数据没被转义、截断。DECLARE sql NVARCHAR(MAX) N; SELECT sql sql CASE WHEN sql N THEN N ELSE N UNION ALL SELECT END QUOTENAME(COLUMN_NAME) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME NConfigItem AND TABLE_SCHEMA Ndbo ORDER BY ORDINAL_POSITION; SET sql NSELECT * INTO #tmp FROM ( sql N) AS src;; PRINT sql;这段代码演示的是动态拼列名真正落地时还会把每一行用 FOR XML PATH 拼成INSERT INTO ... VALUES (...);。这里不想展开太多因为动态 INSERT 的坑非常明显SQL 文本长度有上限数据里带单引号、换行符都会破坏语句结构所以它只适合小表。脚本包里常见做法是把配置表单独放进一个sql\init_data.sql由维护者手工维护不参与大表流程。2.4 选型组合一次迁移任务里三种方案怎么配合一套成熟的脚本包不会只用一种方案。我一般会这样分数据量在百万级以下的维度表用动态 INSERT 生成可读脚本千万级以上的事实表、流水表用 bcp 导出加 BULK INSERT 导入特别大的表超过 200GB则拆分成按月、按天导出多个文件再并行导。三者配合还有一个好处动态 INSERT 出错时可以人工看 SQLbcp 出错时可以看错误文件排查路径清晰。方案适合规模优点缺点bcp queryout百万级以上流式导出、支持 WHERE 增量、不占内存需要命令行环境、参数多BULK INSERT百万级以上T-SQL 内执行、可配批大小和事务文件必须放在服务端可访问路径动态 INSERT万级以下可读可审查、适合配置表SQL 长度有限、大表性能差3. 从解压到跑通一套可复现的导出导入脚本包这一章把完整流程走一遍。拿到 SQLSERVER脚本导出导入.zip 后按目录结构拆开改配置跑导出再跑导入最后校验。3.1 目录结构与统一配置先定规矩再动手脚本包解压后通常长这样这也是我组织脚本的习惯。数据文件、日志、SQL 脚本、批处理各归各的目录避免一个文件夹里堆满几百个文件。SQLSERVER脚本导出导入/ export_all.bat import_all.bat config.ini check_count.sql sql/ import_tables.sql truncate_tables.sql rebuild_index.sql logs/ data/config.ini 是最先要改的东西。把服务器地址、数据库名、账号、密码、目标路径、分隔符全部抽到配置里脚本本身不做任何硬编码。[SERVER] SRC_SERVER192.168.1.10,1433 DST_SERVER192.168.1.20,1433 DB_NAMEMyDB [ACCOUNT] DB_USERbackup_user DB_PASSYourPassword [CONFIG] EXPORT_PATHD:\db_backup FIELD_SEPARATOR| ROW_SEPARATOR\n BATCH_SIZE5000注意密码写在明文 ini 里在内部工具包中没问题但如果这份脚本要交付给外部环境建议改成环境变量读取。bat 文件里用%DB_USER%、%DB_PASS%引用配置值的方式不同脚本包各有差异但思路一致默认值放进 ini命令行参数优先级最高。3.2 批处理导出脚本循环调用 bcp 并收集错误码导出的批处理核心是一个 for 循环遍历表清单逐表调用 bcp。表清单可以是一个文本文件也可以直接写在数组变量里。echo off setlocal enabledelayedexpansion set EXPORT_PATHD:\db_backup set SERVER192.168.1.10,1433 set USERbackup_user set PASSYourPassword set DBMyDB if not exist %EXPORT_PATH% mkdir %EXPORT_PATH% for %%T in (Orders, Customers, Products, ConfigItem) do ( echo [%date% %time%] exporting %%T ... bcp SELECT * FROM [%DB%].[dbo].[%%T] queryout %EXPORT_PATH%\%%T.dat -S %SERVER% -U %USER% -P %PASS% -c -t | -r \n -b 5000 -m 100 -e %EXPORT_PATH%\%%T.err if !errorlevel! neq 0 ( echo [%date% %time%] ERROR - %%T export failed with code !errorlevel! %EXPORT_PATH%\export.log ) else ( echo [%date% %time%] OK - %%T %EXPORT_PATH%\export.log ) ) endlocal这段脚本有几个关键点。setlocal enabledelayedexpansion必须打开因为errorlevel在循环里要用!errorlevel!而不是%errorlevel%读取否则每次取到的都是循环开始前的旧值。if not exist负责建目录忘了建目录会导致 bcp 报“无法打开文件”。每个表错误时只记录日志而不中断整个循环这样一张表失败不会拖垮整批任务。3.3 配合导入建表、清表、BULK INSERT 的顺序不能乱导入前先确认目标库表结构已经存在。脚本包通常会在导出阶段顺手生成一份建表脚本或者用 bcp format 命令生成格式文件作为建表参考。导入的顺序是先 TRUNCATE 旧数据再执行 BULK INSERT最后重建索引。echo off setlocal set SERVER192.168.1.20,1433 set USERbackup_user set PASSYourPassword set DBMyDB set DATA_PATHD:\db_backup sqlcmd -S %SERVER% -U %USER% -P %PASS% -d %DB% -i sql\truncate_tables.sql if errorlevel 1 goto :end sqlcmd -S %SERVER% -U %USER% -P %PASS% -d %DB% -i sql\import_tables.sql if errorlevel 1 goto :end sqlcmd -S %SERVER% -U %USER% -P %PASS% -d %DB% -i sql\rebuild_index.sql if errorlevel 1 goto :end echo [%date% %time%] import success goto :end :end endlocaltruncate_tables.sql里写的是对目标表执行TRUNCATE TABLE目的是让导入可重复执行。这里有一个顺序问题必须先清表再导入否则 BULK INSERT 会往已有数据后面追加第二次运行时数据翻倍。import_tables.sql的内容就是上一章展示的 BULK INSERT 语句每张表一段建议把TABLOCK打开等全部导入完成后再重建索引比导入前保留索引快很多。3.4 校验环节行数、聚合值与样本比对数据导完不等于迁完必须验证。最基础的是行数比对源库和目标库各跑一次 COUNT_BIG人工或脚本比对结果。更稳的校验是同时比对主键的聚合校验和与行数这样能发现同构但数据错位的极端情况。-- 源库执行 SELECT COUNT_BIG(1) AS RowCnt, SUM(CAST(CHECKSUM([ID], [Name], [Amount], [CreateTime]) AS BIGINT)) AS AggKey FROM [MyDB].[dbo].[Orders]; -- 目标库执行结果应与源库完全一致 SELECT COUNT_BIG(1) AS RowCnt, SUM(CAST(CHECKSUM([ID], [Name], [Amount], [CreateTime]) AS BIGINT)) AS AggKey FROM [MyDB].[dbo].[Orders];CHECKSUM聚合校验的核心是把每一行变成一个整数再对全表求和。只要有一行数据的某个字段不一致总和几乎必然变化。要注意CHECKSUM的结果范围是 int 有符号整数直接求和可能溢出所以要包一层CAST(... AS BIGINT)。如果两张表的这两条结果完全一致基本可以断定数据迁移成功。4. 三个必调参数组规模、编码、权限脚本能不能跑通一半看参数调得对不对。这一章讲脚本包里最值得花时间的三个参数组。4.1 批大小与错误上限把大事务拆小把失败控制在文件里bcp 的-b和 BULK INSERT 的BATCHSIZE是同一个概念每 N 行提交一个事务。批越小单次事务占用的日志越少失败后回滚的成本越低批越大导入吞吐越高但一旦中间失败回滚范围也越大。我的实践经验是1GB 以下的表用 1000 到 500010GB 以上的表用 10000 到 20000再往上收益就不明显了。-m错误上限要跟批大小联动-m 100配合-b 5000意味着最多只能容忍 100 行坏数据超过立刻终止防止错误记录把文件撑爆。如果你导出的数据来自业务库建议把-m设为 0也就是一个错误都不允许因为业务库的数据理论上不该有坏行出现任何一条都说明上游有问题。e错误文件会记录行号、列号和错误原因格式如下这是排查时最直接的入口。第 100 行列 3无法将值 abc-123 转换为 bigint 第 105 行列 7字符串或二进制数据将被截断4.2 字符模式、Unicode 与格式文件乱码和错列的根源bcp 的-c是字符模式数据按数据库排序规则转成字符串输出-w是 Unicode 模式输出 UTF-16 文件-C 65001指定使用 UTF-8 代码页。三者选错轻则中文变问号重则直接导入失败。导出参数文件编码适用场景-c随系统代码页纯 ASCII 或数字内容-wUTF-16含 nvarchar / nchar 字段-c -C 65001UTF-8跨平台交换、现代应用如果表里含nvarchar字段我一般直接用-w不要贪图-c的紧凑。字符模式下非 Unicode 类型转成 varchar中文内容取决于目标库排序规则一旦目标库的代码页和源库不一致必乱。-w虽然文件体积大一倍但能完全避开编码问题。格式文件是另一个层面当字段类型需要精确指定、列顺序需要调整时用bcp ... format nul -f schema.fmt -c先生成格式文件再做修改。4.3 权限模型与连接方式让每个账号只做一件事脚本包在正式环境跑权限不足是最常见的“第一夜翻车点”。bcp 导出需要源库的 SELECT 权限BULK INSERT 需要目标库的 INSERT 权限同时要求服务器级别的ADMINISTER BULK OPERATIONS权限sqlcmd 执行脚本需要对应库的 EXECUTE 权限。我建议拆分两个账号导出账号只给db_datareader导入账号单独建只给目标库的db_datawriter加ADMINISTER BULK OPERATIONS。不要复用 sa 账号否则脚本一旦出错影响面会扩大到整个实例。连接方式上-S参数里显式写端口号-U -P走 SQL Server 身份验证如果走 Windows 身份验证批处理里要用-E但任务计划程序运行时注意账号上下文否则会报“用户 NT AUTHORITY\ANONYMOUS 登录失败”。5. 脚本导出导入避坑指南五个现场翻车记录脚本方案最大的风险不在命令本身而在环境差异。这一章记录五个我实际踩过的坑现象、原因、解决办法按顺序列出。5.1 文件找不到路径职责与客户端/服务端位置的混淆现象是 bcp 导出正常但 BULK INSERT 报“无法大容量加载文件 D:\db_backup\orders.dat 不存在”。原因在于路径职责不同bcp 是客户端工具读取客户端文件系统的路径BULK INSERT 是服务端 T-SQL 语句读取 SQL Server 服务进程所在机器的路径。如果你在本地执行 BULK INSERT而 SQL Server 跑在另一台服务器上D 盘路径自然不存在。解决方法是把数据文件上传到 SQL Server 所在机器或者用 UNC 共享路径\\server\share\orders.dat并保证 SQL Server 服务账号有共享目录的读取权限。反过来bcp in 模式则是从客户端读取文件推送给服务端逻辑正好相反。5.2 导入错列数据里的分隔符、引号与行终止符现象是导入后某些行多了一列或者某个字段变成 NULL。最常见的原因是数据内容里本身包含竖线|导出的字段分隔符和内容撞车。比如备注字段“说明A|B|C”被拆成了三个字段后面的列全部错位。解决方法是换用数据中极少出现的控制字符比如\x01十六进制 0x01。bcp 的-t |改成-t \x01BULK INSERT 的FIELDTERMINATOR 0x01。或者干脆用格式文件为每一列显式定义类型和终止符格式文件可以完全避开“读出来再猜列”的问题。5.3 中文变问号代码页和排序规则的拉锯战现象是导出文件用记事本打开正常导入后数据库里的中文全部变成问号。原因通常是字符模式-c下源库的排序规则是 Chinese_PRC_CI_AS导出时按系统 ANSI 代码页转出目标库导入时又按自己的代码页解析中间至少做了一层有损转换。解决方法是含中文或任何非 ASCII 字符的表导出时改用-wBULK INSERT 里对应改成ROWTERMINATOR不变但不要写CODEPAGE。如果你坚持用 UTF-8导出加-C 65001导入侧写CODEPAGE 65001两边缺一不可。5.4 身份列冲突IDENTITY_INSERT 与 seed 重置现象是导入后目标表的自增列值跟源表完全对不上或者导入时报“不能为表插入显式值”。原因很直接源表有 IDENTITY 列bcp 导出时把自增值也导出来了但目标表默认不允许对 IDENTITY 列做显式插入。解决方法是导入前先SET IDENTITY_INSERT [表名] ON导入完成后执行DBCC CHECKIDENT (表名, RESEED)重新校准自增种子。脚本包的import_tables.sql里每张含 IDENTITY 列的表BULK INSERT 前后必须包这两句否则二次导入时自增列会从错误起点继续跑。5.5 超时和中断网络包大小、防火墙与重跑策略现象是导出 1 小时左右任务卡住bcp 进程还在但文件大小长时间不变没有任何报错。原因大多是网络传输问题或防火墙探测长连接后静默断连。bcp 本身不提供精确的查询超时参数但可以通过调节网络包大小降低断连概率同时让脚本具备断点重跑能力。bcp SELECT * FROM [MyDB].[dbo].[Orders] WHERE [CreateTime] 2024-01-01 queryout D:\db_backup\orders_2024.dat -S 192.168.1.10,1433 -U backup_user -P YourPassword -c -t | -r \n -b 5000 -a 4096-a 4096是网络包大小默认值偏保守调大后长传场景更稳。同时给脚本加上“目标文件已存在则跳过”或“按时间分片导出”的逻辑中断后不用从头跑。比如这个例子里的 WHERE 条件已经按日期过滤重跑时把文件改成orders_2024_0901.dat只补跑缺失的区间即可。6. 把脚本包升级成无人值守的定时任务脚本跑通只是第一步真正让这套方案有价值的是接进任务计划让它每天安静地完成导出导入。这里分享两个我常用的技巧。6.1 日期变量与日志文件批处理里直接用%DATE%取日期很玄学不同系统区域设置会给出不同格式%DATE:~0,4%这种切割方式换个环境就翻车。我在生产环境的脚本包里统一改用 PowerShell 生成日期字符串再传给批处理。$date Get-Date -Format yyyyMMdd $logDir D:\db_backup\logs if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir | Out-Null } cmd /c export_all.bat | Out-File $logDir\export_$date.log -Encoding utf8 $exitCode $LASTEXITCODE exit $exitCode这段脚本把每天的导出日志按日期归档同时把批处理的退出码原样传给任务计划方便计划程序把失败任务标注出来。日志文件只要保留最近 30 天的老文件用一个简单循环清理就行。6.2 任务计划程序的调用与退出码任务计划程序里执行 PowerShell 脚本时注意“起始于”目录要设成脚本包所在目录否则相对路径全部失效。批处理里所有路径都写成%~dp0开头是更稳的做法这是指脚本自身所在目录。set BASE_PATH%~dp0 set EXPORT_PATH%BASE_PATH%data set LOG_PATH%BASE_PATH%logs我吃过一次亏脚本在命令行手动跑正常放到任务计划里就报“系统找不到指定的路径”后来发现是任务计划的“起始于”留空了。从那以后我所有脚本包统一改成%~dp0拼绝对路径再不依赖工作目录。最后收个尾脚本导出导入这套玩法技术上并不复杂真正的价值在于把迁移过程变成可重复、可检查、可自动化的流水线。如果你最近也在为数据库搬迁头疼先拿一张小表跑通全流程再逐步放大希望帮到你。本文还有配套的精品资源点击获取