I have been profiling the Read action method on my MVC controller that is used with the Kendo Grid widget and have noticed some strange behavior with Entity Framework and it's queries. When I use ToDataSourceResult()
directly on an IQueryable
the EF context generates a query that selects 1 (i.e. does nothing) but includes all join clauses and filters. After that returns it sends the real query as expected with the filters and paging etc. applied.
Note: this is not breaking anything, I am just trying to optimize as I am using ServerOperation(true)
with AJAX in the grid which could result in a lot of calls to the Read action and a lot of these duplicate ADO.Net queries (also because the dummy call includes join clauses, with large datasets it may actually run slow).
Is there a reason why this is happening? Is there any way to still get server paging and filtering on the database (e.g. OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY
from results below instead of enumerating database with linq-to-objects first) without the extra call?
EntityFramework : v6.2
.Net Framework: v4.7.2
ASP.Net MVC 5
Kendo 2019.1.220
MS SQL database
(shortened) Code to get the duplicate call:
public ActionResult Read([DataSourceRequest]DataSourceRequest request)
{
using (var BP01DB = new BP01DBContext())
{
BP01DB.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
var records = (from customer in BP01DB.CUSTOMER_2007
join group2012 in BP01DB.GROUP_2012 on customer.GROUP_CUST_KEY equals group2012.GROUP_CALC_KEY_2012
join cobrCust in BP01DB.COBRCUSTJ_2006 on customer.CUST_CALC_KEY_2007 equals cobrCust.CUST_COBR_KEY
select new InquireCustBranchSalesRecord
{
CobrCustDbKey = cobrCust.COBRCUSTJ_2006_DBKEY,
});
return Json(records.ToDataSourceResult(request), JsonRequestBehavior.AllowGet);
}
}
Example output:
SELECT
[GroupBy1].[A1] AS [C1]
FROM ( SELECT
COUNT(1) AS [A1]
FROM [dbo].[CUSTOMER_2007] AS [Extent1]
INNER JOIN [dbo].[GROUP_2012] AS [Extent2] ON [Extent1].[GROUP_CUST_KEY] = [Extent2].[GROUP_CALC_KEY_2012]
INNER JOIN [dbo].[COBRCUSTJ_2006] AS [Extent3] ON [Extent1].[CUST_CALC_KEY_2007] = [Extent3].[CUST_COBR_KEY]
) AS [GroupBy1]
-- Executing at 6/11/2020 4:28:20 PM -05:00
-- Completed in 105 ms with result: SqlDataReader
Closed connection at 6/11/2020 4:28:20 PM -05:00
Opened connection at 6/11/2020 4:28:20 PM -05:00
SELECT
[Extent3].[COBRCUSTJ_2006_DBKEY] AS [COBRCUSTJ_2006_DBKEY]
FROM [dbo].[CUSTOMER_2007] AS [Extent1]
INNER JOIN [dbo].[GROUP_2012] AS [Extent2] ON [Extent1].[GROUP_CUST_KEY] = [Extent2][GROUP_CALC_KEY_2012]
INNER JOIN [dbo].[COBRCUSTJ_2006] AS [Extent3] ON [Extent1].[CUST_CALC_KEY_2007] = [Extent3]. [CUST_COBR_KEY]
ORDER BY row_number() OVER (ORDER BY [Extent3].[COBRCUSTJ_2006_DBKEY] ASC)
OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY
-- Executing at 6/11/2020 4:28:20 PM -05:00
-- Completed in 88 ms with result: SqlDataReader
Closed connection at 6/11/2020 4:28:21 PM -05:00
Now all I have to do to get rid of the first extraneous query is enumerate the IQueryable
before ToDataSourceResult()
does it (in this case by adding .ToList()
), which is fine for client paging filtering with small data sets, but not here.
var records = (from customer in BP01DB.CUSTOMER_2007
join group2012 in BP01DB.GROUP_2012 on customer.GROUP_CUST_KEY equals group2012.GROUP_CALC_KEY_2012
join cobrCust in BP01DB.COBRCUSTJ_2006 on customer.CUST_CALC_KEY_2007 equals cobrCust.CUST_COBR_KEY
select new InquireCustBranchSalesRecord
{
CobrCustDbKey = cobrCust.COBRCUSTJ_2006_DBKEY,
}).ToList();
And the output:
Opened connection at 6/11/2020 4:37:24 PM -05:00
SELECT
[Extent3].[COBRCUSTJ_2006_DBKEY] AS [COBRCUSTJ_2006_DBKEY]
FROM [dbo].[CUSTOMER_2007] AS [Extent1]
INNER JOIN [dbo].[GROUP_2012] AS [Extent2] ON [Extent1].[GROUP_CUST_KEY] = [Extent2].[GROUP_CALC_KEY_2012]
INNER JOIN [dbo].[COBRCUSTJ_2006] AS [Extent3] ON [Extent1].[CUST_CALC_KEY_2007] = [Extent3].[CUST_COBR_KEY]
-- Executing at 6/11/2020 4:37:25 PM -05:00
-- Completed in 129 ms with result: SqlDataReader
Closed connection at 6/11/2020 4:37:25 PM -05:00
This query
SELECT
[GroupBy1].[A1] AS [C1]
FROM ( SELECT
COUNT(1) AS [A1]
FROM [dbo].[CUSTOMER_2007] AS [Extent1]
INNER JOIN [dbo].[GROUP_2012] AS [Extent2] ON [Extent1].[GROUP_CUST_KEY] = [Extent2].[GROUP_CALC_KEY_2012]
INNER JOIN [dbo].[COBRCUSTJ_2006] AS [Extent3] ON [Extent1].[CUST_CALC_KEY_2007] = [Extent3].[CUST_COBR_KEY]
) AS [GroupBy1]
Counts the rows. Which apparently the UI widget does to display the total number of results along with the first page.