Entity Framework lazy loading vs explicit loading and the generated SQL
Download the source code here.
If you are using lazy loading with Entity Framework you should check that queries to the database are working as expected.
A lazy load call to something like customer.Orders.FirstOrDefault() will give you a single order, but if you look at the SQL executed you’ll see that all orders for the customer are returned from the database; it is your code that is taking first one from memory. Depending on your application, this may have significant performance implications especially as the amount of data grows.
However, using explicit loading I am able to return just the one order I need.
In this post I’ll show how to use lazy loading and explicit loading, and compare the generated SQL for each.
The CustomerController
contains a lazy and explicit Index action method and the OrderController contains an lazy and explicit Details action method.
To view the SQL generated for each query I used glimpse; turn it on by going to http://localhost:63817/glimpse.axd and clicking the “On” button.
Lazy Loading
By default lazy loading is enabled in EF. To take advantage of it all you do is mark the related entity as virtual, EF will do the rest.
1public class Customer
2{
3 public int CustomerID { get; set; }
4 public string Firstname { get; set; }
5 public string Lastname { get; set; }
6 public virtual ICollection<Order> Orders { get; set; }
7}
When you call customer.Orders
EF overrides the Orders virtual method and makes the appropriate call to the database to load all orders for the that customer. The problem is that all orders are loaded even if you only want to the first order.
Open the attached solution and take a look a the Customer IndexLazy
action method and its associated view, it shows just the first order for each customer.
The IndexLazy
action method loads the customers from the database, but does not load the orders. It is in the view that the orders are lazily loaded when generating the action links to the order details page -
1item.Orders.FirstOrDefault().OrderID
Examination of the executed SQL shows that all orders for that customer were returned from the database even though only the first was used.
Explicit Loading
A more efficient approach is shown in the IndexExplicit action method. It disables lazy loading by calling -
1db.Configuration.LazyLoadingEnabled = false;
The action method then loads the customers from the database as above. Then the first order for each customer is loaded into the Orders property of the Customer.
1 foreach (var customer in customers)
2 {
3 db.Entry(customer).Collection(c => c.Orders).Query().Take(1).Load();
4 }
In this way only one order record for each customer is returned from the database.
The Data
The data model is very simple, an orders table, a customer table and an order items table. Zero or more order items are associated with an order, and zero or more orders are associated with a customer.
In the provided data, there are two customers, each with four orders made up of varying numbers of order items; take a look at the global.asax.cs
to see exactly - note that in this code the way I initialize and seed the database are NOT what you should do for a serious piece of code.
Scroll to the bottom of the page to see the actual data in the tables.
The Execution
Customer page
Firstly go to the customer index which is lazily loaded - http://localhost:63817/Customer/IndexLazy
You’ll see two rows on the page representing the two customers and one order from each. Here is the sql that was executed using lazy loading.
1SELECT
2[Extent1].[CustomerID] AS [CustomerID],
3[Extent1].[Firstname] AS [Firstname],
4[Extent1].[Lastname] AS [Lastname]
5FROM [dbo].[Customers] AS [Extent1]
Two customers returned
1SELECT
2[Extent1].[OrderID] AS [OrderID],
3[Extent1].[OrderDate] AS [OrderDate],
4[Extent1].[Comment] AS [Comment],
5[Extent1].[CustomerID] AS [CustomerID]
6FROM [dbo].[Orders] AS [Extent1]
7WHERE [Extent1].[CustomerID] = 1 /* @EntityKeyValue1 * /
All four orders for customer 1 are returned from the database even though only one is needed
1SELECT
2[Extent1].[OrderID] AS [OrderID],
3[Extent1].[OrderDate] AS [OrderDate],
4[Extent1].[Comment] AS [Comment],
5[Extent1].[CustomerID] AS [CustomerID]
6FROM [dbo].[Orders] AS [Extent1]
7WHERE [Extent1].[CustomerID] = 2 /* @EntityKeyValue1 * /
All four orders for customer 2 are returned from the database even though only one is needed
Now go to localhost:63817/Customer/IndexExplicit and see the improvements of explicit loading.
1SELECT
2[Extent1].[CustomerID] AS [CustomerID],
3[Extent1].[Firstname] AS [Firstname],
4[Extent1].[Lastname] AS [Lastname]
5FROM [dbo].[Customers] AS [Extent1]
Two customers returned
1SELECT TOP (1)
2[Extent1].[OrderID] AS [OrderID],
3[Extent1].[OrderDate] AS [OrderDate],
4[Extent1].[Comment] AS [Comment],
5[Extent1].[CustomerID] AS [CustomerID]
6FROM [dbo].[Orders] AS [Extent1]
7WHERE [Extent1].[CustomerID] = 1 /* @EntityKeyValue1 * /
One order returned for customer 1, this is what we wanted, note the use of TOP (1)
ensuring that just one order is retrieved. Compare that to the SQL called for the lazy load.
1SELECT TOP (1)
2[Extent1].[OrderID] AS [OrderID],
3[Extent1].[OrderDate] AS [OrderDate],
4[Extent1].[Comment] AS [Comment],
5[Extent1].[CustomerID] AS [CustomerID]
6FROM [dbo].[Orders] AS [Extent1]
7WHERE [Extent1].[CustomerID] = 2 /* @EntityKeyValue1 * /
One order returned for customer 2, again here we are getting just the one record we wanted.
Summary of lazy loading vs explicit loading for index page
Task | Lazy Loaded Rows | Explicit Loaded Rows |
---|---|---|
Retrieve customers | 2 | 2 |
Retrieve Steve’s orders | 4 | 1 |
Retrieve James’ orders | 4 | 1 |
When using EF it is not enough to be aware of the reasons for using iqueryable vs ienumerable, you should look at the usage scenario of the entities you are loading from the database, especially those that are lazily loaded.
Order details page
From the customer index view, navigate to the order details view for both of the customers, it shows the customer, his first order and the first order item on that first order. The order details view can be loaded from two action methods, one uses lazy loading and the other explicit.
Below are the SQL calls using lazy loading.
1SELECT TOP (2)
2[Extent1].[CustomerID] AS [CustomerID],
3[Extent1].[Firstname] AS [Firstname],
4[Extent1].[Lastname] AS [Lastname]
5FROM [dbo].[Customers] AS [Extent1]
6WHERE [Extent1].[CustomerID] = 1 /* @p0 * /
returns one customer record
1SELECT
2[Extent1].[OrderID] AS [OrderID],
3[Extent1].[OrderDate] AS [OrderDate],
4[Extent1].[Comment] AS [Comment],
5[Extent1].[CustomerID] AS [CustomerID]
6FROM [dbo].[Orders] AS [Extent1]
7WHERE [Extent1].[CustomerID] = 1 /* @EntityKeyValue1 * /
returns four order records even though only one is needed
1SELECT
2[Extent1].[OrderItemID] AS [OrderItemID],
3[Extent1].[ItemDescription] AS [ItemDescription],
4[Extent1].[Price] AS [Price],
5[Extent1].[OrderID] AS [OrderID]
6FROM [dbo].[OrderItems] AS [Extent1]
7WHERE [Extent1].[OrderID] = 1 /* @EntityKeyValue1 * /
returns six order item records even though only one is needed
Here are the SQL calls explicit loading.
1SELECT TOP (2)
2[Extent1].[CustomerID] AS [CustomerID],
3[Extent1].[Firstname] AS [Firstname],
4[Extent1].[Lastname] AS [Lastname]
5FROM [dbo].[Customers] AS [Extent1]
6WHERE [Extent1].[CustomerID] = 2 /* @p0 * /
returns just one customer
1SELECT TOP (1)
2[Extent1].[OrderID] AS [OrderID],
3[Extent1].[OrderDate] AS [OrderDate],
4[Extent1].[Comment] AS [Comment],
5[Extent1].[CustomerID] AS [CustomerID]
6FROM [dbo].[Orders] AS [Extent1]
7WHERE [Extent1].[CustomerID] = 2 /* @EntityKeyValue1 * /
returns just one order
1SELECT TOP (1)
2[Extent1].[OrderItemID] AS [OrderItemID],
3[Extent1].[ItemDescription] AS [ItemDescription],
4[Extent1].[Price] AS [Price],
5[Extent1].[OrderID] AS [OrderID]
6FROM [dbo].[OrderItems] AS [Extent1]
7WHERE [Extent1].[OrderID] = 5 /* @EntityKeyValue1 * /
returns just one order item
Summary of lazy loading vs explicit loading for order details page
Task | Lazy Loaded Rows | Explicit Loaded Rows |
---|---|---|
Retrieve customers | 1 | 1 |
Retrieve orders | 4 | 1 |
Retrieve order items | 6 | 1 |
Full Data
Customers table
CustomerID | Firstname | Lastname |
---|---|---|
1 | Lazy Steve | Smith |
2 | James | Jones |
Retrieve order items | 6 | 1 |
Orders table
OrderID | OrderDate | Comment | CustomerID |
---|---|---|---|
1 | 8/6/2014 12:00:00 AM | Steve’s first order | 1 |
2 | 8/7/2014 12:00:00 AM | Steve’s second order | 1 |
3 | 8/8/2014 12:00:00 AM | Steve’s third order | 1 |
4 | 8/9/2014 12:00:00 AM | Steve’s fourth order | 1 |
5 | 8/7/2014 12:00:00 AM | James’ first order | 2 |
6 | 8/8/2014 12:00:00 AM | James’ second order | 2 |
7 | 8/9/2014 12:00:00 AM | James’ third order | 2 |
8 | 8/10/2014 12:00:00 AM | James’ fourth order | 2 |
Order Item table
OrderItemID | ItemDescription | Price | OrderID |
---|---|---|---|
1 | Ball | 3 | 1 |
2 | Watch | 32 | 1 |
3 | Book | 12 | 1 |
4 | Glasses | 41 | 1 |
5 | Pen | 4 | 1 |
6 | Chair | 23 | 1 |
7 | Laptop | 400 | 2 |
8 | Mouse | 13 | 2 |
9 | Bottle | 1 | 2 |
10 | Stapler | 7 | 2 |
11 | Wires | 2.5 | 2 |
12 | Hose | 41 | 3 |
13 | Tiles | 22 | 4 |
14 | Table | 49 | 5 |
15 | Spoons | 1.99 | 5 |
16 | Forks | 2.99 | 5 |
17 | Cups | 5 | 5 |
18 | Plates | 14 | 5 |
19 | Knives | 8 | 5 |
20 | Bike | 340 | 6 |
21 | Reflectors | 9 | 6 |
22 | Tool | 10 | 6 |
23 | Tube repair | 4.99 | 6 |
24 | Lights | 11 | 6 |
25 | Helmet | 27 | 6 |
26 | Mouse | 14 | 7 |
27 | Keyboard | 29 | 7 |
28 | Mouse | 14 | 8 |
29 | Keyboard | 29 | 8 |
Download the source code here.