EF Core 11 提升拆分查询性能
速览
EF Core 11 引入了对拆分查询的性能优化,使得在处理复杂查询时能够更高效地分解和执行。该改进减少了不必要的数据库往返,提高了整体数据访问速度,尤其对关联数据加载场景有明显益处。这有助于开发者在 .NET 应用中构建更高效的数据库交互层。
AI 深度解读
背景
在 Entity Framework Core(EF Core)中,默认情况下,当使用 Include 加载导航属性时,EF Core 会生成一个包含多个 JOIN 的单一查询。如果 Include 包含集合导航属性,JOIN 会导致父行对每个子行重复出现,导航属性越多,查询性能越差,这种冗余会随着 JOIN 的增多而加剧。
为了缓解这一问题,EF Core 提供了 AsSplitQuery 方法。使用 AsSplitQuery 后,EF Core 会为根实体执行一次查询,为每个额外的集合导航属性分别执行一次查询,从而避免笛卡尔积爆炸。但这并非银弹:拆分查询会增加数据库往返次数,并且由于不是原子操作(涉及多个事务),根实体和子实体之间的状态可能发生变化。
核心内容
EF Core 11(自 preview 3 起)对 AsSplitQuery 进行了优化,使其生成更高效的 SQL 查询。
问题重现:一个包含一个引用导航属性(BlogType)和一个集合导航属性(Posts)的查询:
var result = context.Blogs
.Include(x => x.BlogType) // 引用导航
.Include(x => x.Posts) // 集合导航
.AsSplitQuery()
.ToList();
在 EF Core 10 及之前版本中,这会产生两个查询:
查询 1(获取根实体和引用导航):
SELECT [b].[Id], [b].[BlogDetailId], [b].[BlogTypeId], [b].[Name], [b0].[Id], [b0].[Type]
FROM [Blogs] AS [b]
LEFT JOIN [BlogType] AS [b0] ON [b].[BlogTypeId] = [b0].[Id]
ORDER BY [b].[Id], [b0].[Id]
查询 2(获取集合导航 Posts):
SELECT [p].[Id], [p].[BlogId], [p].[Title], [b].[Id], [b0].[Id]
FROM [Blogs] AS [b]
LEFT JOIN [BlogType] AS [b0] ON [b].[BlogTypeId] = [b0].[Id]
INNER JOIN [Post] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id], [b0].[Id]
注意第二个查询:它本意是获取 Posts,却仍然 JOIN 了 BlogType 表,并选择了它的 Id 和按 Id 排序。这些操作完全多余——要将 Post 匹配到对应的 Blog,只需主键 Blog.Id 就足够。随着引用导航属性的增多,每个集合查询都会拖累额外的 JOIN 和 ORDER BY 列,数据库做了无用功,查询计划也因此受损。
EF Core 11 的改进:从预览版 3 开始,EF Core 在生成集合查询时会自动剪裁(prune)掉那些不需要的引用导航属性。第二个查询现在变得简洁,就像手写一样:
SELECT [p].[Id], [p].[BlogId], [p].[Title], [b].[Id]
FROM [Blogs] AS [b]
INNER JOIN [Post] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]
基准测试:作者在 .NET 10(EF Core 10)和 .NET 11(EF Core 11)上进行了对比测试。测试环境使用 SQLite 内存数据库,包含 5000 个 Blog,每个 Blog 有 5 个 Post。查询包含三个引用导航属性(Author、Category、Series)和一个集合导航属性(Posts),并使用 AsSplitQuery。结果如下:
| Method | Runtime | Mean | Error | StdDev | Median | Gen0 | Gen1 | Gen2 | Allocated | |------------|-----------|------------|------------|------------|------------|------------|------------|------------|-----------| | SplitQuery | .NET 10.0 | 68.53 ms | 1.358 ms | 2.742 ms | 67.54 ms | 4375.0000 | 1000.0000 | 375.0000 | 33.35 MB | | SplitQuery | .NET 11.0 | 62.07 ms | 1.213 ms | 2.788 ms | 61.10 ms | 4111.1111 | 1333.3333 | 444.4444 | 30.15 MB |
结果显示,.NET 11 上的执行时间更短、内存分配更少(30.15 MB vs 33.35 MB),且 GC 压力也有所降低。
注意:基准测试同时更改了运行时版本(.NET 10 → .NET 11)和 EF Core 版本(10 → 11),理论上部分性能提升可能来自 .NET 运行时本身,但可能性较小;主要优化来自 EF Core 11 的剪枝策略。
关键要点
- 问题根源:EF Core 在拆分查询中,集合导航的查询不必要地包含了所有引用导航属性的 JOIN 和 ORDER BY,导致额外开销。
- 解决方案:EF Core 11 自动剪裁集合查询中的冗余引用导航 JOIN,只保留必要的根实体主键和集合导航表。
- 性能提升:基准测试表明,使用新版本后查询耗时减少约 9.5%,内存分配减少约 9.6%,GC 压力降低。
- 适用范围:适用于使用了
AsSplitQuery且包含至少一个集合导航和多个引用导航的场景;引用导航越多,优化效果越明显。 - 向后兼容:该优化是透明且自动的,无需开发者修改代码;通过升级到 EF Core 11 即可受益。
- 限制:拆分查询仍然存在多往返和非原子性问题,此优化仅减少每次往返的冗余负载,不改变拆分查询的根本特性。
意义与影响
EF Core 11 的这一优化对于依赖拆分查询的应用程序具有实际价值。许多真实项目为了保护数据库资源或提高查询可读性而使用 AsSplitQuery,但过去不得不忍受因多余 JOIN 带来的性能惩罚。现在,EF Core 自动生成了更高效的 SQL,这意味着开发者可以继续享受拆分查询带来的好处(避免笛卡尔积),同时获得更好的执行速度和更低的内存占用。
该改进也体现了 EF Core 团队对查询优化的持续投入——通过分析查询模式并移除冗余操作,使 ORM 生成的 SQL 更接近手写优化版本。对于大型数据集或高并发场景,即使是 6-10% 的性能提升也能显著降低数据库负载和应用程序延迟。
开发者只需升级到 .NET 11 和 EF Core 11(或更高版本),即可在现有项目中无痛获得这一优化。此外,该优化也为未来进一步压缩拆分查询的冗余提供了基础,例如在包含多个集合导航时进一步优化查询计划。
