Entity Framework Core 7 Performance: Cartesian Explosion

In Entity Framework Core 3 (EF 3) the SQL statement generation (re)introduced the Cartesian Explosion problem. A lot has happened since then, so it is time to revisit the issue with Entity Framework Core 7 (EF 7).

In this article:

pg
Pawel Gerr is architect and consultant at Thinktecture. He focuses on backends with .NET Core and knows Entity Framework inside out.

What is a "Cartesian Explosion"?

As the name implies, it has something to do with a cartesian product, i.e., with JOINs. When performing a JOIN on the one-to-many relationship then the rows on the one-side are replicated N times whereby N is the number of matching records on the many-side.

Please note: In this article, I use synchronous methods for better readability.
Also note: In this article, we are talking about one-to-many/many-to-many relationships only. A one-to-one relationship does not cause the Cartesian Explosion.

Here is an example for JOIN-ing 1 ProductGroup with 1000 Products.
The corresponding LINQ query:

				
					var groups = Context.ProductGroups
          .Include(g => g.Products)
          .ToList();
				
			

The SQL statement is like the following one:

				
					SELECT *
FROM [ProductGroups] AS [p]
LEFT JOIN [Products] AS [p0]
   ON [p].[Id] = [p0].[ProductGroupId]
ORDER BY [p].[Id]

				
			

And the result set:

ProductGroup Id
Product Id
1
1
1
2
1
3
1
1
1000

The columns of the ProductGroup are replicated 1000 times.

If there were another navigational property Sellers and there were 10 sellers per Product, then the result set will contain 1 * 1000 * 10 = 10000 rows, although we have just 1 + 1000 + 10 = 1011 records in the database.

				
					var groups = Context.ProductGroups
          .Include(g => g.Products)
          .ThenInclude(p => p.Sellers)
          .ToList();
				
			

With a few Include/ThenInclude more, the result set (i.e., the cartesian product) will explode.

EF-Forced "ORDER BY"

The larger result set due to JOINs is not the only performance issue. Depending on the included navigational properties, EF might add one or more ORDER BY clauses.

The good news is EF 7 doesn’t add an additional ORDER BY clause anymore when 1 navigational property is included; in contrast, EF 3 orders the data by both product group ID and product ID.

				
					SELECT *
FROM [ProductGroups] AS [p]
LEFT JOIN [Products] AS [p0]
   ON [p].[Id] = [p0].[ProductGroupId]
ORDER BY [p].[Id] // EF 7
// ORDER BY [p].[Id], [p0].[Id] // EF 3
				
			

The performance hit comes back when 2 or more navigational properties are included, e.g., Sellers.

				
					var groups = Context.ProductGroups
          .Include(g => g.Products)
          .ThenInclude(p => p.Sellers)
          .ToList();
				
			

For better materialization, i.e., the population of .NET entities, EF puts all identifiers into the ORDER BY clause. The resulting query will produce a considerable load on the database if the result set gets big.

				
					SELECT *
FROM [ProductGroups] AS [p]
LEFT JOIN (
    SELECT *
    FROM [Products] AS [p0]
    LEFT JOIN (
        SELECT *
        FROM [SellerProducts] AS [s]
        INNER JOIN [Sellers] AS [s0] ON [s].[SellerId] = [s0].[Id]
    ) AS [t] ON [p0].[Id] = [t].[ProductId]
) AS [t0] ON [p].[Id] = [t0].[ProductGroupId]
ORDER BY [p].[Id], [t0].[Id], [t0].[ProductId], [t0].[SellerId]

				
			

Query Splitting

The solution to the Cartesian Explosion Problem in EF 7 is still the same as in EF 3. The LINQ query should be split into multiple SQL statements if (and only if) the database load rises significantly. This can be done either manually, by writing multiple LINQ queries, or by using the method AsSplitQuery.

Please note, that the “alternative” way produces not the same result as the original query. The query Context.Sellers.ToList() loads all sellers and not just the ones attached to a product. Loading all sellers is an additional optimization because we know that (almost) all of them are going to be returned anyway.

For the following comparisons, the database contains 1000 products, 10 product groups, and 2 sellers.

				
					var groups = Context.ProductGroups
       // .AsSplitQuery()
          .Include(g => g.Products)
          .ThenInclude(p => p.Sellers)
          .ToList();

// Alternative to "AsSplitQuery"
var groups = Context.ProductGroups.ToList();
var products = Context.Products.ToList();
var sellers = Context.Sellers.ToList();
				
			

Below are some database statistics (MS SQL Server). The absolute numbers are not relevant. Just look at the relative difference, especially in Reads and Rows.

With query splitting, the CPU time seems to be less than 1ms, so the statistics return 0ms. I put 1ms instead because 0ms looks very odd.

Without AsSplitQuery
With AsSplitQuery
Alternative Way
CPU
16
1
1
Duration
233
420
264
Reads
6123
36
16
Rows
2000
2010
1012

With query splitting, the duration gets up because there are multiple round-trips to the database. But this increase in duration is more than acceptable if the number of reads goes down from 6k to 36 or even 16.

Summary

The Cartesian Explosion is still an issue in EF 7 and it is very likely not to disappear in the newer versions. EF team gave us the method AsSplitQuery which can mitigate the problem. This tool yields good results, but it won’t be a replacement for manual optimization by the developers.

Free
Newsletter

Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
.NET
pg
A key aspect of adopting any new pattern is understanding how it interacts with the surrounding application infrastructure. When using Discriminated Unions, questions arise: How can a Result union be serialized to JSON? How can an OrderState union be persisted using Entity Framework Core? This article explores practical integration strategies with common .NET frameworks.
02.11.2025
.NET
pg
While basic value objects solve primitive obsession, complex domain requirements need sophisticated modeling techniques. This article explores advanced patterns using Thinktecture.Runtime.Extensions to tackle real-world scenarios: open-ended dates for employment contracts, composite file identifiers across storage systems, recurring anniversaries without year components, and geographical jurisdictions using discriminated unions.
19.10.2025
.NET
pg
Domain models often involve concepts that exist in multiple distinct states or variations. Traditional approaches using enums and nullable properties can lead to invalid states and scattered logic. This article explores how discriminated unions provide a structured, type-safe way to model domain variants in .NET, aligning perfectly with Domain-Driven Design principles while enforcing invariants at the type level.
06.10.2025