EF Core 11 让拆分查询更快

Hacker News Top 工具

摘要

EF Core 11 通过移除集合查询中不必要的引用导航 JOIN,优化拆分查询,从而减少数据库开销并提升性能。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/07/12 10:47

# EF Core 11 让拆分查询变得更快 来源:https://steven-giesel.com/blogPost/d4401fd0-805a-4703-9d9e-5fe3b57c25ea 如果你在代码库中使用 `AsSplitQuery`,EF Core 11 为你准备了一份礼物:你的查询会变得更快。默认情况下,EF Core 将所有内容加载到一个查询中。如果你 `Include` 一个集合,那就意味着一个 JOIN,而针对集合的 JOIN 会使父行的每个子行都重复出现。你 JOIN 的东西越多,情况就越糟糕。`AsSplitQuery` 解决了这个问题:EF 会为根实体执行一个查询,再为每个额外的集合执行一个查询。当然,这不是万灵药,也可能导致: - 更多的往返次数 - 根实体与子实体之间的状态可能发生变化,因为这不是一个原子操作(我们有多个事务)。 更多信息见:https://learn.microsoft.com/en-us/ef/core/querying/single-split-queries ## 那么问题是什么? 以下是来自原始问题(https://github.com/dotnet/efcore/issues/29182)的设置:一个引用导航,一个集合导航: ```csharp var result = context.Blogs .Include(x => x.BlogType) // 引用导航 .Include(x => x.Posts) // 集合导航 .AsSplitQuery() .ToList(); ``` 直到 EF Core 10,这会生成两个查询: ```sql 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] ``` 看看第二个查询:那个获取 Posts 的查询。为什么 `BlogType` 在里面?它 JOIN 了该表,选择了它的 `Id`,并按它排序。这些都不需要:要将 `Post` 匹配到其 `Blog`,博客的主键就足够了。而且当你拥有更多引用导航时,情况会更糟。五个 `Include` 引用导航?每个集合查询都会拖上五个 JOIN 和五个额外的 ORDER BY 列。数据库做了这些工作,却又丢弃了结果,你的查询计划也会因此受到影响。 ## 那么有什么变化? EF Core 11(从预览版 3 开始)会从集合查询中修剪引用导航。第二个查询现在看起来就像你自己手写的那样: ```sql 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 issue 中找到几乎相同的设置。 --- **免责声明**:我对比了 EF 10(在 .NET 10 中)和 EF 11(在 .NET 11 中)。也就是说,并非所有性能提升都必然来自 EF 11 本身,因为我同时在基准测试中更改了两个变量。理论上,可能完全是由于 .NET 10 到 .NET 11 的升级,但可能性不大。 --- ```csharp [MemoryDiagnoser] [SimpleJob(RuntimeMoniker.Net10_0)] [SimpleJob(RuntimeMoniker.Net11_0)] public class SplitQueryBenchmarks { private SqliteConnection _connection = null!; private DbContextOptions _options = null!; [GlobalSetup] public void Setup() { _connection = new SqliteConnection("Data Source=:memory:"); _connection.Open(); _options = new DbContextOptionsBuilder() .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 SplitQuery() { using var context = new BloggingContext(_options); return BlogQuery(context).AsSplitQuery().ToList(); } private static IQueryable 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 | 更少的运行时间和更少的内存分配! ## 资源 - 本篇博客文章的源代码:这里(https://github.com/linkdotnet/BlogExamples/tree/main/SplitQueryEF11) - 我所有的示例代码都托管在此仓库中:这里(https://github.com/linkdotnet/BlogExamples) - GitHub Issue:https://github.com/dotnet/efcore/issues/29182

相似文章

libffi 的性能改进

Lobsters Hottest

本文详细介绍了 libffi 中的一项性能改进:将参数放置缓存为扁平移动列表(即“计划”),从而消除了每次函数调用时的冗余重新分类,在不使用 JIT 编译的情况下实现了显著的加速。

CORVUS:基于底层同步的LLM编码代理上下文优化与缩减

arXiv cs.LG

CORVUS提出了一种新的轨迹架构用于LLM编码代理,通过维护相关文件的同步注册表,将文件读取操作与观察结果解耦,从而减少输入令牌9-50%,推理周期最多减少37%,同时在SWE-bench基准上保持相当的通过率。