EF 11 Drops a Redundant Join From Split Queries

ef-coreperformancepostgresql
EF 11 Drops a Redundant Join From Split Queries

EF 11 stops joining reference navigations in the collection statements of a split query and drops their keys from ORDER BY. The database can't remove those joins on its own, and EF 11 still keeps one for a required reference that isn't included first. In my PostgreSQL benchmark, a query with one reference got 7 to 11 percent faster, and one with four got 21 to 24 percent faster.

Webinar: State of AI Engineering 2026
Progress and SlashData unpack insights from 800+ developers, architects, and engineering leaders on moving AI from experimentation to production. You'll also see the AI Engineering Maturity Index for benchmarking your own team. Register for the free webinar.

AI writes code fast. Someone still has to read it, verify it, and maintain it. Morph from Modelcode.AI modernizes legacy codebases, and the codebase analysis and modernization plan are free. You pay only if you approve the work, and code NETWEEKLY gets you 60,000 credits per month for six months. Analyze your codebase for free.

Back in September 2022, in the third (yes, 3rd) issue of this newsletter, I wrote about EF query splitting. I was fixing a problematic query with two levels of Include that kept timing out. Using AsSplitQuery fixed it.

A split query loads each included collection with its own SQL statement instead of one big JOIN. But each of those extra statements also carried a JOIN it didn't need, and the database can't optimize it away.

EF 11 fixes this, so I had to give it a try and measure if this gives a performance improvement. How much depends on the number of references, so I measured it with one and with four.

What a Split Query Sends

Here's the query from a small orders API I built for this test:

var orders = await db.Orders
    .Where(o => o.OrderedAt >= from && o.OrderedAt < to)
    .Include(o => o.Customer)
    .Include(o => o.Lines)
        .ThenInclude(l => l.Product)
    .Include(o => o.Payments)
    .AsSplitQuery()
    .AsNoTracking()
    .ToListAsync();

Customer is a reference navigation, one per order, and Lines and Payments are collections. In a single query, the two collections multiply each other, so an order with five lines and two payments comes back as ten rows.

AsSplitQuery sends one SELECT for the orders and one more for each collection. The tradeoff: three round trips instead of one, and nothing keeps the statements consistent unless you wrap them in a transaction.

The Join EF 11 Removes

Here's the statement that loads the lines, as EF 10 generates it for PostgreSQL:

SELECT s."Id", s."OrderId", s."ProductId", s."Quantity", ..., o."Id", c."Id"
FROM "Orders" AS o
INNER JOIN "Customers" AS c ON o."CustomerId" = c."Id"
INNER JOIN (
    SELECT o0."Id", o0."OrderId", ..., p."Name", p."Price", p."Sku"
    FROM "OrderLines" AS o0
    INNER JOIN "Products" AS p ON o0."ProductId" = p."Id"
) AS s ON o."Id" = s."OrderId"
WHERE o."OrderedAt" >= @from AND o."OrderedAt" < @to
ORDER BY o."Id", c."Id"

And the same statement from EF 11:

SELECT s."Id", s."OrderId", s."ProductId", s."Quantity", ..., o."Id"
FROM "Orders" AS o
INNER JOIN (
    SELECT o0."Id", o0."OrderId", ..., p."Name", p."Price", p."Sku"
    FROM "OrderLines" AS o0
    INNER JOIN "Products" AS p ON o0."ProductId" = p."Id"
) AS s ON o."Id" = s."OrderId"
WHERE o."OrderedAt" >= @from AND o."OrderedAt" < @to
ORDER BY o."Id"

In EF 10, the Customers join on line 3 only feeds c."Id" into the ORDER BY. EF 10 adds it so the rows come back in the same order as the first statement, which does need the customer's columns. But an order has exactly one customer, so sorting by c."Id" after o."Id" doesn't change anything.

The EF docs have listed this as a drawback of split queries for years, and the issue closed in March 2026.

EF 11 drops the join and c."Id":

EF 10 joins customers in all three split statements. EF 11 joins them only in the first.

