Entity Framework 6 性能优化实战
2015.11.10
C#
3 min
1.3k 字
// 目录 · contents
1. N+1 查询:最常见的性能杀手 2. 懒加载陷阱 3. AsNoTracking:只读查询的利器 4. 批量操作 5. 正确使用索引 6. 使用 SQL 查询处理复杂场景 踩坑记录 总结
EF 6
让数据访问变得异常方便,但便利的背后隐藏了不少性能陷阱。我们项目在数据量上来之后,慢查询问题一波接一波,最终痛定思痛做了一轮系统排查和优化。这篇文章记录那次优化的主要发现。
1. N+1 查询:最常见的性能杀手
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var orders = db.Orders.Where(o => o.Status == OrderStatus.Pending).ToList();foreach (var order in orders) { Console.WriteLine($"{order.Id} - {order.Customer.Name} " ); }var orders = db.Orders .Include(o => o.Customer) .Include(o => o.OrderItems.Select(i => i.Product)) .Where(o => o.Status == OrderStatus.Pending) .ToList();var result = db.Orders .Where(o => o.Status == OrderStatus.Pending) .Select(o => new { o.Id, CustomerName = o.Customer.Name, ItemCount = o.OrderItems.Count() }) .ToList();
第二种(Projection)通常比 Include 更高效,生成的 SQL
只包含需要的列,避免了传输大量无用数据。
2. 懒加载陷阱
EF 6
默认开启懒加载,导航属性在访问时才触发查询。这在循环中极其危险:
1 2 3 4 5 6 7 8 9 db.Configuration.LazyLoadingEnabled = false ;using (var db = new AppDbContext()) { db.Configuration.LazyLoadingEnabled = true ; }
懒加载在单次访问时很方便,但一旦进入循环就成了性能噩梦。我的原则是:生产代码中关闭懒加载,用显式的
Include 或投影 。
3. AsNoTracking:只读查询的利器
EF 默认会跟踪查询返回的所有实体(变更追踪),以便后续
SaveChanges()
时检测修改。对于只读查询,这个机制纯属浪费:
1 2 3 4 5 6 7 8 9 10 var users = db.Users.Where(u => u.IsActive).ToList();var users = db.Users.AsNoTracking() .Where(u => u.IsActive) .ToList(); db.Configuration.AutoDetectChangesEnabled = false ;
4. 批量操作
EF 的 SaveChanges() 对每条记录生成独立的
INSERT/UPDATE/DELETE 语句,批量操作时性能极差:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 for (int i = 0 ; i < 10000 ; i++) { db.Users.Add(new User { Name = $"User{i} " }); } db.SaveChanges(); const int batchSize = 500 ;for (int i = 0 ; i < users.Count; i++) { db.Users.Add(users[i]); if (i % batchSize == 0 ) { db.SaveChanges(); db = new AppDbContext(); } } db.BulkInsert(users); db.BulkUpdate(users); db.BulkDelete(users);
实测:插入 10000 条数据,逐条 SaveChanges 约 45 秒,BulkInsert 约 1.2
秒,差距约 37 倍。
5. 正确使用索引
EF Code First
默认只对外键和主键创建索引,业务查询字段需要手动添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class User { public int Id { get ; set ; } [Index("IX_User_Email" , IsUnique = true) ] [StringLength(100) ] public string Email { get ; set ; } [Index("IX_User_Status_CreatedAt" , Order = 1) ] public UserStatus Status { get ; set ; } [Index("IX_User_Status_CreatedAt" , Order = 2) ] public DateTime CreatedAt { get ; set ; } }
或者通过 Fluent API:
1 2 3 modelBuilder.Entity<User>() .HasIndex(u => u.Email).IsUnique() .HasIndex(u => new { u.Status, u.CreatedAt });
6. 使用 SQL 查询处理复杂场景
EF 生成的 SQL 并不总是最优的,复杂统计查询直接写 SQL 更可控:
1 2 3 4 5 6 7 8 9 10 11 12 13 var users = db.Database.SqlQuery<User>( "SELECT * FROM Users WHERE Status = @status AND Age > @age" , new SqlParameter("status" , 1 ), new SqlParameter("age" , 18 ) ).ToList();var result = db.Database.SqlQuery<OrderSummary>( "EXEC GetOrderSummary @startDate, @endDate" , new SqlParameter("startDate" , startDate), new SqlParameter("endDate" , endDate) ).ToList();
踩坑记录
有一次上线后收到报警,一个报表接口响应时间从 200ms 飙到 12
秒。排查发现,开发同学在报表逻辑里不小心用了导航属性,循环里触发了 800
次额外查询。更糟糕的是,这个问题在开发环境根本发现不了——测试数据只有 20
条,生产数据是 2000 条。
从那之后我们建立了一个规范:所有涉及集合查询的代码,必须开启 EF 的
SQL 日志,在本地验证生成的 SQL 没有 N+1 问题再提交。
1 2 db.Database.Log = sql => Debug.WriteLine(sql);
总结
EF 6 性能优化的几个核心原则:
显式 Include 代替懒加载,避免 N+1
投影(Select) 只取需要的字段,减少数据传输
AsNoTracking 用于所有只读查询
分批提交 或第三方批量库处理大量写入
SQL 日志 是发现性能问题的第一步
复杂查询不要强迫 EF 生成,直接写 SQL 更可控