EF Core 11 让拆分查询更快
摘要
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
相似文章
我在700个查询上对我的基于推理的检索系统与FAISS和BM25进行了基准测试,所有操作均在本地Qwen上运行。结果及不足之处
一个基于推理的检索系统(ClawIndex)在700个查询上与FAISS和BM25进行基准测试,使用本地Qwen,取得了更高的NDCG@10但在某些数据集上速度较慢且MRR较低,作者希望获得反馈。
@Greptime: GreptimeDB 的扁平格式查询现在可以在任何列上预过滤——标签、字段、时间戳——而不仅仅是主键。而…
GreptimeDB 的扁平格式查询现在支持在任何列(标签、字段、时间戳)上预过滤,而不仅仅是主键,性能提升高达 4.5 倍。此外,mito2 存储引擎移除了其遗留扫描路径,清理了约 1800 行代码。
libffi 的性能改进
本文详细介绍了 libffi 中的一项性能改进:将参数放置缓存为扁平移动列表(即“计划”),从而消除了每次函数调用时的冗余重新分类,在不使用 JIT 编译的情况下实现了显著的加速。
CORVUS:基于底层同步的LLM编码代理上下文优化与缩减
CORVUS提出了一种新的轨迹架构用于LLM编码代理,通过维护相关文件的同步注册表,将文件读取操作与观察结果解耦,从而减少输入令牌9-50%,推理周期最多减少37%,同时在SWE-bench基准上保持相当的通过率。
扩展Rails:41M请求/小时,8个数据库,disable_joins: true
Aura Frames将其Ruby on Rails应用扩展至每小时4100万请求,通过拆分为8个主数据库并利用Active Record中的disable_joins: true特性。