Skip to main content

How to Load Related Data in EF Core: Include vs Select

· 6 min read
Jagdish Kumawat
Founder @ Dewiride

A product belongs to a category, but a query for products doesn't load the category unless you ask. There are two ways to ask: Include and Select. They both produce a join and differ in how much they load. This guide runs each one on MySQL and reads the SQL, including what happens if you ask for neither.

Prerequisites​

If You Ask for Nothing​

A temporary endpoint
var products = await db.Products.OrderBy(p => p.Id).ToListAsync();
return Results.Ok(products.Select(p => new { p.Sku, Category = p.Category?.Name }));
Response body (the first two)
[
{ "sku": "CBL-USBC-2M", "category": null },
{ "sku": "CBL-USBC-1M", "category": null }
]
API log
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `p`.`Id`, `p`.`ArchivedAtUtc`, `p`.`CategoryId`, `p`.`CreatedAtUtc`, `p`.`Description`, `p`.`IsArchived`, `p`.`Name`, `p`.`QuantityOnHand`, `p`.`ReorderLevel`, `p`.`Sku`, `p`.`UnitPrice`, `p`.`UpdatedAtUtc`, `p`.`Version`
FROM `products` AS `p`
ORDER BY `p`.`Id`

No join, so Category is null, and it stays that way: EF Core doesn't load it later by itself, unless you turn on lazy loading.

With Include​

Include loads the related entity in the same query:

A temporary endpoint
var products = await db.Products.Include(p => p.Category).OrderBy(p => p.Id).ToListAsync();
return Results.Ok(products.Select(p => new { p.Sku, Category = p.Category.Name }));
API log
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `p`.`Id`, `p`.`ArchivedAtUtc`, `p`.`CategoryId`, `p`.`CreatedAtUtc`, `p`.`Description`, `p`.`IsArchived`, `p`.`Name`, `p`.`QuantityOnHand`, `p`.`ReorderLevel`, `p`.`Sku`, `p`.`UnitPrice`, `p`.`UpdatedAtUtc`, `p`.`Version`, `c`.`Id`, `c`.`CreatedAtUtc`, `c`.`Name`
FROM `products` AS `p`
INNER JOIN `categories` AS `c` ON `p`.`CategoryId` = `c`.`Id`
ORDER BY `p`.`Id`

There's the INNER JOIN. It's INNER because a product must have a category. The answer is right, but look at the columns: all of products, and all of categories, sixteen in total, to use two values.

With Select​

Use the navigation inside Select, with no Include:

A temporary endpoint
var products = await db.Products
.OrderBy(p => p.Id)
.Select(p => new { p.Sku, Category = p.Category.Name })
.ToListAsync();
API log
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `p`.`Sku`, `c`.`Name` AS `Category`
FROM `products` AS `p`
INNER JOIN `categories` AS `c` ON `p`.`CategoryId` = `c`.`Id`
ORDER BY `p`.`Id`

A terminal comparing three logs: no join without Include, an INNER JOIN with sixteen columns with Include, and an INNER JOIN with two columns with Select.

The same join, and two columns. EF Core saw p.Category.Name and added the join by itself.

Going the Other Way: Collections​

From a category to its products, Include uses a LEFT JOIN, because a category may have none:

A temporary endpoint
var categories = await db.Categories.Include(c => c.Products).OrderBy(c => c.Id).ToListAsync();
API log
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `c`.`Id`, `c`.`CreatedAtUtc`, `c`.`Name`, `p`.`Id`, `p`.`ArchivedAtUtc`, `p`.`CategoryId`, `p`.`CreatedAtUtc`, `p`.`Description`, `p`.`IsArchived`, `p`.`Name`, `p`.`QuantityOnHand`, `p`.`ReorderLevel`, `p`.`Sku`, `p`.`UnitPrice`, `p`.`UpdatedAtUtc`, `p`.`Version`
FROM `categories` AS `c`
LEFT JOIN `products` AS `p` ON `c`.`Id` = `p`.`CategoryId`
ORDER BY `c`.`Id`

Each product row repeats its category's columns, and EF Core sorts it out afterwards. If all you need is how many, Select does it with a subquery, and no product columns:

A temporary endpoint
var categories = await db.Categories
.OrderBy(c => c.Name)
.Select(c => new { c.Name, ProductCount = c.Products.Count })
.ToListAsync();
API log
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT `c`.`Name`, (
SELECT COUNT(*)
FROM `products` AS `p`
WHERE `c`.`Id` = `p`.`CategoryId`) AS `ProductCount`
FROM `categories` AS `c`
ORDER BY `c`.`Name`
Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

[
{
"name": "Adapters",
"productCount": 2
},
{
"name": "Cables",
"productCount": 4
},
{
"name": "Chargers",
"productCount": 2
}
]

Which to Use​

You wantUseWhy
Data to send to a clientSelectOnly the columns you need, and no JSON cycle
Entities you'll change and save, with their related dataIncludeThe entities are tracked, and the navigation is filled
Only a count or a totalSelectA subquery, with no extra rows

Common Mistakes​

MistakeWhat happensDo this instead
Reading a navigation without Include or SelectIt's null, or NullReferenceExceptionAsk for it in the query
Include on a query that ends in SelectEF Core ignores it, since Select decides what loadsLeave it out
Several collection Includes in one queryRows multiply, which EF Core calls a cartesian explosionUse AsSplitQuery, or Select
A query per row in a loopOne query for each categoryOne query with Select

FAQ​

What about lazy loading?​

EF Core can load navigations when you first touch them, with a separate package. Its efficient querying page warns it "makes it extremely easy to inadvertently trigger the N+1 problem", and recommends avoiding it.

Does Select track anything here?​

No, because the results aren't entities. See How to Use AsNoTracking.

Conclusion​

Related data doesn't load by itself. Include adds a join and loads the whole related entity, which suits data you'll change. Select adds the same join but reads only the columns you name, which suits everything you send to a client. To count related rows, Select with .Count uses a subquery.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.