Skip to content

性能优化原则概览 ​

EF Core 性能优化的核心思想和通用原则

📖 目录 ​


性能优化金字塔 ​

        /\
       /  \      第 3 层:架构优化(影响最大)
      /____\     - 缓存策略
     /      \    - 读写分离
    /________\   - CQRS
   /          \  
  /____________\ 第 2 层:查询优化
 /              \ - 避免 N+1 查询
/________________\ - 投影查询
                  \ - 分页
                   \ - 批量操作
                    \
                     \ 第 1 层:基础优化(最容易)
                      \ - AsNoTracking
                       \ - 索引优化
                        \ - 连接池

优化优先级:

  1. 基础优化:投入少,见效快
  2. 查询优化:中等投入,显著效果
  3. 架构优化:高投入,长期收益

查询优化 ​

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大⭐
Include2中⭐⭐⭐
投影查询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.EntityFrameworkCore
csharp
// 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% 的性能提升:

  1. AsNoTracking
  2. 避免 N+1 查询
  3. 投影查询
  4. 添加索引

优先实施这些优化!


2. 测量驱动优化 ​

不要猜测 → 要测量

步骤:

  1. 基准测试(当前性能)
  2. 识别瓶颈(慢查询、高内存)
  3. 实施优化
  4. 再次测试(验证效果)
  5. 持续监控

3. 权衡取舍 ​

优化优点缺点
缓存速度快数据可能过期
无追踪性能好不能直接修改
投影传输少代码复杂
批量操作效率高灵活性低

根据业务场景选择合适的优化策略。


📚 延伸阅读 ​


💡 小结 ​

核心原则:

  • ✅ 减少数据库往返次数
  • ✅ 减少数据传输量
  • ✅ 减少内存占用
  • ✅ 利用缓存
  • ✅ 持续监控和优化

下一步:

  1. 学习 AsNoTracking 详解
  2. 掌握 避免 N+1 查询
  3. 实施 批量操作

基于 MIT 许可发布