file.readalllines 读大文件会爆内存,应改用 streamreader 按行流式读取并 yield return;datatable 不适合 etl 中间态,推荐 record 或自定义 class;sqlbulkcopy 需显式设 columnmappings 并注意类型与长度匹配。

File.ReadAllLines 读大文件会爆内存?用 StreamReader 分块读
直接 File.ReadAllLines 加载 GB 级日志或 CSV,进程瞬间吃光内存甚至 OOM。这不是文件“读不了”,是它把整文件塞进字符串数组,每行一个 string 对象,托管堆压力极大。
- 真实场景:解析 2GB 的带分隔符文本(如
access.log或sales_2024.csv),字段需清洗、过滤、映射到实体 - 正确做法:用
StreamReader按行迭代,配合yield return构建可枚举的流式数据源 - 注意
Encoding参数——中文环境不显式传Encoding.UTF8,默认可能用系统 ANSI,导致乱码或截断 - 别在循环里反复 new
StreamReader;一个实例复用,靠ReadLine()流式推进
while ((line = reader.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(line)) continue;
yield return ParseCsvLine(line); // 自定义解析逻辑
}DataTable 不适合做 ETL 中间态?改用 record 或自定义 class
DataTable 看似方便——列名动态、支持 SqlBulkCopy,但它是 .NET Framework 遗留重对象,字段访问慢、GC 压力高、序列化/跨线程不友好。ETL 管道中频繁构造、筛选、投影时,性能损耗明显。
- 适用场景仅限:必须对接旧版
SqlDataAdapter或报表控件,且数据量小( - 现代替代:用
record(C# 9+)定义不可变结构体,如record Sale(string Product, decimal Amount, DateTime At),内存紧凑、比较语义清晰 - 如果需运行时动态列(比如用户上传任意 CSV),改用
Dictionary<string object></string>+IEnumerable<idictionary object>></idictionary>,比DataTable轻量 3–5 倍 - 别给 record 属性加业务逻辑——ETL 是搬运工,不是执行器;转换逻辑放在独立函数里
Parallel.ForEach 处理转换时丢数据?加锁或改用 PLINQ 的 AsParallel().Select()
用 Parallel.ForEach 并行处理每一行并写入共享 List<t></t>,结果发现输出条数对不上,或者抛出 InvalidOperationException: Collection was modified ——这是典型的非线程安全集合误用。
- 根本原因:
List<t></t>、Dictionary<k></k>默认不支持并发写入 - 错误解法:在 Add 前加
lock—— 锁粒度太粗,吞吐反而不如单线程 - 推荐方案:用 PLINQ,把转换逻辑写成纯函数,返回新对象,再统一收集:
lines.AsParallel().Select(ParseAndValidate).Where(x => x != null).ToList() - 注意:PLINQ 默认开启
WithDegreeOfParallelism,CPU 密集型转换建议设为Environment.ProcessorCount - 1,避免调度开销反超收益
SqlBulkCopy 写入失败报 “The given value of type String from the data source cannot be converted to type nvarchar”?检查列顺序和类型映射
SqlBulkCopy 不按列名匹配,只认**列序号**。源数据是 [OrderId, CustomerName, Total],目标表定义是 [Id, Total, Name],哪怕字段名部分重合,也会因顺序错位导致类型强转失败或数据错位。
- 最稳妥做法:显式设置
ColumnMappings,哪怕字段名一致也加上:
bulk.ColumnMappings.Add("OrderId", "Id");
bulk.ColumnMappings.Add("CustomerName", "Name");
bulk.ColumnMappings.Add("Total", "Total");- 目标列类型要能容纳源值:比如源是
string含 500 字符,目标nvarchar(50)就必然失败;提前用SqlDbType.NVarChar+Size = -1(即MAX)更安全 - 别忽略
SqlBulkCopyOptions.TableLock——大批量导入时加这个选项能显著减少锁竞争,但会阻塞其他写操作,得评估业务窗口
ETL 最容易被忽略的不是语法,是数据边界:空值怎么传、时间精度是否丢失、编码 BOM 是否存在、浮点字段有没有科学计数法。这些细节不卡在代码里,卡在凌晨三点的生产告警上。










