高并发下频繁调用ToList()引发内存爆炸,应延迟执行;Where+FirstOrDefault易致N+1查询,需建索引并批量查;PLINQ不适用于I/O场景;深度分页需键集分页与复合索引配合。

ToList() 在并发请求中引发的内存爆炸
高并发下频繁调用 ToList() 会立即执行查询并生成新集合,如果源数据量大或调用频次高(比如每秒数百次 Web API 请求),会导致大量短生命周期对象堆积在 Gen 0,触发高频 GC,甚至引发 OutOfMemoryException。这不是 LINQ 本身慢,而是过早物化带来的内存压力。
- 只在真正需要遍历多次、或需修改集合时才调用
ToList()或ToArray() - 对数据库查询(如 EF Core 的
IQueryable),优先保持延迟执行,用AsEnumerable()后再链式处理,避免重复执行 SQL - 若必须缓存结果,考虑用
MemoryCache+ 懒加载,而不是每次请求都ToList()
Where + FirstOrDefault 组合在 IQueryable 上的 N+1 风险
EF Core 中写 context.Orders.Where(o => o.UserId == userId).FirstOrDefault() 看似合理,但若 userId 来自未索引字段(如字符串 GUID 且无数据库索引),或该查询被嵌套在循环里(如遍历用户列表查各自最新订单),就会变成 N 次独立查询 —— 并发时直接压垮数据库连接池和 CPU。
- 确认数据库表上对应字段已建索引:
CREATE INDEX IX_Orders_UserId ON Orders(UserId) - 避免在循环内做 LINQ 查询;改用
IN批量查(context.Orders.Where(o => userIds.Contains(o.UserId))),注意 EF Core 6+ 对Contains的翻译更稳定 - 用
SQL Server Profiler或 EF Core 日志(EnableSensitiveDataLogging)验证最终生成的 SQL 是否符合预期
Parallel LINQ(PLINQ)在 I/O 密集型场景反拖慢性能
AsParallel() 适合 CPU 密集型计算(如图像处理、数值聚合),但在 Web 应用中处理 HTTP 请求、DB 查询、文件读取等 I/O 操作时启用 PLINQ,反而因线程争抢、上下文切换和同步开销导致吞吐下降,响应时间波动加剧。
- 除非明确是纯内存计算且单次耗时 > 50ms,否则不要对
IEnumerable调用AsParallel() - 异步 I/O 操作(如
ToListAsync()、ReadAsStringAsync())天然支持并发,应优先用Task.WhenAll()替代 PLINQ - PLINQ 默认使用
ThreadPool线程,可能挤占 ASP.NET Core 的请求处理线程,造成请求排队
OrderBy + Skip + Take 分页在大数据集上的执行计划失效
写 query.OrderBy(x => x.CreatedAt).Skip(10000).Take(20) 看似标准分页,但当跳过行数极大(如 > 50000)时,SQL Server/PostgreSQL 可能放弃使用索引,退化为全表扫描 + 排序,单次查询耗时从毫秒级飙升至秒级,并发叠加后数据库 CPU 持续 100%。
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (ORDER BY CreatedAt) AS RowNum FROM Orders ) AS T WHERE RowNum BETWEEN 10001 AND 10020
- 避免深度分页;改用“键集分页”(Keyset Pagination):记录上一页最后一条的
CreatedAt和Id,下一页查WHERE CreatedAt > lastTime OR (CreatedAt = lastTime AND Id > lastId) - 确保
ORDER BY字段有复合索引,例如CREATE INDEX IX_Orders_CreatedAt_Id ON Orders(CreatedAt, Id) - EF Core 7+ 支持
EntityFrameworkCore.SqlServer的UseRowNumberForPaging,但仅缓解不根治,仍需索引配合
实际压测中,最常被忽略的是分页方式与索引的严格匹配 —— 即使加了索引,只要 ORDER BY 和 WHERE 条件没对齐索引顺序,执行计划就可能失效。