The payments statement loses the same join. The Products join stays, since each line needs its product.

Why the Database Can't Remove It

Query planners rewrite a lot of what you send them, and removing a join is one of those rewrites. But only when the query uses nothing from the joined table. EF 10 selects c."Id" and sorts by it, so the join runs.

Here are the relevant lines from PostgreSQL 18's plan for the EF 10 statement:

Sort Key: o0."OrderId", c."Id"
->  Hash Join  (actual time=0.746..8.686 rows=9865.00 loops=1)
      Hash Cond: (o."CustomerId" = c."Id")
      ->  Seq Scan on "Customers" c  (actual time=0.005..0.040 rows=1000.00 loops=1)

The EF 11 plan doesn't have them, and the median of ten warm runs went from 12.13 ms to 10.40 ms. SQL Server 2022 keeps the join too.

Performance Improvements

I measured both versions with BenchmarkDotNet. Here's the benchmark for the two queries from the API, with a pooled DbContext:

[MemoryDiagnoser]
public class SplitQueryBenchmarks
{
    private const string ConnectionString =
        "Host=127.0.0.1;Port=5438;Database=orders;Username=mnw220;Password=mnw220";

    private static readonly DateTime From = Seeder.FirstDay.AddDays(10);
    private static readonly DateTime To = From.AddDays(3);

    private PooledDbContextFactory<AppDbContext> _factory = null!;

    [GlobalSetup]
    public void Setup() =>
        _factory = new PooledDbContextFactory<AppDbContext>(
            new DbContextOptionsBuilder<AppDbContext>().UseNpgsql(ConnectionString).Options);

    [Benchmark]
    public async Task<int> Page()
    {
        await using var db = _factory.CreateDbContext();
        var orders = await OrderQueries.PageAsync(db, customerId: 42, page: 0, CancellationToken.None);
        return orders.Count;
    }

    [Benchmark]
    public async Task<int> Report()
    {
        await using var db = _factory.CreateDbContext();
        var orders = await OrderQueries.RangeAsync(db, From, To, CancellationToken.None);
        return orders.Count;
    }
}

One benchmark project targets .NET 10 with EF 10 and .NET 11 with EF 11, and runs both queries against the same Postgres database:

var config = DefaultConfig.Instance
    .AddJob(Job.Default.WithRuntime(CoreRuntime.Core10_0).WithId("EF 10").AsBaseline())
    .AddJob(Job.Default.WithRuntime(CoreRuntime.Core11_0).WithId("EF 11"));

BenchmarkRunner.Run<SplitQueryBenchmarks>(config, args);

The project file references a different EF package for each target framework, so each job builds against its own EF version. Page loads one customer's 20 latest orders, and Report loads about 2,000 orders from a three-day window.

One reference is the smallest case, because EF 10 repeats every reference join in every collection statement. So I also ran both queries with four references on the order: Customer, a required ShippingAddress, an optional BillingAddress, and an optional Carrier.

QueryEF 10EF 11ChangeEF 10 allocatedEF 11 allocated
Page2.47 ms2.21 ms-11%277 KB262 KB
Report47.7 ms44.4 ms-7%22.3 MB21.1 MB
Page, four references3.32 ms2.51 ms-24%347 KB310 KB
Report, four references67.5 ms53.2 ms-21%28.0 MB24.5 MB

One reference was worth 7 to 11 percent, and four were worth 21 to 24 percent. Each EF 11 job also runs on .NET 11, so runtime changes are part of these numbers.

The split query lab has the benchmark project, the SQL from both versions, and my results.

The EF team's own benchmark measured 16 to 29 percent on SQL Server, for split queries with one to five optional references.

The Join EF 11 Still Keeps

EF 11 didn't remove all four joins. Here's its payments statement for the four-reference report:

SELECT p0."Id", p0."Amount", p0."Method", p0."OrderId", p0."PaidAt", o."Id"
FROM "Orders" AS o
INNER JOIN "Addresses" AS a ON o."ShippingAddressId" = a."Id"
INNER JOIN "Payments" AS p0 ON o."Id" = p0."OrderId"
WHERE o."OrderedAt" >= @from AND o."OrderedAt" < @to
ORDER BY o."Id"

