ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SQL Server数据导入导出实战:从SSMS向导到BCP与BULK INSERT

SQL Server数据导入导出实战:从SSMS向导到BCP与BULK INSERT 前两天帮朋友折腾一个系统迁移的活儿把 SQL Server 从一台老旧的物理机搬到新的虚拟化环境数据量不大不小两百多张表、几十 GB 的样子。本来以为就是个“右键下一步”的活结果光类型映射和编码问题就来回返工了两遍。整理这篇实验记录的时候我特意把图形向导、命令行工具、大数据量场景这三条路线全部重新过了一遍踩过的坑、试过的参数、验证过的结论都写在里面。如果你正准备做数据库迁移、报表库初始化、或者日常的数据分发这篇文章应该能帮你少走不少弯路。1. 先搞清楚一件事导入导出的本质是什么很多人一提到数据导入导出第一反应是“备份还原”。这俩确实都能把数据从 A 搬到 B但适用场景完全不同。备份还原是物理级别的操作备份文件里存的是页、区、文件组这些底层的物理结构还原的时候要求目标实例的版本不低于备份来源而且整个数据库会一起过去——结构、权限、作业、用户全部保留。导入导出则完全不同它是逻辑级别的操作。数据被读取出来转换成行和列再通过某种协议写到目标端。你可以只导一张表、一个视图的查询结果、或者某个 schema 下的部分对象。这意味着你有很大的自由度去筛选、转换、重组数据但代价是速度和完整度都比不上物理备份。我在这次迁移中实际用到的导入导出场景有三个把旧库的 20 张核心业务表按自定义查询抽取到新库剔除掉历史垃圾数据把一张近 2 亿行的流水表按月份拆成 24 个文件分批导入每周把生产库的订单数据同步到分析库只增量不覆盖这三种场景分别对应了三种不同的工具路线没有哪一种工具是万能的。我实验下来的结论是关键不在于选“最好的工具”而在于搞清楚你每次操作的目标是什么——是追求速度、追求灵活性、还是追求最简单的操作方式。2. 图形化路线SSMS 导入导出向导的完整操作与隐藏限制如果你只有一次性的、小数据量的导入导出需求SSMS 自带的导入导出向导是最快上手的方案。它本质上是一个轻量级的 SSIS 包外壳通过图形界面让你配置数据源、目标源和映射关系背后帮你生成一个临时的 DTSX 包。2.1 正确的操作步骤用向导做导入导出有几个关键节点容易迷糊我按实际点击顺序列一下在目标数据库上右键 → 任务 → 导入数据或导出数据。注意方向是站在目标端看往新库导数据就选导入从旧库抽数据放文件就选导出选择数据源。如果是从 SQL Server 导到 SQL Server数据源选 “SQL Server Native Client 11.0”如果是从 Excel 导选 “Microsoft Excel”如果是导成平面文件选 “Flat File Source”下一步到 “指定表复制或查询” 这一步是最关键的岔路口选 “复制一个或多个表或视图的数据” 是整表搬运选 “编写查询以指定要传输的数据” 可以写 T-SQL 做筛选在映射页面注意每列下方的数据类型下拉框是可以改的不是只能默认最后一步勾选 “保存 SSIS 包” 可以把这个流程固化成可重复执行的 DTSX 包2.2 向导的隐藏限制版本和位数问题这里必须重点提一个 64 位和 32 位的坑。SSMS 里的导入导出向导默认加载的是 64 位版本的驱动。如果你需要导 Excel 文件而机器上只装了 32 位的 Access Database Engine向导会直接报错报错信息“Microsoft.ACE.OLEDB.12.0” 未注册解决办法有两个要么装 64 位的 ACE 驱动要么用 32 位版本的 SSMS老版本去连接。我之前在一台只有 Office 32 位的机器上处理 Excel 导入卡了很久才想起是驱动位数不匹配的问题。还有一个隐藏限制是向导对超大表的支持。如果你导一张超过 10 亿行的表向导的默认批处理大小每批 10000 行会跑得非常慢。这时候你应该放弃向导改用命令行工具下面会讲。2.3 向导的适用结论经过这次实验我的看法是向导适合“一次性、小数据量、表结构简单”的场景。它最大的问题是黑盒——你无法精确控制事务粒度、无法方便地做复杂转换、出错调试也麻烦。但它有一个独一无二的优势就是“零门槛”我经常让团队里刚入职的同事用向导处理临时性数据需求不会出什么大乱子。3. 命令行三板斧BCP、BULK INSERT、OPENROWSET 的适用边界如果说向导是傻瓜相机那么 BCP、BULK INSERT 和 OPENROWSET 就是单反相机——功能强大但需要理解原理才能用好。这三者我这次实验全部实际跑了一遍它们的定位差异非常明显。3.1 BCP导出最灵活可编程性最强BCP 是 SQL Server 自带的命令行工具最大的特点是既能导出又能导入而且不依赖 SSMS 图形界面。它可以在任何能连到 SQL Server 的机器上执行非常适合做自动化的定时导出任务。我这次导出 2 亿行流水表时用的就是 BCP 命令bcp [数据库名].dbo.流水表 out D:\data\流水_202401.txt -S 服务器地址 -U 用户名 -P 密码 -c -t | -T参数说明-c使用字符类型导出兼容性最好-t |指定字段分隔符为竖线-T使用 Windows 集成认证如果用的是 SQL 认证就换成 -U 和 -P导出的文件是大文本文件每个字段用|分隔。这里有一个小技巧生产环境字段值里如果本身包含|字符就不能用竖线做分隔符需要换成其他不太可能出现的组合比如|||或者\t制表符。BCP 还可以通过-q参数支持中文表名-b参数指定批大小。我导出时试了几个批大小参数-b 50000在机械硬盘上速度最优而在 SSD 上区别不大。3.2 BULK INSERT导入速度之王但格式要求严格BULK INSERT 是 T-SQL 语句它的导入速度非常快因为它直接调用批量复制接口绕过了很多不必要的日志记录和检查逻辑。相对地它对文件格式要求也相当严格——字段分隔符、行分隔符必须完全匹配而且没有图形界面给你试错。我导入拆分后的流水文件时用的语句是BULK INSERT [目标库].dbo.流水表 FROM D:\data\流水_202401.txt WITH ( FIELDTERMINATOR |, ROWTERMINATOR \n, BATCHSIZE 50000, TABLOCK, KEEPNULLS );重点解释几个 WITH 选项TABLOCK指定导入期间对目标表持有表锁。这会最大化导入速度但代价是并发访问被阻塞。如果不是在生产高峰期操作强烈建议加上KEEPNULLS默认情况下BULK INSERT 会把空字符串转为 NULL。如果表里有空字符串需要保留必须加这个选项BATCHSIZE批大小决定事务粒度。每批次完成后日志会 checkpoint 一次如果中途失败已完成批次的数据会保留3.3 OPENROWSET临时性导入的一把好手OPENROWSET 适合从外部数据文件临时查询数据不需要建永久链接服务器。它的使用场景是“一次性把文件里的数据查出来和本地表做 JOIN 或者直接插入”。SELECT * INTO 临时表 FROM OPENROWSET( BULK D:\data\流水_202401.txt, FORMATFILE D:\data\流水_format.xml ) AS t;这个用法要求你有一个格式文件Format File它描述了目标文件的列结构。格式文件可以用 BCP 的format参数生成也可以手写。我这次的实验中手写格式文件花了不少时间而且踩过字段顺序不匹配的坑。如果只是简单导入没有额外处理逻辑建议优先 BULK INSERT。3.4 三条路线的实测对比这一轮实验我记录了一个对比表用的都是同一张 500 万行、约 800 MB 的表目标库和源库都在同一台 SSD 机器上工具导出耗时导入耗时灵活性适合场景SSMS 向导4 分 12 秒5 分 03 秒低小数据量、临时操作BCP2 分 38 秒3 分 41 秒高自动化、大批量导出BULK INSERT不适用不能导出1 分 56 秒中大批量导入OPENROWSET不适用2 分 30 秒中格式文件 临时查询这个结果符合预期BULK INSERT 的导入速度几乎是向导的两倍多。但注意BCP 和 BULK INSERT 的速度远没有达到极限——实际瓶颈通常集中在目标表的索引数量和文件存储的 IO 性能上。如果目标表有 4 个以上索引导入速度会断崖式下跌这一点在后面的优化部分会展开。4. 数据完整性陷阱类型映射、编码与主键冲突的排查做导入导出实验最容易翻车的还不是速度而是数据“看起来导入成功、实际内容错得离谱”。这一节我按踩坑顺序复盘了三类最常见的数据完整性问题。4.1 编码问题中文乱码的根源但凡涉及文本文件的导入导出编码问题是绕不开的大坑。BCP 默认使用代码页Code Page来解析字符。如果你的数据库是默认的 SQL_Latin1_General_CP1_CI_AS 排序规则BCP 导出时默认的代码页是 1252西欧字符集。当数据里包含中文时导出的文件会被编码得“看起来正常、实际错乱”。我这次实验实测的结果是用-c参数导出的 UTF-8 文件在导入时如果目标端没有显式指定代码页中文会直接变成乱码。解决方法是 BCP 命令里加-C 65001指定 UTF-8 代码页bcp [数据库名].dbo.流水表 out D:\data\流水.csv -c -C 65001 -t | -T同样地BULK INSERT 导入时也要显式指定 CODEPAGEBULK INSERT 目标表 FROM D:\data\流水.csv WITH (CODEPAGE 65001);经验教训永远不要在命令里省略代码页参数用默认设置去赌编码不出错。宁可每次多敲几个字符也不要事后花一整天去排查乱码。4.2 类型映射隐式转换的隐性风险用 SSMS 向导做导入时界面上会显示每列的源数据类型和目标数据类型。很多人直接点下一步结果发现目标表里定义的decimal(18,2)被向导映射成了float——浮点数的舍入误差直接导致金额数据出现 0.01 的偏差。这是因为向导的类型映射表是“最安全”的匹配而不是“最精准”的匹配。它在拿不准的情况下倾向选择更通用的类型。正确的操作是在映射页面逐个检查数据类型尤其是这些常见组合源类型向导默认映射手动调整为varchar(50)nvarchar(255)varchar(50) 或 nvarchar(50)decimal(18,2)floatdecimal(18,2)datetime2datetimedatetime2char(10)nchar(10)char(10)注意尾随空格的行为变化我这次迁移里有一张订单表金额字段全被映射成了 float。核对数据时发现对账不平查了半天才发现是类型映射的锅。最后调整映射关系重新导了一遍数据就完全准确了。4.3 主键冲突和 IDENTITY 列另一种常见翻车现场是导出时候表里有 IDENTITY 自增列导入到新库时直接报主键冲突。原因在于 BCP 导出文本文件时IDENTITY 列的值也作为普通列值输出了。如果目标表是新建的空表直接 BULK INSERT 会默认“不保留标识值”也就是说不导入文件里这一列的值而是让目标表自动重新生成——这就会和文件里已有的主键值产生冲突而且看起来像是“主键重复”错误。解决这个问题的办法是给 BULK INSERT 加KEEPIDENTITY选项BULK INSERT 目标表 FROM D:\data\流水.txt WITH (KEEPIDENTITY, CODEPAGE 65001);KEEPIDENTITY会强制导入文件里的 IDENTITY 值跳过自动生成逻辑。我在实验中对两种做法对比过不加 KEEPIDENTITY 时导入 500 万行成功但主键 ID 全部重新生成为 1 到 500 万加了之后ID 保留原值和外键关系完全对应。如果你用的是 SSMS 向导可以在映射页面的“编辑映射”中勾选“启用标识插入”功能等价于 KEEPIDENTITY。5. 大数据量场景的性能优化与我的实测记录数据量一旦上亿导入导出就不只是“能不能跑通”的问题了而是“多久能跑完”。这一节我分享的实验数据全部来自那台 2 亿行流水表的迁移任务涉及调优的参数和策略都重新验证过一遍。5.1 日志和恢复模式最大的隐藏瓶颈导入大量数据时最容易被忽略的瓶颈是事务日志。默认情况下目标库如果是完整恢复模式Full Recovery每一批次导入的数据都会写满完整的日志记录日志文件会膨胀得非常快IO 也全部耗在写日志上。我的实验数据导入 1 亿行数据完整恢复模式下日志文件膨胀了约 25 GB导入耗时 1 小时 50 分钟。切到简单恢复模式后日志文件基本保持原始大小导入耗时降到 42 分钟。操作方法是ALTER DATABASE 目标库 SET RECOVERY SIMPLE;导入完成后务必改回完整恢复模式然后立刻做一次全备份否则之后的事务日志备份链会断掉。根据我个人的操作习惯上线时间允许的话我都是用“导入前切简单模式 → 导入 → 切回完整模式 → 立即全备份”这个固定流程。不会在完整恢复模式下硬扛大数据量导入。5.2 索引策略导入前删索引还是导入后建索引目标表有多个索引时导入过程中每一个索引都要同步维护。索引越多维护成本越高导入速度越慢。业界有两个倾向先删索引再导入最后重建索引保留索引直接导入我实测了一张带 3 个非聚集索引的表单批 50000 行的导入耗时对比策略导入耗时最终索引维护耗时总耗时保留索引直接导1 小时 38 分无额外耗时1 小时 38 分删索引导入再建索引28 分钟15 分钟43 分钟结论非常明确数据量越大删除索引再导入、最后统一重建的收益越高。但注意这个策略只适用于“一次性、全量”的导入场景。如果是持续增量同步删除索引会严重影响业务查询的性能得不偿失。5.3 批大小Batch Size的调优实验BULK INSERT 的 BATCHSIZE 参数直接影响事务大小和内存使用。我用了四组参数对同一份 1 亿行数据测试BATCHSIZE导入耗时日志增长备注1000056 分钟20 GBSSMS 默认行为5000042 分钟18 GB较均衡10000038 分钟16 GB对内存要求更高50000041 分钟22 GB大事务反而降低效率有趣的是批大小从 10 万调大到 50 万时耗时反而变长了。原因是大批次意味着单事务持有的锁更多内存压力更大导致部分数据页被提前写入磁盘。实测下来 50000-100000 是比较合理的区间。如果你不确定自己的服务器配置从 50000 起步观察内存压力和日志增长速度再做调整是最稳妥的方式。5.4 并行导入的收益与风险SQL Server 本身不直接支持并行 BULK INSERT 到同一个表需要手动把文件拆分后、开多个会话分别导入到同一个表里的不同分区。这次实验我用分区表把 2 亿行按日期分了 24 个区然后用 4 个并发会话分别导入对应月份的分区。结果如下并发数总耗时说明142 分钟基准226 分钟约 1.6 倍加速419 分钟约 2.2 倍加速821 分钟IO 成为瓶颈反而掉速这个数字说明并行导入的收益不是线性的到达一定并发量后磁盘 IO 和日志写入会互相争抢资源。4 个并发是物理磁盘上比较合理的上限如果是 NVMe SSD可以试着提到 8 个。并行导入的一个前提是你对目标表做了分区或者手动分配了不同数据范围给不同会话。否则多会话同时插入同一张表会触发严重的锁等待和页拆分效果和单线程差不多甚至会互相阻塞变成负优化。6. 自动化场景把导入导出做成可维护的日常脚本最后说一下实验之外的收获。如果你需要把导入导出变成一个每周或者每天自动运行的任务靠命令行工具 操作系统的计划任务是最轻量、最可控的方案。我为订单同步任务写过一个简单的批次脚本核心逻辑如下REM 导出前一天的数据 bcp orders.dbo.订单表 out D:\sync\订单_%date:~0,4%%date:~5,2%%date:~8,2%.txt -S 生产服务器 -U 同步账号 -P 密码 -c -C 65001 -t | -T REM 导入到分析库 sqlcmd -S 分析服务器 -U 同步账号 -P 密码 -d 分析库 -Q BULK INSERT 订单表 FROM D:\sync\订单_%date:~0,4%%date:~5,2%%date:~8,2%.txt WITH (CODEPAGE65001, FIELDTERMINATOR|, KEEPIDENTITY, BATCHSIZE50000)实际使用中我有几个提醒不要在脚本里写死密码。Windows 的任务计划程序可以用-T走集成身份验证把脚本挂在专门的域账号下运行更安全每次同步之前先做一次行数校验比如用 sqlcmd 查源库的最大订单号然后在导入脚本末尾做断言防止源端数据异常传送到分析库文件名的日期部分是用系统变量拼接的这个在中文环境下会有兼容性问题。建议在脚本里显式传递日期参数而不是依赖%date%变量自动化任务最怕的就是“静默失败”——任务跑了但数据没同步过来一张报表用的旧数据业务方还没发现。所以脚本里无论如何都要加上失败告警最简单的方式是 sqlcmd 执行出错时将错误输出重定向到事件日志或者发一封邮件。我在实际运行这套脚本的一年里最大的体会是数据导入导出这个活儿追求的不是“用过一次”而是“每次都能跑得让人放心”。把流程脚本化、参数化、加监控才是真正能解放生产力的做法。以上所有实验和参数都是我实际跑过之后得出的结论如果你在自己的环境里遇到差异不妨先看看硬件和 SQL Server 版本的差异再对比调整批大小和并发数基本上都能找到最优参数。
返回列表