Appearance
性能优化原则概览
EF Core 性能优化的核心思想和通用原则
📖 目录
性能优化金字塔
/\
/ \ 第 3 层:架构优化(影响最大)
/____\ - 缓存策略
/ \ - 读写分离
/________\ - CQRS
/ \
/____________\ 第 2 层:查询优化
/ \ - 避免 N+1 查询
/________________\ - 投影查询
\ - 分页
\ - 批量操作
\
\ 第 1 层:基础优化(最容易)
\ - AsNoTracking
\ - 索引优化
\ - 连接池优化优先级:
- 基础优化:投入少,见效快
- 查询优化:中等投入,显著效果
- 架构优化:高投入,长期收益
查询优化
1. 避免 N+1 查询问题
问题示例
csharp
// ❌ 错误:N+1 查询
var orders = await context.Orders.ToListAsync(); // 1 次查询
foreach (var order in orders)
{
var customer = await context.Customers.FindAsync(order.CustomerId); // N 次查询
Console.WriteLine($"{order.Id} - {customer.Name}");
}
// 总查询次数:1 + N = 101 次(如果有 100 个订单)解决方案 1:Include 预加载
csharp
// ✅ 正确:使用 Include
var orders = await context.Orders
.Include(o => o.Customer)
.ToListAsync();
foreach (var order in orders)
{
Console.WriteLine($"{order.Id} - {order.Customer.Name}");
}
// 总查询次数:2 次(Orders + Customers)解决方案 2:投影查询(推荐)
csharp
// ✅✅ 最佳:只查询需要的字段
var orders = await context.Orders
.Select(o => new
{
o.Id,
o.OrderDate,
CustomerName = o.Customer.Name
})
.ToListAsync();
// 生成的 SQL:
// SELECT o.Id, o.OrderDate, c.Name
// FROM Orders o
// JOIN Customers c ON o.CustomerId = c.Id
// 总查询次数:1 次
// 数据传输量:最小性能对比:
| 方案 | 查询次数 | 数据传输 | 性能 |
|---|---|---|---|
| N+1 查询 | 101 | 大 | ⭐ |
| Include | 2 | 中 | ⭐⭐⭐ |
| 投影查询 | 1 | 小 | ⭐⭐⭐⭐⭐ |
2. 使用 AsNoTracking
csharp
// ❌ 跟踪查询( unnecessary overhead)
var products = await context.Products
.Where(p => p.IsActive)
.ToListAsync();
// ✅ 无追踪查询(性能提升 20-50%)
var products = await context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();适用场景:
- ✅ API 返回数据
- ✅ 报表查询
- ✅ 下拉列表选项
- ❌ 需要修改的实体
3. 投影查询减少数据传输
csharp
// ❌ 查询所有字段
var products = await context.Products.ToListAsync();
// SELECT * FROM Products
// ✅ 只查询需要的字段
var products = await context.Products
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
// SELECT Id, Name, Price FROM Products性能提升:
- 网络传输减少 60-80%
- 内存占用减少 50-70%
- 序列化速度提升 30-50%
4. 分页查询
csharp
public async Task<PagedResult<Product>> GetProductsAsync(
int pageNumber,
int pageSize)
{
var query = context.Products.AsNoTracking();
// 先获取总数
var totalCount = await query.CountAsync();
// 再获取分页数据
var items = await query
.OrderByDescending(p => p.CreatedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<Product>
{
Items = items,
TotalCount = totalCount,
PageNumber = pageNumber,
PageSize = pageSize
};
}
// 使用
var result = await GetProductsAsync(1, 20);注意事项:
- ⚠️ 深度分页性能差(Skip 10000 很慢)
- ✅ 使用游标分页解决深度分页
5. 批量操作(EF Core 8+)
csharp
// ❌ 传统方式:N+1 次数据库往返
foreach (var product in products)
{
product.Price *= 1.1m;
}
await context.SaveChangesAsync();
// ✅ 批量更新:1 次数据库往返
await context.Products
.Where(p => p.CategoryId == 1)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Price, p => p.Price * 1.1m));
// 生成的 SQL:
// UPDATE Products SET Price = Price * 1.1 WHERE CategoryId = 1性能对比:
| 操作 | 传统方式 | 批量操作 | 提升 |
|---|---|---|---|
| 更新 1000 条 | 1001 次往返 | 1 次往返 | 1000x |
| 删除 1000 条 | 1001 次往返 | 1 次往返 | 1000x |
| 插入 1000 条 | 1001 次往返 | 1 次往返 | 100x |
6. 预编译查询(EF Core 8+)
csharp
// 定义预编译查询
private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById =
EF.CompileAsyncQuery(
(AppDbContext context, int id) =>
context.Products.FirstOrDefault(p => p.Id == id));
// 使用(跳过 LINQ 翻译,直接执行)
var product = await GetProductById(context, productId);性能提升:10-30%(高频查询场景)
适用场景:
- ✅ 频繁执行的相同查询
- ✅ 复杂查询(翻译开销大)
- ❌ 偶尔执行的查询
7. 查询拆分(避免 JOIN 爆炸)
csharp
// ❌ 多个 Include 导致 JOIN 爆炸
var order = await context.Orders
.Include(o => o.Customer)
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.Include(o => o.Payments)
.FirstOrDefaultAsync(o => o.Id == orderId);
// 生成复杂的 JOIN,性能差
// ✅ 拆分为多个简单查询
var order = await context.Orders
.AsSplitQuery() // 自动拆分
.Include(o => o.Customer)
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.Include(o => o.Payments)
.FirstOrDefaultAsync(o => o.Id == orderId);
// 生成 3-4 个简单查询,性能更好生成的 SQL:
sql
-- 查询 1: Orders
SELECT * FROM Orders WHERE Id = @id
-- 查询 2: Customers
SELECT * FROM Customers WHERE Id IN (...)
-- 查询 3: OrderItems + Products
SELECT * FROM OrderItems
JOIN Products ON ...
WHERE OrderId IN (...)
-- 查询 4: Payments
SELECT * FROM Payments WHERE OrderId IN (...)数据库设计优化
1. 索引优化
csharp
modelBuilder.Entity<Product>(entity =>
{
// 单列索引
entity.HasIndex(e => e.Name);
// 唯一索引
entity.HasIndex(e => e.Sku).IsUnique();
// 复合索引
entity.HasIndex(e => new { e.CategoryId, e.Price });
// 包含列的索引(EF Core 8+)
entity.HasIndex(e => e.CategoryId)
.IncludeProperties(e => e.Name, e.Price);
// 降序索引
entity.HasIndex(e => e.CreatedAt).IsDescending();
});索引选择原则:
- ✅ WHERE 条件字段
- ✅ JOIN 字段(外键)
- ✅ ORDER BY 字段
- ❌ 低基数字段(如性别)
- ❌ 频繁更新的字段
2. 避免 SELECT *
csharp
// ❌ 查询所有列
var products = await context.Products.ToListAsync();
// ✅ 只查询需要的列
var products = await context.Products
.Select(p => new { p.Id, p.Name, p.Price })
.ToListAsync();3. 合理使用数据类型
csharp
public class Product
{
// ❌ 浪费空间
[MaxLength(4000)]
public string ShortCode { get; set; } // 实际只需要 10 字符
// ✅ 合适的大小
[MaxLength(10)]
public string ShortCode { get; set; }
// ❌ 精度过高
public decimal Price { get; set; } // 默认 decimal(18,2)
// ✅ 根据业务需求
[Column(TypeName = "decimal(10,2)")]
public decimal Price { get; set; }
}应用层优化
1. DbContext 生命周期管理
csharp
// ✅ Web 应用:Scoped(默认)
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
// ✅ Blazor/后台服务:Factory 模式
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlServer(connectionString));
// ✅ 高并发:池化
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(connectionString),
poolSize: 128);2. 禁用不必要的变更跟踪
csharp
// 全局设置
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});
// 单个查询
context.ChangeTracker.AutoDetectChangesEnabled = false;
try
{
// 批量操作
await context.Products.AddRangeAsync(products);
await context.SaveChangesAsync();
}
finally
{
context.ChangeTracker.AutoDetectChangesEnabled = true;
}3. 缓存策略
csharp
public class ProductService
{
private readonly IMemoryCache _cache;
private readonly AppDbContext _context;
public ProductService(IMemoryCache cache, AppDbContext context)
{
_cache = cache;
_context = context;
}
public async Task<List<Product>> GetActiveProductsAsync()
{
const string cacheKey = "active_products";
// 尝试从缓存获取
if (_cache.TryGetValue(cacheKey, out List<Product> products))
{
return products;
}
// 查询数据库
products = await _context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
// 存入缓存(5 分钟)
_cache.Set(cacheKey, products, TimeSpan.FromMinutes(5));
return products;
}
}4. 异步编程
csharp
// ❌ 同步阻塞
var products = context.Products.ToList();
// ✅ 异步非阻塞
var products = await context.Products.ToListAsync();优势:
- ✅ 提高吞吐量
- ✅ 避免线程阻塞
- ✅ 更好的可扩展性
监控与诊断
1. 启用日志记录
csharp
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
if (environment.IsDevelopment())
{
options.LogTo(
Console.WriteLine,
new[] { DbLoggerCategory.Database.Command.Name },
LogLevel.Information);
options.EnableSensitiveDataLogging();
}
});2. 慢查询检测
csharp
public class SlowQueryInterceptor : DbCommandInterceptor
{
private readonly ILogger<SlowQueryInterceptor> _logger;
private readonly TimeSpan _threshold = TimeSpan.FromSeconds(1);
public SlowQueryInterceptor(ILogger<SlowQueryInterceptor> logger)
{
_logger = logger;
}
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
var stopwatch = Stopwatch.StartNew();
var reader = base.ReaderExecuting(command, eventData, result);
stopwatch.Stop();
if (stopwatch.Elapsed > _threshold)
{
_logger.LogWarning(
"Slow query detected ({Elapsed}ms): {Sql}",
stopwatch.ElapsedMilliseconds,
command.CommandText);
}
return reader;
}
}
// 注册
options.AddInterceptors(new SlowQueryInterceptor(logger));3. MiniProfiler 集成
bash
dotnet add package MiniProfiler.EntityFrameworkCorecsharp
// Program.cs
builder.Services.AddMiniProfiler(options =>
{
options.RouteBasePath = "/profiler";
});
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
options.UseMiniProfilerProfiledConnection();
});访问:https://localhost:5001/profiler
性能测试方法
1. 基准测试
csharp
public class EfCoreBenchmark
{
private readonly AppDbContext _context;
public EfCoreBenchmark(AppDbContext context)
{
_context = context;
}
[Benchmark]
public async Task<List<Product>> QueryWithTracking()
{
return await _context.Products.ToListAsync();
}
[Benchmark]
public async Task<List<Product>> QueryWithoutTracking()
{
return await _context.Products.AsNoTracking().ToListAsync();
}
}2. 性能对比测试
csharp
// 测试不同方案的性能
var stopwatch = Stopwatch.StartNew();
// 方案 1
var result1 = await Method1();
Console.WriteLine($"Method 1: {stopwatch.ElapsedMilliseconds}ms");
stopwatch.Restart();
// 方案 2
var result2 = await Method2();
Console.WriteLine($"Method 2: {stopwatch.ElapsedMilliseconds}ms");📊 性能优化检查清单
查询优化
- [ ] 避免 N+1 查询
- [ ] 使用 AsNoTracking(只读场景)
- [ ] 投影查询减少数据传输
- [ ] 分页查询
- [ ] 使用批量操作(EF Core 8+)
- [ ] 预编译高频查询
- [ ] 查询拆分避免 JOIN 爆炸
数据库优化
- [ ] 添加合适的索引
- [ ] 避免 SELECT *
- [ ] 使用合适的数据类型
- [ ] 定期维护索引和统计信息
应用层优化
- [ ] 正确的 DbContext 生命周期
- [ ] 禁用不必要的变更跟踪
- [ ] 实现缓存策略
- [ ] 使用异步编程
监控诊断
- [ ] 启用日志记录
- [ ] 监控慢查询
- [ ] 集成 MiniProfiler
- [ ] 定期进行性能测试
💡 最佳实践总结
1. 80/20 原则
20% 的优化带来 80% 的性能提升:
- AsNoTracking
- 避免 N+1 查询
- 投影查询
- 添加索引
优先实施这些优化!
2. 测量驱动优化
不要猜测 → 要测量步骤:
- 基准测试(当前性能)
- 识别瓶颈(慢查询、高内存)
- 实施优化
- 再次测试(验证效果)
- 持续监控
3. 权衡取舍
| 优化 | 优点 | 缺点 |
|---|---|---|
| 缓存 | 速度快 | 数据可能过期 |
| 无追踪 | 性能好 | 不能直接修改 |
| 投影 | 传输少 | 代码复杂 |
| 批量操作 | 效率高 | 灵活性低 |
根据业务场景选择合适的优化策略。
📚 延伸阅读
💡 小结
核心原则:
- ✅ 减少数据库往返次数
- ✅ 减少数据传输量
- ✅ 减少内存占用
- ✅ 利用缓存
- ✅ 持续监控和优化
下一步:
- 学习 AsNoTracking 详解
- 掌握 避免 N+1 查询
- 实施 批量操作