EF Core 11 让拆分查询更快:运行时间缩短、内存分配减少!
2026 年 6 月 8 日,这是一件值得开发者关注的技术优化事件。如果你在代码库中使用了 `AsSplitQuery`,EF Core 11 会给你带来查询速度提升的惊喜。
目录
问题是什么?
有什么变化?
基准测试
资源
默认情况下,EF Core 会在一个查询中加载所有内容。若使用 `Include` 加载一个集合,会使用 JOIN 操作,对集合使用 JOIN 会为每个子项复制父行,连接内容越多情况越糟。`AsSplitQuery` 可解决该问题,EF 会为根实体执行一个查询,为每个额外的集合执行一个查询。不过这并非万能之策,还可能导致更多往返操作,且因不是原子操作(涉及多个事务),根实体和子项之间的状态可能发生变化。更多信息请见: 。
问题是什么?
以下是 原始问题 的设置:一个引用导航属性,一个集合导航属性。
var result = context.Blogs .Include(x => x.BlogType) // 引用导航属性 .Include(x => x.Posts) // 集合导航属性 .AsSplitQuery() .ToList();在 EF Core 10 及之前的版本中,这会生成两个查询。
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] 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]看看第二个查询,即获取文章的查询。其中出现 `BlogType` 是不必要的,它连接了该表,选择了其 `Id` 并按其排序。要将 `Post` 与其 `Blog` 匹配,博客的主键就足够了。而且,引用导航属性越多情况越糟,若有五个引用导航属性使用 `Include`,每个集合查询都会附带五个 JOIN 操作和五个额外的 ORDER BY 列,数据库执行这些操作却丢弃结果,查询计划也会受影响。
有什么变化?
从预览版 3 开始,EF Core 11 会从集合查询中移除引用导航属性。现在,第二个查询就像手动编写的一样。
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]基准测试
会在博客文章末尾提供完整设置的链接。以下是使用的一个小设置,可在上面链接的 Github 问题中找到几乎相同的内容。
免责声明:对比了 .NET 10 中的 EF 10 和 .NET 11 中的 EF 11。需说明的是,并非所有性能提升都一定仅来自 EF 11,因为在基准测试中改变了两个变量。理论上,也可能完全是从 .NET 10 升级到 .NET 11 的原因,但这种可能性不大。
[MemoryDiagnoser] [SimpleJob(RuntimeMoniker.Net10_0)] [SimpleJob(RuntimeMoniker.Net11_0)] public class SplitQueryBenchmarks { private SqliteConnection _connection = null!; private DbContextOptions<BloggingContext> _options = null!; [GlobalSetup] public void Setup() { _connection = new SqliteConnection("Data Source=:memory:"); _connection.Open(); _options = new DbContextOptionsBuilder<BloggingContext>() .UseSqlite(_connection) .Options; using var context = new BloggingContext(_options); context.Database.EnsureCreated(); Seeder.Seed(context, blogCount: 5_000, postsPerBlog: 5); } [GlobalCleanup] public void Cleanup() => _connection.Dispose(); [Benchmark] public List<Blog> SplitQuery() { using var context = new BloggingContext(_options); return BlogQuery(context).AsSplitQuery().ToList(); } private static IQueryable<Blog> BlogQuery(BloggingContext context) => context.Blogs .AsNoTracking() .Include(b => b.Author) .Include(b => b.Category) .Include(b => b.Series) .Include(b => b.Posts); }结果如下:
| 方法 | 运行时 | 平均值 | 误差 | 标准差 | 中位数 | Gen0 | Gen1 | Gen2 | 分配内存 |
|---|---|---|---|---|---|---|---|---|---|
| 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 |
可以看到,运行时间更短,内存分配也更少!
资源
本文的源代码:点击查看
所有的示例代码都托管在这个仓库中:点击查看
Github 问题:https://github.com/dotnet/efcore/issues/29182
你可能感兴趣的相关文章
EF Core 6 - 新特性
EF Core 6 即将推出,看起来非常有前景。在这篇博客文章中,将介绍即将推出的新特性。2021 年 7 月 12 日 阅读时长:3 分钟 EF CoreC#.NET 6EF Core 6
我的 EF LINQ to SQL 查询安全吗?
是否曾自问,EF LINQ 查询是否容易受到 SQL 注入攻击?是因为从文本字段中查询用户数据,还是直接将 API 接收到的数据用于数据库查询?2024 年 6 月 17 日 阅读时长:3 分钟 C#.NETEntity FrameworkEF
.NET 11 中的 Zstandard 压缩
.NET 已经有 DeflateStream、GZipStream、ZLibStream 和 BrotliStream 有一段时间了。在 .NET 11 中,一个新成员加入了:ZstandardStream。现在可以在 .NET 中使用 “Zstd” 了。2026 年 5 月 18 日 阅读时长:2 分钟 .NET.NET 11