EF Core 高级查询
大约 11 分钟约 3402 字
EF Core 高级查询
简介
Entity Framework Core 是 .NET 平台主流的 ORM 框架,除了基本的 LINQ 查询外,还提供了丰富的高级查询功能。掌握 Include/ThenInclude 预加载、拆分查询、原生 SQL、编译查询和全局查询过滤器等特性,可以帮助开发者编写高效且可维护的数据访问代码。
特点
Include/ThenInclude 预加载
导航属性预加载
// 预加载(Eager Loading)避免 N+1 查询问题
// 问题场景:N+1 查询
// 查询 100 个订单,然后逐个访问订单的用户信息
// 会产生 1 + 100 = 101 条 SQL
// 解决方案:使用 Include 预加载
var orders = await context.Orders
.Include(o => o.User) // 加载用户
.Include(o => o.OrderItems) // 加载订单项
.Where(o => o.Status == "paid")
.ToListAsync();
// 多级预加载:ThenInclude
var orders = await context.Orders
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product) // 订单项 -> 产品
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.ThenInclude(p => p.Category) // 订单项 -> 产品 -> 分类
.Include(o => o.User)
.Where(o => o.CreatedAt >= startDate)
.ToListAsync();
// 生成的 SQL(使用 JOIN):
// SELECT [o].[Id], [o].[UserId], [o].[Status], ...
// [u].[Id], [u].[Username], ...
// [oi].[Id], [oi].[ProductId], [oi].[Quantity], ...
// [p].[Id], [p].[Name], [p].[Price], ...
// FROM [Orders] AS [o]
// LEFT JOIN [Users] AS [u] ON [o].[UserId] = [u].[Id]
// LEFT JOIN [OrderItems] AS [oi] ON [o].[Id] = [oi].[OrderId]
// LEFT JOIN [Products] AS [p] ON [oi].[ProductId] = [p].[Id]
// WHERE [o].[Status] = N'paid'
// 条件过滤 Include 集合
var users = await context.Users
.Include(u => u.Orders
.Where(o => o.Status == "paid")
.OrderByDescending(o => o.CreatedAt)
.Take(5))
.Where(u => u.IsActive)
.ToListAsync();
// 使用字符串路径 Include(适用于无编译时类型检查的场景)
var orders = await context.Orders
.Include("OrderItems.Product.Category")
.ToListAsync();显式加载与懒加载
// 显式加载(Explicit Loading)
var order = await context.Orders
.FirstAsync(o => o.Id == orderId);
// 在需要时显式加载导航属性
await context.Entry(order)
.Reference(o => o.User) // 加载单个导航属性
.LoadAsync();
await context.Entry(order)
.Collection(o => o.OrderItems) // 加载集合导航属性
.LoadAsync();
// 带过滤条件的显式加载
await context.Entry(order)
.Collection(o => o.OrderItems)
.Query() // 获取可查询对象
.Where(oi => oi.Quantity > 1)
.OrderBy(oi => oi.ProductId)
.LoadAsync();
// 懒加载(Lazy Loading)— 需要安装 Microsoft.EntityFrameworkCore.Proxies
// 配置懒加载代理
/*
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseLazyLoadingProxies()
.UseSqlServer(connectionString);
}
// 导航属性必须为 virtual
public class Order
{
public int Id { get; set; }
public virtual User User { get; set; } // virtual 允许代理重写
public virtual ICollection<OrderItem> OrderItems { get; set; }
}
// 访问导航属性时自动加载(注意 N+1 问题)
var order = context.Orders.First();
var userName = order.User.Username; // 触发额外 SQL 查询
*/
// 建议:优先使用 Include 预加载,避免懒加载导致的 N+1 问题SplitQueries 拆分查询
拆分查询优化
// 笛卡尔爆炸问题
// 当 Include 多个集合导航属性时,会产生笛卡尔积导致数据膨胀
// 例如:订单 + 订单项 + 支付记录 + 物流记录
// 问题:单个查询使用 JOIN 导致结果集膨胀
var orders = await context.Orders
.Include(o => o.OrderItems) // 集合 1
.Include(o => o.Payments) // 集合 2
.Include(o => o.Shipments) // 集合 3
.Where(o => o.UserId == userId)
.ToListAsync();
// 如果一个订单有 10 个订单项、5 个支付记录、3 个物流记录
// 结果集行数 = 10 * 5 * 3 = 150 行(只是 1 个订单!)
// 解决方案 1:AsSplitQuery(EF Core 5.0+)
// 将一个查询拆分为多个 SQL,每个集合单独查询
var orders = await context.Orders
.Include(o => o.OrderItems)
.Include(o => o.Payments)
.Include(o => o.Shipments)
.AsSplitQuery() // 关键:拆分为多个查询
.Where(o => o.UserId == userId)
.ToListAsync();
// 生成的 SQL 变为多条:
// 查询 1: SELECT ... FROM Orders WHERE UserId = @userId
// 查询 2: SELECT ... FROM OrderItems WHERE OrderId IN (...)
// 查询 3: SELECT ... FROM Payments WHERE OrderId IN (...)
// 查询 4: SELECT ... FROM Shipments WHERE OrderId IN (...)
// 解决方案 2:全局配置默认使用拆分查询
/*
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlServer(connectionString,
sqlOptions => sqlOptions.UseQuerySplittingBehavior(
QuerySplittingBehavior.SplitQuery));
}
*/
// 解决方案 3:手动分步查询
var orderIds = await context.Orders
.Where(o => o.UserId == userId)
.Select(o => o.Id)
.ToListAsync();
var orders = await context.Orders
.Where(o => orderIds.Contains(o.Id))
.Include(o => o.OrderItems)
.ToListAsync();
// 注意:拆分查询在同一个事务中执行,保证数据一致性
// 但会增加数据库往返次数,适合集合较大但不需要 JOIN 的场景查询模式对比
// 对比不同查询模式的适用场景
// | 模式 | 方法 | 适用场景 | 性能特点 |
// |---------------|---------------------|---------------------------|-------------------|
// | 预加载 | Include | 1-2个集合,数据量小 | 单次查询,JOIN连接 |
// | 拆分查询 | AsSplitQuery | 多个集合或大数据量 | 多次查询,无笛卡尔积 |
// | 显式加载 | Entry.Reference | 按需加载单个属性 | 按需加载,灵活 |
// | 手动分步 | 分开查询 | 复杂场景,需要精细控制 | 最灵活,需手动管理 |
// 单个查询(默认)vs 拆分查询性能对比
// 场景:100 个订单,每个订单 20 个订单项
// 单个查询:100 * 20 = 2000 行结果集,EF 需要去重合并
// 拆分查询:100 行 + 2000 行,数据更紧凑
// 选择建议:
// 1. 只 Include 1-2 个集合 -> 使用默认的单查询
// 2. Include 3 个以上集合 -> 使用 AsSplitQuery
// 3. 集合数据量特别大 -> 使用 AsSplitQuery
// 4. 需要精确控制 -> 手动分步查询原生 SQL 查询
原生 SQL 使用
// 1. FromSqlRaw — 执行原生 SQL 查询返回实体
var orders = await context.Orders
.FromSqlRaw(@"
SELECT * FROM Orders
WHERE Status = {0}
AND CreatedAt >= {1}", "paid", new DateTime(2026, 1, 1))
.Where(o => o.TotalAmount > 100) // 可以继续用 LINQ 组合
.OrderByDescending(o => o.CreatedAt)
.ToListAsync();
// 2. FromSqlInterpolated — 使用插值语法(防止 SQL 注入)
var status = "paid";
var startDate = new DateTime(2026, 1, 1);
var orders = await context.Orders
.FromSqlInterpolated($@"
SELECT * FROM Orders
WHERE Status = {status}
AND CreatedAt >= {startDate}")
.Include(o => o.User)
.ToListAsync();
// 3. ExecuteSqlRaw — 执行非查询 SQL(INSERT/UPDATE/DELETE)
var affectedRows = await context.Database
.ExecuteSqlRawAsync(@"
UPDATE Products
SET Price = Price * {0}
WHERE CategoryId = {1}", 0.9, categoryId);
// 批量删除
await context.Database.ExecuteSqlRawAsync(@"
DELETE FROM Logs
WHERE CreatedAt < {0}", DateTime.Now.AddMonths(-6));
// 4. ExecuteSqlInterpolated — 插值语法的非查询 SQL
await context.Database.ExecuteSqlInterpolatedAsync($@"
UPDATE Orders
SET Status = {'completed'}
WHERE CreatedAt < {DateTime.Now.AddDays(-30)}
AND Status = {'shipped'}");
// 5. 存储过程调用
var orders = await context.Orders
.FromSqlRaw("EXEC GetOrdersByUser @UserId = {0}, @Status = {1}",
userId, "paid")
.ToListAsync();
// 带输出参数的存储过程
var totalParam = new SqlParameter("@Total", SqlDbType.Int)
{ Direction = ParameterDirection.Output };
var orders = await context.Orders
.FromSqlRaw("EXEC GetOrdersByPage @Page = {0}, @PageSize = {1}, @Total = @Total OUTPUT",
1, 20, totalParam)
.ToListAsync();
var totalCount = (int)totalParam.Value;复杂查询场景
// 1. 使用 ADO.NET 直接执行复杂 SQL
using var connection = context.Database.GetDbConnection();
await connection.OpenAsync();
using var command = connection.CreateCommand();
command.CommandText = @"
WITH MonthlyStats AS (
SELECT
FORMAT(CreatedAt, 'yyyy-MM') AS Month,
COUNT(*) AS OrderCount,
SUM(TotalAmount) AS TotalRevenue,
AVG(TotalAmount) AS AvgOrderValue
FROM Orders
WHERE CreatedAt >= @StartDate
GROUP BY FORMAT(CreatedAt, 'yyyy-MM')
)
SELECT * FROM MonthlyStats
ORDER BY Month DESC";
command.Parameters.Add(new SqlParameter("@StartDate", DateTime.Now.AddYears(-1)));
using var reader = await command.ExecuteReaderAsync();
var stats = new List<MonthlyStat>();
while (await reader.ReadAsync())
{
stats.Add(new MonthlyStat
{
Month = reader.GetString(0),
OrderCount = reader.GetInt32(1),
TotalRevenue = reader.GetDecimal(2),
AvgOrderValue = reader.GetDecimal(3)
});
}
// 2. 使用 Dapper 配合 EF Core(复杂查询用 Dapper)
/*
var connection = context.Database.GetDbConnection();
var results = await connection.QueryAsync<OrderDetailDto>(@"
SELECT o.Id, o.OrderNo, o.TotalAmount,
u.Username, u.Email,
COUNT(oi.Id) AS ItemCount
FROM Orders o
INNER JOIN Users u ON o.UserId = u.Id
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
WHERE o.Status = @Status
GROUP BY o.Id, o.OrderNo, o.TotalAmount, u.Username, u.Email",
new { Status = "paid" });
*/编译查询
编译查询提升性能
// LINQ 查询每次执行时都需要编译表达式树,开销不小
// 编译查询只编译一次,后续直接使用编译结果
// 1. 简单编译查询
private static readonly Func<MyDbContext, int, Task<Order?>> GetOrderById =
EF.CompileAsyncQuery((MyDbContext context, int id) =>
context.Orders
.Include(o => o.User)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.FirstOrDefault(o => o.Id == id));
// 使用编译查询
var order = await GetOrderById(context, 123);
// 2. 带多个参数的编译查询
private static readonly Func<MyDbContext, string, DateTime, int, Task<List<Order>>> GetOrdersByStatus =
EF.CompileAsyncQuery((MyDbContext context, string status, DateTime since, int userId) =>
context.Orders
.Include(o => o.OrderItems)
.Where(o => o.Status == status)
.Where(o => o.CreatedAt >= since)
.Where(o => o.UserId == userId)
.OrderByDescending(o => o.CreatedAt)
.ToList());
// 使用
var orders = await GetOrdersByStatus(context, "paid", new DateTime(2026, 1, 1), 100);
// 3. 分页编译查询
private static readonly Func<MyDbContext, int, int, Task<List<Order>>> GetOrdersPaged =
EF.CompileAsyncQuery((MyDbContext context, int page, int pageSize) =>
context.Orders
.Include(o => o.User)
.OrderByDescending(o => o.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList());
// 使用
var page1 = await GetOrdersPaged(context, 1, 20);
var page2 = await GetOrdersPaged(context, 2, 20);
// 性能对比(高频调用场景)
// | 方式 | 首次调用 | 后续调用 | 适用场景 |
// |-----------|----------|-----------|-----------------|
// | 普通 LINQ | 编译+执行 | 编译+执行 | 低频查询 |
// | 编译查询 | 编译+执行 | 直接执行 | 高频、重复查询 |
// 建议:在以下场景使用编译查询
// - 热点查询(被频繁调用)
// - 复杂 LINQ 表达式(编译开销大)
// - 循环中的查询(如批量处理)全局查询过滤器
全局过滤器的应用
// 全局查询过滤器(Global Query Filter)
// 自动应用于所有 LINQ 查询,无需每次手动添加条件
// 1. 软删除过滤器
/*
public class BaseEntity
{
public int Id { get; set; }
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
}
public class Product : BaseEntity
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
// 在 DbContext 中配置全局过滤器
public class AppDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 为所有继承 BaseEntity 的实体配置软删除过滤器
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(BaseEntity).IsAssignableFrom(entityType.ClrType))
{
modelBuilder.Entity(entityType.ClrType)
.HasQueryFilter(
GenerateSoftDeleteFilter(entityType.ClrType));
}
}
}
private static LambdaExpression GenerateSoftDeleteFilter(Type entityType)
{
var parameter = Expression.Parameter(entityType, "e");
var property = Expression.Property(parameter, "IsDeleted");
var condition = Expression.Equal(property, Expression.Constant(false));
return Expression.Lambda(condition, parameter);
}
}
*/
// 2. 多租户过滤器
/*
public class AppDbContext : DbContext
{
private readonly int _currentTenantId;
public AppDbContext(DbContextOptions options, ITenantProvider tenantProvider)
: base(options)
{
_currentTenantId = tenantProvider.GetCurrentTenantId();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasQueryFilter(o => o.TenantId == _currentTenantId);
modelBuilder.Entity<Product>()
.HasQueryFilter(p => p.TenantId == _currentTenantId);
modelBuilder.Entity<User>()
.HasQueryFilter(u => u.TenantId == _currentTenantId && u.IsActive);
}
}
*/
// 3. 禁用全局过滤器(某些场景需要查询全部数据)
// 使用 IgnoreQueryFilters
var allProducts = await context.Products
.IgnoreQueryFilters() // 忽略全局过滤器
.Where(p => p.IsDeleted) // 查询已删除的
.ToListAsync();
// 4. 组合多个过滤条件
/*
modelBuilder.Entity<Order>()
.HasQueryFilter(o =>
!o.IsDeleted &&
o.TenantId == _currentTenantId &&
o.Status != "draft");
*/优点
缺点
总结
EF Core 提供了丰富的高级查询功能,Include/ThenInclude 解决了 N+1 查询问题,SplitQueries 优化了多集合关联查询的性能,原生 SQL 弥补了 LINQ 在复杂查询上的不足,编译查询显著提升了高频查询的性能,全局查询过滤器则提供了优雅的数据隔离方案。在实际项目中,应该根据具体场景选择合适的查询策略,在开发效率和运行性能之间取得平衡。
关键知识点
- 数据库主题一定要同时看数据模型、读写模式和执行代价。
- 很多性能问题不是 SQL 语法问题,而是索引、统计信息、事务和数据分布问题。
- 高可用、备份、迁移和治理与查询优化同样重要。
- 先把数据模型、访问模式和执行代价绑定起来理解。
项目落地视角
- 所有优化前后都保留执行计划、样本 SQL 和关键指标对比。
- 上线前准备回滚脚本、备份点和校验方案。
- 把连接池、锁等待、慢查询和容量增长纳入日常巡检。
- 保留执行计划、样本 SQL、索引定义和优化前后指标。
常见误区
- 脱离真实数据分布讨论索引或分片。
- 只看单条 SQL,不看整条业务链路的事务和锁。
- 把测试环境结论直接等同于生产环境结论。
- 脱离真实数据分布设计索引。
进阶路线
- 继续向执行计划、存储引擎、复制机制和数据治理层深入。
- 把主题与 ORM、缓存、消息队列和归档策略联动起来思考。
- 沉淀成数据库设计规范、SQL 审核规则和变更流程。
- 继续深入存储引擎、复制机制、归档与冷热分层治理。
适用场景
- 当你准备把《EF Core 高级查询》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合数据建模、查询优化、事务控制、高可用和迁移治理。
- 当系统开始遇到慢查询、锁冲突、热点数据或容量增长时,这类主题价值最高。
落地建议
- 先分析真实查询模式、数据量级和写入特征,再决定索引或分片策略。
- 所有优化结论都结合执行计划、样本数据和监控指标验证。
- 高风险操作前准备备份、回滚脚本与校验 SQL。
排错清单
- 先确认瓶颈在 CPU、I/O、锁等待、网络还是 SQL 本身。
- 检查执行计划是否走错索引、是否发生排序或全表扫描。
- 排查长事务、隐式类型转换、统计信息过期和参数嗅探。
复盘问题
- 如果把《EF Core 高级查询》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《EF Core 高级查询》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《EF Core 高级查询》最大的收益和代价分别是什么?
