Loading multiple sibling collection navigations in a single Entity Framework Core query causes the database to return the Cartesian product of those collections, duplicating the root entity’s data across many rows and inflating the query result.
Why is this an issue?
When a query includes two collection navigations on the same root entity, Entity Framework Core joins them in a single SQL statement. The database returns the Cartesian product of the two collections, and each returned row repeats the root entity’s columns.
Consider a Course with two Enrollments and two Announcements:
var courses = await ctx.Courses
.Include(c => c.Enrollments)
.Include(c => c.Announcements)
.ToListAsync();
Entity Framework Core joins both collections onto the root entity in a single SQL statement:
SELECT c.CourseId, c.Title, e.EnrollmentId, e.Grade, a.AnnouncementId, a.Message
FROM Courses AS c
LEFT JOIN Enrollments AS e ON c.CourseId = e.CourseId
LEFT JOIN Announcements AS a ON c.CourseId = a.CourseId
ORDER BY c.CourseId
Because the two joins are unrelated to each other, the database returns every Enrollment/Announcement combination for
each course - the Cartesian product of the two collections, with the course’s columns (CourseId, Title) repeated on every
row:
| c.CourseId | c.Title | e.EnrollmentId | e.Grade | a.AnnouncementId | a.Message |
|---|---|---|---|---|---|
|
1050 |
Chemistry |
1 |
A |
1 |
Midterm moved to Friday |
|
1050 |
Chemistry |
1 |
A |
2 |
Lab session canceled |
|
1050 |
Chemistry |
2 |
B |
1 |
Midterm moved to Friday |
|
1050 |
Chemistry |
2 |
B |
2 |
Lab session canceled |
Two Enrollments and two Announcements produce four rows instead of two. With 10 of each, the same query returns 100 rows,
and each additional sibling collection multiplies the row count again - so a query that should return a few hundred rows can return hundreds of
thousands.
Entity Framework Core also flags this situation at runtime through MultipleCollectionIncludeWarning.
Exceptions
The rule does not apply when:
- a single collection navigation, regardless of how many reference navigations the chain contains
- a collection reached through
ThenIncludewhen a collection earlier in the sameInclude/ThenIncludebranch was already counted - each row then represents a single descendant, not a sibling cross product. AThenIncludethat is the first collection reached in its branch is not exempt: it collides with sibling branches exactly like a top-levelIncludewould - a query that opts into split queries through
AsSplitQuery - a query that explicitly accepts the trade-off through
AsSingleQuery
How to fix it
Call AsSplitQuery on the query so that Entity Framework Core loads each collection in its own SQL statement.
Code examples
Noncompliant code example
var courses = await ctx.Courses
.Include(c => c.Enrollments)
.Include(c => c.Announcements) // Noncompliant: second sibling collection causes a Cartesian explosion
.ToListAsync();
Compliant solution
var courses = await ctx.Courses
.Include(c => c.Enrollments)
.Include(c => c.Announcements)
.AsSplitQuery()
.ToListAsync();
How does this work?
AsSplitQuery tells Entity Framework Core to run one SQL statement per collection navigation. Each statement returns only the rows of
its own table, so the Cartesian product disappears and the number of rows transferred drops from a product to a sum.
Pitfalls
Split queries run as separate database round trips. If the data changes between them, the returned object graph may be inconsistent. When the application requires a consistent view, wrap the query in a serializable or snapshot transaction.
Combining Skip or Take with AsSplitQuery on Entity Framework Core versions prior to 10 requires an ordering
that uniquely identifies each row. Otherwise the split queries may materialize different pages and return mismatched results.
How to opt out
Call AsSingleQuery instead to keep the single join and explicitly accept the Cartesian explosion. Unlike AsSplitQuery, it
does not change how the query runs - it only signals, in a way that both this rule and Entity Framework Core’s own runtime warning recognize, that the
trade-off was reviewed and accepted rather than overlooked.
Resources
Documentation
- Microsoft Learn - Single vs. Split Queries
- Microsoft Learn -
RelationalEventId.MultipleCollectionIncludeWarningField - Microsoft Learn -
RelationalQueryableExtensions.AsSplitQueryMethod