使用ADO.NET SqlDataReader读取18万条记录时遇内存不足异常求解决
解决SqlDataReader读取大量数据内存溢出的可行方案
1. 延迟加载返回流式数据(最推荐)
不要一次性把所有对象存入List,改用IEnumerable<MyObject>配合yield return延迟生成对象。调用方遍历的时候才会逐个创建并处理对象,内存中始终只保留当前处理的实例,不会一次性占用大量内存。
代码示例:
public IEnumerable<MyObject> GetMyObjects(SqlCommand cmd) { using var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess); // 搭配SequentialAccess进一步优化内存 while(reader.Read()) { var obj = new MyObject(); obj.Property1 = Convert.ToInt32(reader["Column1"]); obj.Property2 = Convert.ToString(reader["Column2"]); // ... 其他176个属性赋值 obj.Property178 = Convert.ToString(reader["Column178"]); yield return obj; } }
调用方处理时直接遍历即可:
foreach(var item in GetMyObjects(cmd)) { // 逐个处理item,处理完成后会被GC自动回收 }
2. 数据库分页分批读取
把18万条数据拆分成多批次查询,比如每次读取1000条,处理完当前批次再加载下一批,避免一次性把所有数据加载到内存。
分页SQL示例(SQL Server):
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER(ORDER BY Column1) AS RowNum FROM YourTargetTable ) AS T WHERE T.RowNum BETWEEN @StartRow AND @EndRow
C#代码示例:
int batchSize = 1000; // 先获取总记录数,或者循环直到reader读取不到数据为止 int totalCount = GetTotalRecordCount(cmd); for(int i = 0; i < totalCount; i += batchSize) { cmd.CommandText = "上述分页SQL语句"; cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@StartRow", i + 1); cmd.Parameters.AddWithValue("@EndRow", Math.Min(i + batchSize, totalCount)); using var reader = cmd.ExecuteReader(); while(reader.Read()) { var obj = new MyObject(); // 属性赋值逻辑 ProcessSingleObject(obj); // 直接处理当前对象,无需存入大集合 } }
如果调用方必须拿到完整集合,这种方式不适用;但如果可以分批处理业务逻辑,这是控制内存的有效手段。
3. 优化MyObject的内存占用
3.1 将MyObject改为值类型(struct)
struct是值类型,复用实例时每次赋值都是拷贝操作,不会出现引用类型“所有记录值相同”的问题,同时值类型的内存开销比引用类型更小(无需对象头、GC额外开销)。
修改MyObject定义:
public struct MyObject { public int Property1 { get; set; } public string Property2 { get; set; } // ... 其他176个属性定义 }
复用实例代码:
List<MyObject> collection = new List<MyObject>(); MyObject obj = new MyObject(); using var reader = cmd.ExecuteReader(); while(reader.Read()) { obj.Property1 = Convert.ToInt32(reader["Column1"]); obj.Property2 = Convert.ToString(reader["Column2"]); // ... 其他属性赋值 collection.Add(obj); // 值类型会自动拷贝,集合中是独立的实例 }
注意:如果MyObject包含大量字符串等引用类型,优化效果会打折扣,但仍优于类实例。
3.2 缓存重复字符串
如果部分字符串属性存在大量重复值(如状态、类别字段),用字符串池缓存重复实例,减少内存占用:
private static readonly Dictionary<string, string> _stringCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); // 赋值时替换为缓存逻辑 obj.Property2 = GetCachedString(Convert.ToString(reader["Column2"])); private string GetCachedString(string value) { if(string.IsNullOrEmpty(value)) return value; if(_stringCache.TryGetValue(value, out var cachedValue)) { return cachedValue; } _stringCache[value] = value; return value; }
相同的字符串只会在内存中存储一份,能大幅降低重复字符串的内存开销。
4. 启用SqlDataReader的SequentialAccess模式
默认情况下SqlDataReader会把整行数据加载到内存,使用CommandBehavior.SequentialAccess可以让它按需读取列数据,减少内存占用,尤其适合列数较多的场景:
using var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
注意:使用该模式时必须按数据库列的顺序读取,不能跳读(比如先读Column178再读Column1),否则会触发错误。
内容的提问来源于stack exchange,提问作者Phani
相关产品推荐
相关产品推荐

