Introduction
When you work with Entity Framework Core (EF Core), loading related data is a normal part of building an application. For example, an e-commerce application may need to display a customer, their orders, and the products included in each order.
EF Core makes this easy with Include() and ThenInclude(). But there is one thing developers often overlook: loading more related data does not always mean the query will run efficiently.
When multiple collections are loaded together, EF Core may generate a SQL query that returns many rows containing repeated data. This can increase the amount of data transferred from the database and make the application slower.
EF Core provides two ways to handle this situation: single queries and split queries.
A single query loads related data through one SQL statement. A split query uses multiple SQL statements to retrieve the data and then combines the results.
Both approaches have their place. Let's understand how they work, compare their differences, and see which one makes more sense for your application.
What Is a Single Query in EF Core?
A single query retrieves the requested data using one SQL statement. This is the default behavior for most EF Core queries that use Include() unless you configure a different query-splitting behavior.
Consider an application that stores customers, orders, and order items.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Order> Orders { get; set; }
= new List<Order>();
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public decimal TotalAmount { get; set; }
public ICollection<OrderItem> Items { get; set; }
= new List<OrderItem>();
}
public class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
}
Now, suppose you want to retrieve customers along with their orders and order items.
var customers = await context.Customers
.Include(c => c.Orders)
.ThenInclude(o => o.Items)
.ToListAsync();
EF Core translates this query into SQL that joins the related tables and returns the data in a single result set.
This approach works well for many applications. However, the amount of data returned can grow quickly when several collections are involved.
Why can a single query become slow?
Imagine a customer has five orders, and each order contains four items. The database needs to return information about the customer, orders, and their items. Customer and order details may appear repeatedly across the result rows.
Now imagine the same query loads two separate collections, such as orders and addresses. Joining both collections can multiply the number of rows returned.
This problem is known as cartesian explosion. It can increase network traffic, database processing, and the amount of work EF Core performs to build the final objects.
The problem is not the Include() method itself. The problem is how much data the generated SQL needs to return.
What Is a Split Query in EF Core?
A split query retrieves related collections using separate SQL queries instead of combining everything into one large joined result set.
You can enable this behavior by adding AsSplitQuery() to your LINQ query.
var customers = await context.Customers
.Include(c => c.Orders)
.ThenInclude(o => o.Items)
.AsSplitQuery()
.ToListAsync();
EF Core executes multiple SQL statements and combines the results using the entity relationships.
Instead of returning one large result set with repeated customer and order information, EF Core retrieves the relevant data through separate queries.
This can reduce duplicated data, particularly when an application loads multiple large collections.
However, split queries are not always faster. Each additional SQL statement can require another database round trip. If the database is accessed over a network with noticeable latency, those extra round trips can increase the total response time.
That is why you should choose a query strategy based on the actual query and its performance, rather than assuming one approach is always better.
Single Query vs. Split Query: What's the Difference?
Feature | Single Query | Split Query |
|---|
SQL statements | Usually one | Multiple |
Database round trips | Usually fewer | Usually more |
Repeated data in results | Can be significant | Often reduced |
Cartesian explosion | Possible with multiple collections | Reduced for split collections |
Performance | Often suitable for smaller result sets | Can help with large collections |
Data consistency | One statement-level view, subject to isolation | Separate statements may observe different data |
Best use case | Simple queries and smaller object graphs | Queries loading multiple large collections |
The actual performance depends on the generated SQL, database indexes, data size, network latency, and database provider.
How to Use Single and Split Queries
EF Core gives you control over the query behavior. You can choose a strategy for an individual query or configure a default for the application.
1. Use AsSingleQuery()
If you want EF Core to execute a query as a single SQL statement, use AsSingleQuery().
var customers = await context.Customers
.Include(c => c.Orders)
.ThenInclude(o => o.Items)
.AsSingleQuery()
.ToListAsync();
This is especially useful when your application is configured to use split queries by default, but a particular query performs better as a single query.
For example, loading a product and its category usually does not require splitting because the result set is relatively simple.
2. Use AsSplitQuery()
When you need to load multiple collections, you can try AsSplitQuery().
var customers = await context.Customers
.Include(c => c.Orders)
.Include(c => c.Addresses)
.AsSplitQuery()
.ToListAsync();
This example assumes that the Customer entity has an Addresses navigation property.
EF Core can retrieve orders and addresses through separate queries instead of joining both collections into one result set.
This can help when the collections contain many records and a single query would return a large number of repeated rows.
3. Configure split queries globally
If most of your application's queries load multiple collections, you can configure split-query behavior as the default.
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
connectionString,
sqlOptions =>
sqlOptions.UseQuerySplittingBehavior(
QuerySplittingBehavior.SplitQuery)));
This configuration makes split queries the default behavior where applicable. You can still override the setting for an individual query with AsSingleQuery().
Before changing the global setting, review your existing queries. A configuration that improves one part of the application may increase latency in another.
When Should You Use a Single Query?
A single query is usually a good starting point when the amount of related data is small.
Consider using it when:
You are loading a single collection or a small object graph.
The generated SQL does not return excessive duplicate rows.
Database round-trip latency is a concern.
The query performs well under realistic workloads.
You want to retrieve related data in one statement.
For example, loading a product along with its category is a straightforward operation. Splitting this query would usually add complexity without providing a clear benefit.
A single query can also be a good choice when measurements show that its smaller number of database round trips outweighs the cost of processing the joined results.
When Should You Use a Split Query?
A split query is worth considering when the application loads several collections and the resulting SQL returns much more data than the application needs.
Consider using it when:
Multiple sibling collections are included in the same query.
The joined result contains substantial repeated data.
The query uses significant memory or transfers a large amount of data.
Performance measurements show that the additional round trips are worthwhile.
You need to reduce the cost of retrieving a large object graph.
For example, an order-management application might load orders, order items, and other related collections for a customer. If the single query produces a very large result set, splitting those collections may improve performance.
Remember that split queries do not automatically fix every slow query. If you retrieve thousands of unnecessary records, you still need to improve filtering, pagination, or the overall query design.
Best Practices for Production Applications
1. Check the generated SQL
Before changing the query strategy, understand what EF Core sends to the database.
var query = context.Customers
.Include(c => c.Orders)
.Include(c => c.Addresses)
.AsSplitQuery();
Console.WriteLine(query.ToQueryString());
ToQueryString() helps you inspect the SQL generated for the query. For split queries, also check EF Core's database command logs because the operation involves multiple SQL commands.
Look for unnecessary joins, large result sets, and repeated data.
2. Measure performance instead of guessing
Test both approaches with realistic data. Compare total request time, database execution time, the number of SQL commands, memory usage, and the amount of data returned.
A query that performs well with a few hundred records may behave differently when the database contains millions of records.
3. Use projections when you need only a few fields
Sometimes the best solution is not to load the entire entity graph.
For example, if a screen needs only an order ID, total amount, and item count, use a projection.
var orders = await context.Orders
.AsNoTracking()
.Select(o => new
{
o.Id,
o.TotalAmount,
ItemCount = o.Items.Count
})
.ToListAsync();
This query asks the database for only the required information instead of loading every order item.
For read-only screens, projections can reduce unnecessary data retrieval and object creation.
4. Apply filtering and pagination
Avoid loading every record when the application needs only a limited set.
var orders = await context.Orders
.Where(o => o.TotalAmount > 1000)
.OrderByDescending(o => o.Id)
.Take(50)
.AsSplitQuery()
.ToListAsync();
This query filters orders and limits the result to 50 records. The OrderByDescending() call provides a defined ordering before pagination.
Filtering and pagination reduce the number of root entities retrieved, although related collections can still contain many records.
5. Consider transaction consistency
A single SQL statement and a split query can behave differently when other transactions modify the database during execution.
A split query executes multiple statements. Data may change between those statements, so the final object graph might not represent one consistent point in time.
If the application requires a consistent view of related data, use an appropriate transaction and isolation level. On SQL Server, snapshot-based isolation may be suitable for some workloads when configured correctly.
Choose the isolation level according to the application's requirements, since stronger consistency can affect concurrency and database resource usage.
Common Mistakes and Troubleshooting
Problem | Possible reason | What to check |
|---|
The query is still slow | Too many records or inefficient SQL | Review filters, indexes, and execution plans |
Split query takes longer | Additional database round trips | Compare total latency for both strategies |
Memory usage remains high | The application loads a large object graph | Use projections and pagination |
Related data appears inconsistent | Data changes between split-query statements | Review transaction isolation |
Too many SQL commands execute | Split queries or repeated application-level loading | Inspect database command logs |
One common mistake is assuming that adding AsSplitQuery() will solve every performance problem. It addresses the cost of loading related collections through large joined result sets. It does not replace proper indexing, efficient filtering, or a suitable data model.
Another mistake is changing the query-splitting behavior globally without testing existing queries. Always verify that the change improves the queries that matter to your application.
Advantages and Disadvantages
Single-query approach
Advantages
Usually requires fewer database round trips.
Retrieves related data in one SQL statement.
Works well for small and straightforward object graphs.
Avoids inconsistencies caused by changes between separate statements of the same query operation.
Disadvantages
Can produce large result sets with repeated data.
May increase network traffic and memory usage.
Can become inefficient when several large collections are joined.
Split-query approach
Advantages
Reduces row multiplication caused by joining multiple collections.
Can reduce the amount of repeated data transferred.
Can improve performance for collection-heavy queries.
Makes it easier to inspect individual SQL commands in some cases.
Disadvantages
Requires additional database round trips.
Can increase response time when network latency is high.
May return data from different points in time without suitable transaction isolation.
May still generate expensive SQL depending on the query structure.
Summary
EF Core single queries and split queries provide two ways to load related data, but neither is the right choice for every situation.
A single query is often suitable for smaller object graphs and queries where reducing database round trips is important. A split query can be more efficient when multiple large collections cause excessive row duplication.
For production applications, start by inspecting the generated SQL and measuring actual performance. Consider projections, filtering, pagination, and transaction isolation before deciding which approach to use.
The best query strategy is the one that gives your application the right balance of performance, resource usage, and data consistency.
Join the conversation! Your thoughts help the community grow.