一、为什么需要关注EF Core的查询优化

在开发中,数据库查询往往是性能瓶颈的重灾区。假设你正在开发一个电商平台,当用户查看商品详情时,如果每次都要加载无关的供应商信息、库存记录,不仅浪费资源,还会让页面响应慢得像蜗牛。EF Core提供了三种武器来应对这种场景:延迟加载(Lazy Loading)、急切加载(Eager Loading)和投影查询(Projection Query)。选对方法,能让你的应用从"卡顿老爷车"变成"流畅超跑"。

二、延迟加载:按需取数的"懒人模式"

延迟加载就像点外卖时的"加购"——只有点击购买时才去拿商品。EF Core通过动态代理实现这一特性,使用时需要满足两个条件:

  1. 安装Microsoft.EntityFrameworkCore.Proxies
  2. 启用UseLazyLoadingProxies()配置
// 示例1:延迟加载的基本用法
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual Supplier Supplier { get; set; } // 必须标记为virtual
}

// 配置DbContext
optionsBuilder.UseLazyLoadingProxies()
              .UseSqlServer(connectionString);

// 实际查询
using (var context = new AppDbContext())
{
    var product = context.Products.First(); 
    // 访问导航属性时才触发查询
    Console.WriteLine(product.Supplier.Name); // 此处生成第二条SQL
}

优点

  • 自动按需加载,代码简洁
  • 避免一次性加载过多无用数据

缺点

  • 容易产生N+1查询问题(主查询+循环内子查询)
  • 需要处理代理对象可能为空的情况

三、急切加载:一次性打包的"购物车模式"

急切加载就像超市大采购——把关联数据一次性全部装入购物车。通过IncludeThenInclude方法实现:

// 示例2:多级关联的急切加载
var orders = context.Orders
    .Include(o => o.Customer)
        .ThenInclude(c => c.Address)
    .Include(o => o.Items)
        .ThenInclude(i => i.Product)
    .ToList();

// 生成的SQL会包含JOIN语句
/*
SELECT [o].[Id], [c].[Name], [a].[City], [i].[Price]
FROM [Orders] AS [o]
LEFT JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
LEFT JOIN [Addresses] AS [a] ON [c].[AddressId] = [a].[Id]
LEFT JOIN [OrderItems] AS [i] ON [o].[Id] = [i].[OrderId]
LEFT JOIN [Products] AS [p] ON [i].[ProductId] = [p].[Id]
*/

适用场景

  • 明确知道需要哪些关联数据
  • 关联层级固定且不深

注意事项

  • 可能导致查询结果过大(Cartesian Explosion问题)
  • 深度关联时SQL会变得复杂

四、投影查询:精打细算的"定制套餐"

投影查询就像餐厅的点餐——只选择需要的菜品。通过Select直接映射到DTO:

// 示例3:使用匿名类型减少数据传输
var productInfos = context.Products
    .Where(p => p.Price > 100)
    .Select(p => new 
    {
        p.Id,
        p.Name,
        SupplierName = p.Supplier.Name,
        DiscountPrice = p.Price * 0.9
    })
    .ToList();

// 示例4:映射到强类型DTO
public class ProductDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string CategoryName { get; set; }
}

var dtos = context.Products
    .Join(context.Categories,
          p => p.CategoryId,
          c => c.Id,
          (p, c) => new ProductDto 
          {
              Id = p.Id,
              Name = p.Name,
              CategoryName = c.Name
          })
    .ToList();

性能优势

  • 仅查询需要的列,减少数据传输量
  • 避免加载整个实体对象
  • 可配合AutoMapper简化代码

五、实战中的选择策略

  1. 延迟加载适合:

    • 不确定是否需要关联数据的场景
    • 原型开发阶段快速迭代
  2. 急切加载适合:

    • 确定需要完整对象图的场景
    • 需要减少数据库往返次数时
  3. 投影查询适合:

    • Web API返回精简数据
    • 报表类数据导出场景

性能对比测试建议

// 使用Stopwatch进行简单基准测试
var watch = Stopwatch.StartNew();
// 执行查询操作
watch.Stop();
Console.WriteLine($"耗时:{watch.ElapsedMilliseconds}ms");

六、高级技巧与陷阱规避

  1. 批量查询优化

    // 使用Explicit Loading批量加载
    var order = context.Orders.First();
    context.Entry(order)
           .Collection(o => o.Items)
           .Query()
           .Where(i => i.Quantity > 5)
           .Load();
    
  2. N+1问题解决方案

    • 使用Include改为急切加载
    • 通过Batch扩展方法合并查询
  3. 追踪与非追踪查询

    // 只读场景使用AsNoTracking
    var products = context.Products
        .AsNoTracking()
        .ToList();
    

七、总结与决策流程图

最终选择哪种方式,可以参考以下决策路径:

  1. 是否需要所有字段? → 否 → 选择投影查询
  2. 是否确定需要关联数据? → 是 → 选择急切加载
  3. 是否数据使用时机不确定? → 是 → 选择延迟加载

记住:没有银弹,只有最适合当前场景的方案。建议在关键路径上通过性能测试验证选择,并善用EF Core的查询日志功能(LogTo(Console.WriteLine))来观察实际生成的SQL。