上周客户反馈系统卡死,CPU 直接飙到 100%。
排查了半天,发现是一行看起来人畜无害的 LINQ 代码惹的祸。
这行代码在测试环境跑得好好的,一上生产就炸了。原因很简单:测试库 100 条数据,生产库 100 万条。
问题代码
先看这行"杀手代码":
// 查询订单总金额大于1000的用户 var users = db.Users .Where(u => db.Orders.Where(o => o.UserId == u.Id).Sum(o => o.Amount) > 1000) .ToList();
看起来没毛病对吧?逻辑很清晰:找出订单总金额超过 1000 的用户。
但这行代码会让数据库对每个用户都执行一次子查询。
用户表 1 万条,订单表 10 万条,实际执行的查询次数:1 万次!
这就是经典的 N+1 查询问题,LINQ 写法不当最容易踩的坑。
正确写法
用 GroupBy + Join 改成一次查询:
// 先聚合订单数据 var userAmounts = db.Orders .GroupBy(o => o.UserId) .Select(g => new { UserId = g.Key, Total = g.Sum(o => o.Amount) }) .Where(x => x.Total > 1000) .ToList(); // 再查用户信息 var userIds = userAmounts.Select(x => x.UserId).ToList(); var users = db.Users.Where(u => userIds.Contains(u.Id)).ToList();或者用更简洁的 Join 写法:
var users = (from u in db.Users join o in db.Orders on u.Id equals o.UserId into orders let total = orders.Sum(o => o.Amount) where total > 1000 select u).ToList();
性能对比
同样的数据量(用户 1 万,订单 10 万),两种写法的差距:
性能提升 150 倍!
为什么会这样
LINQ to SQL/EF 的执行机制:
- 延迟执行:LINQ 表达式不会立即执行,调用
ToList() 时才真正查数据库 - 表达式树转换:EF 会把 LINQ 转成 SQL,但嵌套查询会被翻译成子查询
- N+1 问题
用 SQL Profiler 抓一下就能看到,错误写法生成的 SQL 类似这样:
-- 对每个用户都执行一次! SELECT SUM(Amount) FROM Orders WHERE UserId = 1 SELECT SUM(Amount) FROM Orders WHERE UserId = 2 SELECT SUM(Amount) FROM Orders WHERE UserId = 3 ... 重复 1 万次
如何避免
记住这几个原则:
1. 避免在 Where 里嵌套查询
// 错误:Where 里套 Where .Where(u => db.Orders.Where(...)) // 正确:先 GroupBy 聚合,再 Join
2. 用 Include 预加载关联数据
// 一次性加载用户和订单 var users = db.Users.Include(u => u.Orders).ToList();
3. 检查生成的 SQL
// EF Core 开启日志 optionsBuilder.LogTo(Console.WriteLine); // 或者用 ToQueryString() var sql = query.ToQueryString();
4. 大数据量用原生 SQL
复杂统计查询,直接写 SQL 可能更清晰高效。
总结
LINQ 很优雅,但写不好就是性能杀手。
记住一句话:能用一条 SQL 解决的,别让它变成 N 条。
下次写 LINQ 前,先想想它会生成什么样的 SQL。
觉得有用的话,点个赞收藏一下~
#程序员 #开发 #代码 #CSharp #LINQ #性能优化