The Customers, billing address, and Carriers joins are gone, but the ShippingAddress join is still there.

In EF 11, optional references (LEFT JOIN) are always removed. A required reference (INNER JOIN) is removed only when it comes first in the Include chain.

Here Customer comes first, so its join is gone. With ShippingAddress first, EF 11 removed that join and kept the Customers join.

So include the required reference with the biggest table first.

Summary

EF 11 removes a JOIN that split queries have carried since EF 5, and you don't change any code to see the benefits. The database couldn't remove it for you.

On PostgreSQL that was worth 7 to 11 percent with one reference, and 21 to 24 percent with four.

Whether or not those benefits materialize to anything substantial in your applications is something you'll have to measure.

I still recommend upgrading to .NET 11 to get all the runtime improvements. The new EF 11 features come as a nice bonus.

Thanks for reading.

And stay awesome!


Frequently Asked Questions

What changed for split queries in EF 11?

When a query includes a reference navigation and a collection and uses AsSplitQuery, EF 11 no longer joins the reference table in the SQL it generates for the collection. It also stops adding the reference key to the ORDER BY clause. The first statement, which returns the reference columns, keeps its join.

Why did earlier EF versions add that join?

The collection statements repeat the shape of the first statement so the rows come back in the same order, and the first statement joins the reference to return its columns. Its key then ended up in the ORDER BY of every collection statement. A reference key is fixed by the foreign key on the parent row, so ordering by it after the parent key changes nothing, and once it leaves the ORDER BY the join has no purpose.

Can the database optimizer remove the redundant join on its own?

Not for the SQL that EF 10 generates. PostgreSQL and SQL Server can remove a join only when the query uses nothing from the joined table, and EF 10 selects the reference key and sorts by it. The execution plans on PostgreSQL 18 and SQL Server 2022 both still run the join.

Does EF 11 change single queries too?

Yes, but less. In a single query the join has to stay because its columns are in the result, so EF 11 only removes the redundant reference key from the ORDER BY clause.

How much faster are split queries in EF 11?

It depends on how many reference navigations the query includes. In my BenchmarkDotNet run on PostgreSQL 18, with one reference a paged query loading 20 orders went from 2.47 ms to 2.21 ms, about 11 percent, and a report loading about 2,000 orders from 47.7 ms to 44.4 ms, about 7 percent. With four references the page went from 3.32 ms to 2.51 ms, about 24 percent, and the report from 67.5 ms to 53.2 ms, about 21 percent. The EF 11 job also runs on .NET 11, so runtime changes are part of that. The EF team measured 16 to 29 percent on SQL Server for split queries with optional references.

Does EF 11 remove every reference join from a split query?

No. Joins for optional references (LEFT JOIN) are always removed. The join for a required reference (INNER JOIN) is removed only when it is the first reference in the Include chain. A required reference included after another one keeps its join in every collection statement, although its key still leaves the ORDER BY clause.

Do I need to change my code to get the shorter SQL?

No. The change is in the query pipeline, so every existing query with Include and AsSplitQuery produces the new SQL as soon as the project references EF 11. Turning on LogTo for one split query is the quickest way to see the difference.

When should I not use a split query?

When the query includes one collection and a handful of references, a single query is usually fine and costs one round trip. Split queries pay off when two or more collections multiply the rows of a single query. Without an explicit transaction nothing keeps the separate statements consistent, so wrap them in a transaction if a concurrent insert between statements would matter.

Loading comments...

Whenever you're ready, there are 5 ways I can help you:

  1. Pragmatic Self-Hosting: Learn to deploy and run your apps on a VPS with Dokploy and Tailscale. Keep administration private, automate releases, and practice restoring backups, with source code and a runbook you can reuse.
  2. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  5. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

One practical .NET tip, every Saturday.

Join 66,000+ engineers who read it. Free, and you can unsubscribe any time.