应于多级include引发笛卡尔爆炸时使用assplitquery:即sql变慢、内存暴涨、返回行数远超预期,如100订单×5订单项→500行冗余;它将join拆为独立查询并在内存组合,适用于主表小、子表大的关联加载场景。

AsSplitQuery 什么时候该用
当 Include 一对多关系后,SQL 执行明显变慢、内存暴涨、返回记录数远超预期——大概率是笛卡尔积爆炸了。比如查 100 个订单,每个订单平均 5 条订单项,Include(o => o.OrderItems) 会生成单条 JOIN 查询,结果集变成 500 行,客户信息重复 500 次,不是 100 次。
这种情况 AsSplitQuery 就是解药:它把一次 JOIN 查询拆成两次独立查询(主表 + 子表),再由 EF Core 在内存里组合。适合子表数据量大、但你又确实需要加载关联集合的场景。
- 主表数据量小(
- 你用了
Include且观察到 SQL 返回行数呈乘积增长 - 你不需要跨表做 WHERE 或 ORDER BY 等数据库端聚合逻辑
- 你没启用懒加载,也确认没漏掉
AsNoTracking()(拆分查询 + 跟踪模式 = 更高内存开销)
AsSplitQuery 怎么写才生效
AsSplitQuery 必须紧跟在查询构建链的末尾、执行方法(如 ToList()、FirstOrDefault())之前,且只对支持拆分的查询有效——也就是含 Include 的查询。它不会影响纯 Select 投影或 Join 写法。
正确写法示例:
var orders = context.Orders
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.AsSplitQuery() // ✅ 放这里,紧挨执行前
.ToList();
常见失效点:
- 写在
Where或OrderBy前面 → 不报错但无效 - 和
AsNoTracking()顺序颠倒(如AsNoTracking().AsSplitQuery())→ EF Core 8+ 会忽略AsSplitQuery - 查询里用了
ThenInclude加载三级以上关系 → 拆分只作用于第一层Include,深层仍可能 JOIN - 启用了全局查询过滤器(Global Query Filters)→ 拆分后每个查询都会重复应用过滤,需确认语义是否符合预期
拆分查询的代价你得知道
它不是银弹。每次拆分会多一次数据库往返,网络延迟和连接开销真实存在。如果主表只有几条记录,或者子表极小(如每个订单最多 1–2 条项),用 AsSplitQuery 反而更慢。
更要小心的是事务边界:两个查询不在同一个数据库事务中(除非你手动包在 TransactionScope 里)。如果子表查询中途失败,主表数据已读出,EF Core 不会回滚主表查询结果。
- 测试时务必对比
AsSplitQuery前后的实际 SQL 日志,确认是否真拆成了两条 SELECT - 避免在高并发短时接口中无差别开启,尤其连接池紧张时
- 若子表需按主表字段排序(如
OrderItems.OrderBy(i => i.Order.CreatedAt)),拆分后无法在数据库端完成,得靠内存排序,大数据量下吃 CPU
比 AsSplitQuery 更轻量的替代方案
如果只是展示用,根本不需要实体对象图,优先考虑 Select 投影——它生成的是单条精简 SQL,字段可控、无冗余、不触发笛卡尔积。
例如查订单号、客户名、订单项数量:
var result = context.Orders
.Select(o => new {
OrderNo = o.OrderNo,
CustomerName = o.Customer.Name,
ItemCount = o.OrderItems.Count()
})
.ToList();
这种写法比 AsSplitQuery 更快、更省内存,且天然规避所有 JOIN 相关陷阱。只有当你明确需要修改子集合(比如加载后调用 .Add() / .Remove()),才回到 Include + AsSplitQuery 路线。
真正容易被忽略的点是:很多人以为“用了 Include 就必须用 AsSplitQuery 来优化”,其实第一步该问的是——我到底需不需要整个实体?还是只要几个字段?后者才是最常被跳过的性能开关。











