Chapter 10 · Loading Related Data: Eager, Explicit, Lazy, Filtered Include, and Split Queries
Single vs Split Queries: Cartesian Explosion, Round Trips, Ordering, and Consistency Tradeoffs
Compare single and split related-data queries with same-level collections, cartesian explosion, duplicated payloads, extra round trips, EF Core 10 ordering behavior, and consistency tradeoffs.
Learning outcomes
A dashboard needs work orders with tags, notes, and attachments. A single query returns the right graph but multiplies sibling collections; a split query removes the cross product but executes multiple SQL statements and can observe database changes between them. This lesson compares the two strategies with cardinality math, command logs, EF Core 10 ordering behavior, network/consistency tradeoffs, and real provider evidence.
Explain cartesian explosion versus ordinary principal-column duplication.
Use AsSingleQuery and AsSplitQuery intentionally and inspect their SQL/command counts.
Understand EF Core 10 split-query ordering improvements while retaining deterministic ordering discipline.
Explain why split statements can observe different database states without an appropriate transaction/isolation guarantee.
Include provider/network latency and buffering constraints in the decision.
Build an evidence sheet instead of adopting “always split” or “always single” rules.
Mandatory labs use .NET 10 SDK 10.0.400, .NET runtime 10.0.11, EF Core/SQLite 10.0.11, the disposable servicehub-lab.db, deterministic seed data, and no paid tooling. SQL Server/PostgreSQL notes are comparative only unless explicitly labeled.
1. Same-level collections multiply each other
var query = db.WorkOrders .Where(w => w.Id == id) .Include(w => w.WorkOrderTags) .Include(w => w.Notes) .Include(w => w.Attachments) .AsSingleQuery();Console.WriteLine(query.ToQueryString());
If the principal has 5 tag links, 4 notes, and 3 attachments, the relational join can produce 60 rows for that one work order. This is cartesian explosion across sibling collections. A nested path such as WorkOrder → WorkOrderTags → Tag does not create the same sibling cross product by itself because the second collection is not another collection directly off WorkOrder.
SELECT w..., wt..., n..., a...FROM work_orders AS wLEFT JOIN work_order_tags AS wt ON w.work_order_id = wt.work_order_idLEFT JOIN work_order_notes AS n ON w.work_order_id = n.work_order_idLEFT JOIN work_order_attachments AS a ON w.work_order_id = a.WorkOrderIdWHERE w.work_order_id = @__id_0ORDER BY w.work_order_id, wt.work_order_id, wt.tag_id, n.id;
2. Data duplication is different from cartesian explosion
Even one collection Include duplicates principal columns once per child row. This is often harmless, but it matters if the principal contains a large text/BLOB/JSON payload. Cartesian explosion multiplies sibling child rows; data duplication repeats principal columns. The repairs differ: projection can omit large principal columns, while split queries can avoid sibling cross products.
| Problem | Symptom | Typical lever |
|---|---|---|
| Principal duplication | Same principal payload repeated per child row | Projection / omit large columns |
| Cartesian explosion | Sibling collection counts multiply | Split query or reshape request |
| Hidden N+1 | Many separate child commands | Set-based eager/projection/explicit batch |
| Too much graph | Correct entities but unused data | DTO/read-model projection |
3. AsSplitQuery changes execution into multiple statements
var query = db.WorkOrders .Where(w => w.Id == id) .Include(w => w.WorkOrderTags) .Include(w => w.Notes) .Include(w => w.Attachments) .AsSplitQuery() .TagWith("Chapter10/Lesson3 split graph");var graph = await query.SingleAsync(ct);
EF executes a root query and additional query/queries for included collection edges, then assembles the graph using keys. One-to-one related entities remain joined because they do not create collection multiplication. In current EF Core, each additional statement still represents a database round trip, so high network latency can make split mode slower even when it transfers fewer rows.
Command 1: SELECT ... FROM work_orders WHERE work_order_id = @idCommand 2: SELECT ... FROM work_orders JOIN work_order_tags ... WHERE work_order_id = @idCommand 3: SELECT ... FROM work_orders JOIN work_order_notes ... WHERE work_order_id = @idCommand 4: SELECT ... FROM work_orders JOIN work_order_attachments ... WHERE work_order_id = @id
4. EF Core 10 fixed inconsistent split-query ordering propagation
Before EF Core 10, some split queries using pagination could apply an augmented unique ordering in the first statement but fail to propagate the same ordering into a later subquery, creating an incorrect-results risk. EF Core 10 fixes that propagation. That does not make ordering optional: relational databases still provide no default row order, and production pagination should use a fully deterministic order.
var page = await db.WorkOrders .OrderBy(w => EF.Property<DateTime>(w, "CreatedUtc")) .ThenBy(w => w.Id) .Skip(pageIndex * pageSize) .Take(pageSize) .Include(w => w.Notes) .AsSplitQuery() .ToListAsync(ct);
The shadow CreatedUtc : DateTime is reused from
Chapter 04 because it is safely orderable in the mandatory
SQLite lab. The primary key tie-breaker makes the ordering
unique.
5. Consistency: multiple statements can see multiple moments
A single SQL statement normally observes one statement-level database view according to that engine/isolation mode. A split query executes multiple statements. Another transaction can insert/delete/update related rows between them, so the assembled graph may combine observations from different moments. Wrapping the split query in a stronger transaction (for example, snapshot/serializable where supported and appropriate) can improve consistency but adds locking/version-store/performance tradeoffs. Provider semantics matter.
The local SQLite lab can demonstrate multiple statements and command counts, but it is not a substitute for testing the production provider’s transaction/isolation behavior. Reproduce consistency requirements against SQL Server/PostgreSQL/MySQL/Oracle as applicable.
6. Deliberately wrong: make split queries a global superstition
options.UseSqlite(connectionString, sqlite => sqlite.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
A global default can be valid for a known workload, but it is
not a free optimization. It can increase round trips, buffering,
and repeated reference joins. Conversely, leaving single-query
mode everywhere can trigger EF warnings and huge joins. The
repair is to define a default intentionally and override hot
queries with AsSingleQuery/AsSplitQuery
based on evidence.
var graph = await db.WorkOrders .Include(w => w.WorkOrderTags) .Include(w => w.Notes) .AsSplitQuery() // justified by measured sibling cardinality .ToListAsync(ct);
7. Build an evidence sheet, not a folklore rule
| Evidence | Single query | Split query |
|---|---|---|
| SQL commands | Usually one | Root + collection statements |
| Sibling collection rows | Can multiply | Avoids sibling cross product |
| Network round trips | Fewer | More in current implementation |
| Consistency window | One statement | Multiple statements unless protected by transaction semantics |
| Memory/buffering | Large joined row stream possible | Earlier result sets may need buffering before later statements |
| Reference navigations | Joined once in single shape | May be joined into each split collection statement |
| Best choice | Workload-dependent | Workload-dependent |
Capture dataset size/distribution, collection cardinalities, selected columns, tracking mode, command count, elapsed time in Release mode, database plan/metrics, and network topology. If you cannot reproduce the production latency/topology locally, record that limitation instead of inventing a number.
8. Hands-on lab: same graph, two execution strategies
- Seed 20 work orders with deterministic child cardinalities, including one “wide” work order with 5 tags, 4 notes, and 3 attachments.
-
Run the same include graph with
AsSingleQuery()andAsSplitQuery(). -
Capture
ToQueryString(), EF command logs, and result counts. -
Use SQLite
EXPLAIN QUERY PLANon the emitted statements where useful; do not treat it as a cross-provider plan. -
Add deterministic pagination using
CreatedUtc+Idand verify stable keys across repeated runs. - Record a decision table for local SQLite and separately state what must be retested on the production provider/network.
Check your understanding
- What causes cartesian explosion?
- How is principal duplication different?
- What does AsSplitQuery trade away?
- What EF Core 10 split-query issue was fixed?
- Should you still use unique ordering in EF Core 10 pagination?
- Why can SQLite results not settle a cloud SQL decision?
Review the answers
Multiple sibling collection joins at the same level produce a cross product of their child rows.
It repeats the principal columns per child row even with one collection; it does not require sibling multiplication.
It avoids the large sibling join but adds SQL statements/round trips and a multi-statement consistency window.
Ordering augmentation is propagated consistently into the split subqueries used for pagination.
Yes. Relational order is undefined without ORDER BY, and a unique tie-breaker gives deterministic pagination semantics.
Provider isolation, plans, latency, buffering, indexes, and network topology can differ materially.
9. Production judgment and bridge
Use AsSplitQuery when it solves a measured
collection-join problem and its extra statements/consistency
window are acceptable. Use AsSingleQuery when the
joined shape is bounded and round-trip latency dominates. Often
the best answer is a projection that avoids loading an entity
graph at all. Lesson 4 covers another multiple-query
strategy—explicit loading—where the application decides exactly
when a relationship query runs.
Authoritative references
- Single vs. Split Queries - EF Core — cartesian explosion, data duplication, split-query characteristics and configuration.
- What's New in EF Core 10 — consistent ordering fix for split queries.
- Eager Loading of Related Data - EF Core — Include behavior and collection-loading caution.
- Using Transactions - EF Core — transaction/isolation coordination.
- SQLite Query Planning — local-plan evidence for the free lab.