optimizing-ef-core-queries
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOptimizing EF Core Queries
优化EF Core查询
When to Use
适用场景
- EF Core queries are slow or generating too many SQL statements
- Database CPU/IO is high due to ORM inefficiency
- N+1 query patterns are detected in logs
- Large result sets cause memory pressure
- EF Core查询运行缓慢,或生成过多SQL语句
- 因ORM效率低下导致数据库CPU/IO占用过高
- 日志中检测到N+1查询模式
- 大结果集引发内存压力
When Not to Use
不适用场景
- The user is using Dapper or raw ADO.NET (not EF Core)
- The performance issue is database-side (missing indexes, bad schema)
- The user is building a new data access layer from scratch
- 用户使用的是Dapper或原生ADO.NET(而非EF Core)
- 性能问题出在数据库侧(索引缺失、schema设计不合理)
- 用户正在从头搭建新的数据访问层
Inputs
输入参数
| Input | Required | Description |
|---|---|---|
| Slow EF Core queries | Yes | The LINQ queries or DbContext usage to optimize |
| SQL output or logs | No | EF Core generated SQL or query execution logs |
| 输入项 | 必填 | 描述 |
|---|---|---|
| 慢EF Core查询 | 是 | 待优化的LINQ查询或DbContext使用代码 |
| SQL输出或日志 | 否 | EF Core生成的SQL或查询执行日志 |
Workflow
操作流程
Step 1: Enable query logging to see the actual SQL
步骤1:启用查询日志查看实际生成的SQL
csharp
// In Program.cs or DbContext configuration:
optionsBuilder
.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging() // shows parameter values (dev only!)
.EnableDetailedErrors();Or use the log category:
Microsoft.EntityFrameworkCorejson
{
"Logging": {
"LogLevel": {
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}csharp
// In Program.cs or DbContext configuration:
optionsBuilder
.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging() // shows parameter values (dev only!)
.EnableDetailedErrors();或者使用日志分类:
Microsoft.EntityFrameworkCorejson
{
"Logging": {
"LogLevel": {
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}Step 2: Fix N+1 query patterns
步骤2:修复N+1查询模式
The #1 EF Core performance killer. Happens when loading related entities in a loop.
Before (N+1 — 1 query for orders + N queries for items):
csharp
var orders = await db.Orders.ToListAsync();
foreach (var order in orders)
{
// Each access triggers a lazy-load query!
var items = order.Items.Count;
}After (eager loading — 1 or 2 queries total):
csharp
// Option 1: Include (JOIN)
var orders = await db.Orders
.Include(o => o.Items)
.ToListAsync();
// Option 2: Split query (separate SQL, avoids cartesian explosion)
var orders = await db.Orders
.Include(o => o.Items)
.AsSplitQuery()
.ToListAsync();
// Option 3: Explicit projection (best - only fetches needed columns)
var orderSummaries = await db.Orders
.Select(o => new OrderSummary
{
OrderId = o.Id,
Total = o.Items.Sum(i => i.Price),
ItemCount = o.Items.Count
})
.ToListAsync();When to use Split vs Single query:
| Scenario | Use |
|---|---|
| 1 level of Include | Single query (default) |
| Multiple Includes (Cartesian risk) | |
| Include with large child collections | |
| Need transaction consistency | Single query |
EF Core性能的头号杀手。 在循环中加载关联实体时会触发该问题。
优化前(N+1 —— 1次查询获取订单 + N次查询获取订单项):
csharp
var orders = await db.Orders.ToListAsync();
foreach (var order in orders)
{
// Each access triggers a lazy-load query!
var items = order.Items.Count;
}优化后(预先加载 —— 总共仅1或2次查询):
csharp
// Option 1: Include (JOIN)
var orders = await db.Orders
.Include(o => o.Items)
.ToListAsync();
// Option 2: Split query (separate SQL, avoids cartesian explosion)
var orders = await db.Orders
.Include(o => o.Items)
.AsSplitQuery()
.ToListAsync();
// Option 3: Explicit projection (best - only fetches needed columns)
var orderSummaries = await db.Orders
.Select(o => new OrderSummary
{
OrderId = o.Id,
Total = o.Items.Sum(i => i.Price),
ItemCount = o.Items.Count
})
.ToListAsync();拆分查询 vs 单查询适用场景:
| 场景 | 适用方案 |
|---|---|
| 仅1层Include | 单查询(默认) |
| 多层Include(存在笛卡尔积风险) | |
| Include关联大型子集合 | |
| 需要事务一致性 | 单查询 |
Step 3: Use NoTracking for read-only queries
步骤3:只读查询使用NoTracking
Change tracking overhead is significant. Disable it when you don't need to update entities:
csharp
// Per-query
var products = await db.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
// Global default for read-heavy apps
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));Use when the query returns duplicate entities to avoid duplicated objects in memory.
AsNoTrackingWithIdentityResolution()变更跟踪的开销非常可观。 当你不需要更新实体时可以禁用该功能:
csharp
// Per-query
var products = await db.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
// Global default for read-heavy apps
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));当查询返回重复实体时,使用可避免内存中出现重复对象。
AsNoTrackingWithIdentityResolution()Step 4: Use compiled queries for hot paths
步骤4:热点路径使用编译查询
csharp
// Define once as static
private static readonly Func<AppDbContext, int, Task<Order?>> GetOrderById =
EF.CompileAsyncQuery((AppDbContext db, int id) =>
db.Orders
.Include(o => o.Items)
.FirstOrDefault(o => o.Id == id));
// Use repeatedly — skips query compilation overhead
var order = await GetOrderById(db, orderId);csharp
// Define once as static
private static readonly Func<AppDbContext, int, Task<Order?>> GetOrderById =
EF.CompileAsyncQuery((AppDbContext db, int id) =>
db.Orders
.Include(o => o.Items)
.FirstOrDefault(o => o.Id == id));
// Use repeatedly — skips query compilation overhead
var order = await GetOrderById(db, orderId);Step 5: Avoid common query traps
步骤5:规避常见查询陷阱
| Trap | Problem | Fix |
|---|---|---|
| Loads entire table into memory | Filter first: |
| Scans all rows | Use |
| Include is ignored with projection | Remove Include, use Select only |
| May not translate, falls to client eval | Use |
Calling | Causes nested queries | Use projection with |
| 陷阱 | 问题 | 修复方案 |
|---|---|---|
| 将整张表加载到内存 | 先过滤再转列表: |
用 | 扫描全表 | 改用 |
| 投影操作会忽略Include | 移除Include,仅使用Select |
Where中使用 | 可能无法转换为SQL,降级为客户端求值 | 使用 |
| 导致嵌套查询 | 全程使用 |
Step 6: Use raw SQL or FromSql for complex queries
步骤6:复杂查询使用原生SQL或FromSql
When LINQ can't express it efficiently:
csharp
var results = await db.Orders
.FromSqlInterpolated($@"
SELECT o.* FROM Orders o
INNER JOIN (
SELECT OrderId, SUM(Price) as Total
FROM OrderItems
GROUP BY OrderId
HAVING SUM(Price) > {minTotal}
) t ON o.Id = t.OrderId")
.AsNoTracking()
.ToListAsync();当LINQ无法高效表达查询逻辑时:
csharp
var results = await db.Orders
.FromSqlInterpolated($@"
SELECT o.* FROM Orders o
INNER JOIN (
SELECT OrderId, SUM(Price) as Total
FROM OrderItems
GROUP BY OrderId
HAVING SUM(Price) > {minTotal}
) t ON o.Id = t.OrderId")
.AsNoTracking()
.ToListAsync();Validation
效果验证
- SQL logging shows expected number of queries (no N+1)
- Read-only queries use
AsNoTracking() - Hot-path queries use compiled queries
- No client-side evaluation warnings in logs
- Include/split strategy matches data shape
- SQL日志显示查询数量符合预期(无N+1问题)
- 只读查询使用了
AsNoTracking() - 热点路径查询使用了编译查询
- 日志中无客户端求值警告
- Include/拆分查询策略匹配数据结构
Common Pitfalls
常见误区
| Pitfall | Solution |
|---|---|
| Lazy loading silently creating N+1 | Remove |
| Global query filters forgotten in perf analysis | Check |
| DbContext should be scoped (per-request); don't cache it |
| Batch updates fetching then saving | EF Core 7+: use |
String interpolation in | SQL injection risk — use |
| 误区 | 解决方案 |
|---|---|
| 懒加载静默触发N+1 | 移除 |
| 性能分析时忽略全局查询过滤器 | 检查模型配置中的 |
| DbContext应采用作用域生命周期(每次请求一个实例),不要缓存它 |
| 批量更新先查再存 | EF Core 7+:使用 |
| 存在SQL注入风险 —— 使用 |