You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);

根因分析

  1. 第一版代码重复插入+内存高的原因
    • 自定义的ReadAsLines返回的是延迟执行的迭代器,每次调用First()、Skip(1)、遍历records时,都会重新创建StreamReader从文件头开始读取,而不是在同一个流上向后偏移。第一次取表头读了第一行,后续遍历Skip(1)的结果时又从头开读,把表头当成第一条数据,每次枚举都重新读文件,最终DataTable里的行就会逐行累加重复。
    • 哪怕修复了迭代器重入的问题,当前逻辑还是把所有行全部加载到DataTable后再一次性调用WriteToServer,本质还是全量内存加载,百万行数据+DataTable的结构开销,内存和CPU压力飙升是必然结果。
  2. 第二版代码编译报错+性能问题的原因
    • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 20:51:23