C# SSIS逐行导入大型CSV到SQL Server的问题排查修复
C# SSIS脚本导入大CSV到SQL Server问题排查与修复
问题现象
- 用C#编写SSIS脚本任务导入CSV到SQL Server,60万条级别的文件运行正常,100万条级别的文件会触发服务器卡顿、CPU占用飙升
- 初步定位为全量数据加载到内存导致的资源耗尽,先后尝试
StreamReader.ReadLine、File.ReadAllLines均未实现逐行流式加载,仍然会把全量数据读入内存后再执行插入 - 修改代码尝试逐行加载后出现数据重复异常:插入首条记录后,处理下一条时会重复插入前两条记录,后续每处理一行就会重复插入之前所有已处理过的行,未定位到逻辑错误点
- 后续尝试用
File.ReadLines重写逻辑时,在lines[0].Split(',')处触发编译错误CS0021:无法对类型为IEnumerable<string>的表达式应用[]索引器,需要确认该方案是否可行及报错修复方式
问题代码1:自定义迭代器实现
System.Collections.Generic.IEnumerable<string> ReadAsLines(string filename) { using (var reader = new StreamReader(sodFileName)) while (!reader.EndOfStream) yield return reader.ReadLine(); } { var reader = ReadAsLines(sodFileName); var data = new DataTable(); // 读取列头 var headers = reader.First().Split(','); foreach (var header in headers) data.Columns.Add(header); // 读取记录 var records = reader.Skip(1); foreach (var record in records) data.Rows.Add(record.Split(',')); // 写入SQL Server var sqlBulk = new SqlBulkCopy(Conn); sqlBulk.BulkCopyTimeout = 0; sqlBulk.DestinationTableName = "dbo.sodFileName"; sqlBulk.WriteToServer(data); }
问题代码2:File.ReadLines实现
var lines = System.IO.File.ReadLines(sodFileName); if (lines.Count() == 0) return; var columns = lines[0].Split(','); var table = new DataTable(); foreach (var c in columns) table.Columns.Add(c); for (int i = 1; i < lines.Count() - 1; i++) table.Rows.Add(lines[i].Split(',')); var sqlBulk = new SqlBulkCopy(Conn); sqlBulk.BulkCopyTimeout = 0; sqlBulk.DestinationTableName = "dbo.sodFileName"; sqlBulk.WriteToServer(table);
根因分析
- 第一版代码重复插入+内存高的原因
- 自定义的
ReadAsLines返回的是延迟执行的迭代器,每次调用First()、Skip(1)、遍历records时,都会重新创建StreamReader从文件头开始读取,而不是在同一个流上向后偏移。第一次取表头读了第一行,后续遍历Skip(1)的结果时又从头开读,把表头当成第一条数据,每次枚举都重新读文件,最终DataTable里的行就会逐行累加重复。 - 哪怕修复了迭代器重入的问题,当前逻辑还是把所有行全部加载到DataTable后再一次性调用
WriteToServer,本质还是全量内存加载,百万行数据+DataTable的结构开销,内存和CPU压力飙升是必然结果。
- 自定义的
- 第二版代码编译报错+性能问题的原因
File.ReadLines返回值是IEnumerable<string>,这个类型不支持索引器访问,不能直接用[0]按下标取元素,所以触发CS0021编译错误。- 代码里反复调用
lines.Count(),每次调用都会完整遍历一遍整个文件统计行数,性能极差,哪怕修了索引器的问题,最终还是把所有行全加载到DataTable,没有解决内存占用过高的问题。 - 补充:
File.ReadLines本身是流式读取实现,比ReadAllLines全量加载到内存的方案更优,只是之前的用法完全错误。
正确实现方案
核心思路是单流读取+分批批量写入:全程只维护一个文件读取流,每读固定条数(建议5000-20000条,根据服务器性能调整)就用SqlBulkCopy写入一次,写完清空DataTable的缓存行,内存占用永远只和单批大小有关,和文件总条数无关,百万级甚至千万级文件都不会触发内存溢出。
参考代码:
// 单批写入行数,可根据实际环境调整 const int importBatchSize = 10000; using (var fileReader = new StreamReader(sodFileName)) { // 仅读取一次表头 var headerLine = fileReader.ReadLine(); if (string.IsNullOrWhiteSpace(headerLine)) return; var dataBuffer = new DataTable(); foreach (var col in headerLine.Split(',')) { dataBuffer.Columns.Add(col); } using (var bulkCopy = new SqlBulkCopy(Conn)) { bulkCopy.BulkCopyTimeout = 0; bulkCopy.DestinationTableName = "dbo.sodFileName"; string currentLine; while ((currentLine = fileReader.ReadLine()) != null) { dataBuffer.Rows.Add(currentLine.Split(',')); // 缓冲区达到批次大小就写入数据库,写完清空缓冲区 if (dataBuffer.Rows.Count >= importBatchSize) { bulkCopy.WriteToServer(dataBuffer); dataBuffer.Clear(); } } // 写入最后不足一批的剩余数据 if (dataBuffer.Rows.Count > 0) { bulkCopy.WriteToServer(dataBuffer); dataBuffer.Clear(); } } }
注意:如果CSV字段中存在带引号包裹、内含逗号的内容,直接用Split(',')会出现字段拆分错误,需要用专门的CSV解析库处理,不要手动拆分。
内容的提问来源于stack exchange,提问作者Ken Balestrieri
相关产品推荐
相关产品推荐

