如何在 Entity Framework Core 2.1 中优化慢速(不太)的复杂查询
How can I optimize slow (not-so) complex queries in Entity Framework Core 2.1
我有一个 LINQ 查询,可以在几个表中进行字符串搜索。然而,查询在大表上非常慢。在我第一次尝试时,我超时了。我能够稍微提高性能。这是代码的第一个版本:
public ListResponse<UserDTO> GetUsers(FilterParameters filter)
{
var query = from user in _dbContext.Users
.Include(w => w.UserRoles).ThenInclude(u => u.Role)
join accountHolder in _dbContext.AccountHolders
.Include(c => c.OperationCountry)
.Include(x => x.Accounts)
.ThenInclude(x => x.Currency)
on user.Id equals accountHolder.ObjectId into aHolder
from a in aHolder.DefaultIfEmpty()
select new UserDTO
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
Username = user.UserName,
Email = user.Email,
Roles = Mapper.Map<IList<RoleDTO>>(user.UserRoles.Select(i => i.Role)),
LastActivity = user.LastActivity,
CreatedAt = user.CreatedAt,
EmailConfirmed = user.EmailConfirmed,
AccountBalance = a.Accounts.Where(p => p.CurrencyId == a.OperationCountry.LocalCurrencyId).Single().Balance,
AccountReference = a.Accounts.Where(p => p.CurrencyId == a.OperationCountry.LocalCurrencyId).Single().AccountRef
};
// Apply search term
if (!IsNullOrEmpty(filter.SearchTerm))
query = query.Where(w =>
w.FirstName.Contains(filter.SearchTerm)
w.LastName.Contains(filter.SearchTerm) ||
w.Email.Contains(filter.SearchTerm) ||
w.AccountReference.Contains(filter.SearchTerm));
if (filter.ColumnFilters != null)
{
if (filter.ColumnFilters.ContainsKey("EmailConfirmed"))
{
var valueStr = filter.ColumnFilters["EmailConfirmed"];
if (bool.TryParse(valueStr, out var value))
query = query.Where(x => x.EmailConfirmed == value);
}
}
// Get total item count before pagination
var totalItemCount = query.Count();
// Apply pagination
query = query.ApplySortAndPagination(filter);
var userDtoList = query.ToList();
return new ListResponse<UserDTO>()
{
List = userDtoList,
TotalCount = totalItemCount
};
}
我怀疑查询中的非数据库代码(例如 Single 和 Mapping)导致查询缓慢,因此我努力摆脱它们。我仍然很好奇如何在不在查询中调用 Single()
的情况下获得单个 Account
。这是修改后的版本。
public ListResponse<UserDTO> GetUsers(FilterParameters filter)
{
var query = from user in _dbContext.Users
.Include(w => w.UserRoles)
.ThenInclude(u => u.Role)
.Include(w => w.AccountHolder)
.ThenInclude(c => c.OperationCountry)
.Include(w => w.AccountHolder)
.ThenInclude(c => c.Accounts)
.ThenInclude(x => x.Currency)
select user;
if (!IsNullOrEmpty(filter.SearchTerm))
{
query = query.Where(w =>
w.FirstName.StartsWith(filter.SearchTerm) ||
w.LastName.StartsWith(filter.SearchTerm) ||
w.UserName.StartsWith(filter.SearchTerm) ||
w.AccountHolder.Accounts.Any(x => x.AccountRef.StartsWith(filter.SearchTerm)));
}
// total before pagination
var totalItemCount = query.Count();
// Nothing fancy, just OrderBy(filter.OrderBy).Skip(filter.Page).Take(filter.Length)
query = query.ApplySortAndPagination(filter);
userList = query.ToList() //To deal with "Single" calls below, this returns at most filter.Length records
var userDtoResult = (from user in query
select new UserDTO
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
Username = user.UserName,
Email = user.Email,
Roles = Mapper.Map<IList<RoleDTO>>(user.UserRoles.Select(i => i.Role)),
LastActivity = user.LastActivity,
CreatedAt = user.CreatedAt,
EmailConfirmed = user.EmailConfirmed,
AccountBalance = user.AccountHolder.Accounts.Single(p => p.CurrencyId == user.AccountHolder.OperationCountry.LocalCurrencyId).Balance
AccountReference = user.AccountHolder.Accounts.Single(p => p.CurrencyId == user.AccountHolder.OperationCountry.LocalCurrencyId).AccountRef
}).ToList();
return new ListResponse<UserDTO>()
{
List = userDtoResult,
TotalCount = totalItemCount
};
}
此查询 运行 生成的 SQL 查询也很慢,而如果我在 SQL 中编写连接查询,它会在几百毫秒内完成。我怀疑我遇到了 N+1 查询问题,但不确定,因为当我在 SQL Server Profiler 中进行跟踪时,EF 似乎生成了一个查询。
这是我在 SSMS 上 运行 时 Entity framework 和 运行 在大约 8 秒内生成的查询:
exec sp_executesql N'SELECT TOP(@__p_4) [w].[Id], [w].[AccessFailedCount], [w].[ConcurrencyStamp], [w].[CreatedAt], [w].[CreatedBy], [w].[DeletedAt], [w].[DeletedBy], [w].[DetailId], [w].[Email], [w].[EmailConfirmed], [w].[EmailConfirmedAt], [w].[FacebookId], [w].[FirstName], [w].[GoogleId], [w].[IsActive], [w].[IsDeleted], [w].[LastActivity], [w].[LastName], [w].[LockoutEnabled], [w].[LockoutEnd], [w].[NormalizedEmail], [w].[NormalizedUserName], [w].[Password], [w].[PasswordHash], [w].[PhoneNumber], [w].[PhoneNumberConfirmed], [w].[RoleId], [w].[SecurityStamp], [w].[TwoFactorEnabled], [w].[UpdatedAt], [w].[UpdatedBy], [w].[UserName], [w].[WorkflowId], [t].[Id], [t].[AccountHolderLevel], [t].[AccountHolderType], [t].[CreatedAt], [t].[CreatedBy], [t].[DeletedAt], [t].[DeletedBy], [t].[IsDeleted], [t].[ObjectId], [t].[OperationCountryId], [t].[UpdatedAt], [t].[UpdatedBy], [t0].[Id], [t0].[ContinentId], [t0].[CountryCode], [t0].[CreatedAt], [t0].[CreatedBy], [t0].[DeletedAt], [t0].[DeletedBy], [t0].[ISOCode2], [t0].[IsActive], [t0].[IsDeleted], [t0].[IsOperational], [t0].[LocalCurrencyId], [t0].[Name], [t0].[PhoneCode], [t0].[PostCodeProvider], [t0].[Regex], [t0].[SmsProvider], [t0].[UpdatedAt], [t0].[UpdatedBy]
FROM [Users] AS [w]
LEFT JOIN (
SELECT [a].[Id], [a].[AccountHolderLevel], [a].[AccountHolderType], [a].[CreatedAt], [a].[CreatedBy], [a].[DeletedAt], [a].[DeletedBy], [a].[IsDeleted], [a].[ObjectId], [a].[OperationCountryId], [a].[UpdatedAt], [a].[UpdatedBy]
FROM [AccountHolders] AS [a]
WHERE [a].[IsDeleted] = 0
) AS [t] ON [w].[Id] = [t].[ObjectId]
LEFT JOIN (
SELECT [c].[Id], [c].[ContinentId], [c].[CountryCode], [c].[CreatedAt], [c].[CreatedBy], [c].[DeletedAt], [c].[DeletedBy], [c].[ISOCode2], [c].[IsActive], [c].[IsDeleted], [c].[IsOperational], [c].[LocalCurrencyId], [c].[Name], [c].[PhoneCode], [c].[PostCodeProvider], [c].[Regex], [c].[SmsProvider], [c].[UpdatedAt], [c].[UpdatedBy]
FROM [Countries] AS [c]
WHERE [c].[IsDeleted] = 0
) AS [t0] ON [t].[OperationCountryId] = [t0].[Id]
WHERE ([w].[IsDeleted] = 0) AND ((((([w].[FirstName] LIKE @__filter_SearchTerm_0 + N''%'' AND (LEFT([w].[FirstName], LEN(@__filter_SearchTerm_0)) = @__filter_SearchTerm_0)) OR (@__filter_SearchTerm_0 = N'''')) OR (([w].[LastName] LIKE @__filter_SearchTerm_1 + N''%'' AND (LEFT([w].[LastName], LEN(@__filter_SearchTerm_1)) = @__filter_SearchTerm_1)) OR (@__filter_SearchTerm_1 = N''''))) OR (([w].[UserName] LIKE @__filter_SearchTerm_2 + N''%'' AND (LEFT([w].[UserName], LEN(@__filter_SearchTerm_2)) = @__filter_SearchTerm_2)) OR (@__filter_SearchTerm_2 = N''''))) OR EXISTS (
SELECT 1
FROM [Accounts] AS [x]
WHERE (([x].[IsDeleted] = 0) AND (([x].[AccountRef] LIKE @__filter_SearchTerm_3 + N''%'' AND (LEFT([x].[AccountRef], LEN(@__filter_SearchTerm_3)) = @__filter_SearchTerm_3)) OR (@__filter_SearchTerm_3 = N''''))) AND ([t].[Id] = [x].[AccountHolderId])))
ORDER BY [w].[LastActivity] DESC, [w].[Id], [t].[Id]',N'@__p_4 int,@__filter_SearchTerm_0 nvarchar(100),@__filter_SearchTerm_1 nvarchar(100),@__filter_SearchTerm_2 nvarchar(256),@__filter_SearchTerm_3 nvarchar(450)',@__p_4=10,@__filter_SearchTerm_0=N'james',@__filter_SearchTerm_1=N'james',@__filter_SearchTerm_2=N'james',@__filter_SearchTerm_3=N'james'
最后,这是我的 SQL 查询 returns 在不到 100 毫秒内需要什么:
declare @searchTerm varchar(100) = '%james%'
select top 10
u.Id,
u.UserName,
u.FirstName,
u.LastName,
u.LastActivity,
u.CreatedAt,
a.Balance,
a.AccountRef,
ah.AccountHolderLevel,
u.Email,
r.Name
from Users u
join AccountHolders ah on ah.ObjectId = u.Id
join Accounts a on ah.Id = a.AccountHolderId
join UserRoles ur on ur.UserId = u.Id
join Roles r on r.Id = ur.RoleId
where FirstName like @searchTerm or LastName like @searchTerm or u.UserName like @searchTerm or FirstName + ' ' + LastName like @searchTerm or a.AccountRef like @searchTerm
and a.CurrencyId = ah.OperationCountryId
顺便说一句,我正在搜索的列都已建立索引,所以这不是问题。我知道新的 EF-Core 有很多性能改进。不幸的是,由于大量的重大更改,我无法更新。
我不确定将查询拆分为 2 个(一个用于用户,一个用于帐户)是否有效,因为会再次进行联接。如果我找不到解决方案,我计划将我的查询转换为视图,但我想作为最后的手段这样做,因为我们的惯例是尽可能多地使用 EF。而且我拒绝相信 EF 没有解决方案。这实际上根本不是一个复杂的查询,我确信这是一个相当常见的用例。
那么,使用 EF-Core 优化此查询的最佳方法是什么?
So, what is the best way to optimize this query using EF-Core?
自 2.1(3.0、3.1、5.0,现在在 6.0 上工作)以来,EF Core 查询管道发生了很多变化,但可以使用一些通用规则,目标是摆脱客户端查询评估 (完全不支持从 3.0 开始的版本,因此最好开始准备转换 - 对 2.1 的支持将于今年 8 月结束。
首先是删除所有这些 Include
/ ThenInclude
。如果查询在不涉及实体实例的情况下在 DTO 中投影结果,那么所有这些都是 redundant/not 需要的,删除它们将确保查询完全转换为 SQL.
var query = _dbContext.Users.AsQueryable();
// Apply filters...
接下来是Roles
合集。您必须删除 Mapper.Map
调用,否则无法翻译。通常要么使用 AutoMapper 映射和 ProjectTo
来完全处理投影,要么根本不使用它(永远不要将 Map
方法调用放在查询表达式树中)。根据你的SQL,应该是这样的
Roles = user.UserRoles.Select(ur => ur.Role)
.Select(r => new RoleDTO { Name = r.Name })
.ToList(),
实际上 EF Core 会将此作为单独的查询执行(这种行为在 3.x 中被“单一查询模式”打破,并可选择使用 6.0“拆分查询模式”恢复),因此重要的是最后调用 ToList()
,否则你会得到 N + 1 个查询而不是 2 个。
最后,Single()
调用。可以通过使用相关 SelectMany
或其查询语法等价物
展平子集合来避免这种情况
from user in query
let ah = user.AccountHolder
from a in ah.Accounts
where a.CurrencyId == ah.OperationCountryId
let
语句不是强制性的,我添加它只是为了便于阅读。现在您可以在最终 select
中使用范围变量 user
、ah
和 a
,类似于 SQL.[=33 中的 table 别名=]
此外,由于您的 SQL 查询并未真正强制执行 单个 帐户匹配,因此 LINQ 查询中也没有此类强制执行。如果需要,则可以使用 SelectMany
+ Where
+ `Take(1) 实现 Single
的等价物,例如
from user in query
let ah = user.AccountHolder
from a in ah.Accounts
.Where(a => a.CurrencyId == ah.OperationCountryId)
.Take(1)
(查询和方法语法的混合,但 LINQ 允许)
所以最终的查询应该是这样的
from user in query
let ah = user.AccountHolder
from a in ah.Accounts
where a.CurrencyId == ah.OperationCountryId
select new //UserDTO
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
Username = user.UserName,
Email = user.Email,
Roles = user.UserRoles.Select(ur => ur.Role)
.Select(r => new RoleDTO { Name = r.Name })
.ToList(),
LastActivity = user.LastActivity,
CreatedAt = user.CreatedAt,
EmailConfirmed = user.EmailConfirmed,
AccountBalance = a.Balance,
AccountReference = a.AccountRef
}
并且应该翻译成与手工制作的非常相似 SQL。并希望执行得更快。
我有一个 LINQ 查询,可以在几个表中进行字符串搜索。然而,查询在大表上非常慢。在我第一次尝试时,我超时了。我能够稍微提高性能。这是代码的第一个版本:
public ListResponse<UserDTO> GetUsers(FilterParameters filter)
{
var query = from user in _dbContext.Users
.Include(w => w.UserRoles).ThenInclude(u => u.Role)
join accountHolder in _dbContext.AccountHolders
.Include(c => c.OperationCountry)
.Include(x => x.Accounts)
.ThenInclude(x => x.Currency)
on user.Id equals accountHolder.ObjectId into aHolder
from a in aHolder.DefaultIfEmpty()
select new UserDTO
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
Username = user.UserName,
Email = user.Email,
Roles = Mapper.Map<IList<RoleDTO>>(user.UserRoles.Select(i => i.Role)),
LastActivity = user.LastActivity,
CreatedAt = user.CreatedAt,
EmailConfirmed = user.EmailConfirmed,
AccountBalance = a.Accounts.Where(p => p.CurrencyId == a.OperationCountry.LocalCurrencyId).Single().Balance,
AccountReference = a.Accounts.Where(p => p.CurrencyId == a.OperationCountry.LocalCurrencyId).Single().AccountRef
};
// Apply search term
if (!IsNullOrEmpty(filter.SearchTerm))
query = query.Where(w =>
w.FirstName.Contains(filter.SearchTerm)
w.LastName.Contains(filter.SearchTerm) ||
w.Email.Contains(filter.SearchTerm) ||
w.AccountReference.Contains(filter.SearchTerm));
if (filter.ColumnFilters != null)
{
if (filter.ColumnFilters.ContainsKey("EmailConfirmed"))
{
var valueStr = filter.ColumnFilters["EmailConfirmed"];
if (bool.TryParse(valueStr, out var value))
query = query.Where(x => x.EmailConfirmed == value);
}
}
// Get total item count before pagination
var totalItemCount = query.Count();
// Apply pagination
query = query.ApplySortAndPagination(filter);
var userDtoList = query.ToList();
return new ListResponse<UserDTO>()
{
List = userDtoList,
TotalCount = totalItemCount
};
}
我怀疑查询中的非数据库代码(例如 Single 和 Mapping)导致查询缓慢,因此我努力摆脱它们。我仍然很好奇如何在不在查询中调用 Single()
的情况下获得单个 Account
。这是修改后的版本。
public ListResponse<UserDTO> GetUsers(FilterParameters filter)
{
var query = from user in _dbContext.Users
.Include(w => w.UserRoles)
.ThenInclude(u => u.Role)
.Include(w => w.AccountHolder)
.ThenInclude(c => c.OperationCountry)
.Include(w => w.AccountHolder)
.ThenInclude(c => c.Accounts)
.ThenInclude(x => x.Currency)
select user;
if (!IsNullOrEmpty(filter.SearchTerm))
{
query = query.Where(w =>
w.FirstName.StartsWith(filter.SearchTerm) ||
w.LastName.StartsWith(filter.SearchTerm) ||
w.UserName.StartsWith(filter.SearchTerm) ||
w.AccountHolder.Accounts.Any(x => x.AccountRef.StartsWith(filter.SearchTerm)));
}
// total before pagination
var totalItemCount = query.Count();
// Nothing fancy, just OrderBy(filter.OrderBy).Skip(filter.Page).Take(filter.Length)
query = query.ApplySortAndPagination(filter);
userList = query.ToList() //To deal with "Single" calls below, this returns at most filter.Length records
var userDtoResult = (from user in query
select new UserDTO
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
Username = user.UserName,
Email = user.Email,
Roles = Mapper.Map<IList<RoleDTO>>(user.UserRoles.Select(i => i.Role)),
LastActivity = user.LastActivity,
CreatedAt = user.CreatedAt,
EmailConfirmed = user.EmailConfirmed,
AccountBalance = user.AccountHolder.Accounts.Single(p => p.CurrencyId == user.AccountHolder.OperationCountry.LocalCurrencyId).Balance
AccountReference = user.AccountHolder.Accounts.Single(p => p.CurrencyId == user.AccountHolder.OperationCountry.LocalCurrencyId).AccountRef
}).ToList();
return new ListResponse<UserDTO>()
{
List = userDtoResult,
TotalCount = totalItemCount
};
}
此查询 运行 生成的 SQL 查询也很慢,而如果我在 SQL 中编写连接查询,它会在几百毫秒内完成。我怀疑我遇到了 N+1 查询问题,但不确定,因为当我在 SQL Server Profiler 中进行跟踪时,EF 似乎生成了一个查询。
这是我在 SSMS 上 运行 时 Entity framework 和 运行 在大约 8 秒内生成的查询:
exec sp_executesql N'SELECT TOP(@__p_4) [w].[Id], [w].[AccessFailedCount], [w].[ConcurrencyStamp], [w].[CreatedAt], [w].[CreatedBy], [w].[DeletedAt], [w].[DeletedBy], [w].[DetailId], [w].[Email], [w].[EmailConfirmed], [w].[EmailConfirmedAt], [w].[FacebookId], [w].[FirstName], [w].[GoogleId], [w].[IsActive], [w].[IsDeleted], [w].[LastActivity], [w].[LastName], [w].[LockoutEnabled], [w].[LockoutEnd], [w].[NormalizedEmail], [w].[NormalizedUserName], [w].[Password], [w].[PasswordHash], [w].[PhoneNumber], [w].[PhoneNumberConfirmed], [w].[RoleId], [w].[SecurityStamp], [w].[TwoFactorEnabled], [w].[UpdatedAt], [w].[UpdatedBy], [w].[UserName], [w].[WorkflowId], [t].[Id], [t].[AccountHolderLevel], [t].[AccountHolderType], [t].[CreatedAt], [t].[CreatedBy], [t].[DeletedAt], [t].[DeletedBy], [t].[IsDeleted], [t].[ObjectId], [t].[OperationCountryId], [t].[UpdatedAt], [t].[UpdatedBy], [t0].[Id], [t0].[ContinentId], [t0].[CountryCode], [t0].[CreatedAt], [t0].[CreatedBy], [t0].[DeletedAt], [t0].[DeletedBy], [t0].[ISOCode2], [t0].[IsActive], [t0].[IsDeleted], [t0].[IsOperational], [t0].[LocalCurrencyId], [t0].[Name], [t0].[PhoneCode], [t0].[PostCodeProvider], [t0].[Regex], [t0].[SmsProvider], [t0].[UpdatedAt], [t0].[UpdatedBy]
FROM [Users] AS [w]
LEFT JOIN (
SELECT [a].[Id], [a].[AccountHolderLevel], [a].[AccountHolderType], [a].[CreatedAt], [a].[CreatedBy], [a].[DeletedAt], [a].[DeletedBy], [a].[IsDeleted], [a].[ObjectId], [a].[OperationCountryId], [a].[UpdatedAt], [a].[UpdatedBy]
FROM [AccountHolders] AS [a]
WHERE [a].[IsDeleted] = 0
) AS [t] ON [w].[Id] = [t].[ObjectId]
LEFT JOIN (
SELECT [c].[Id], [c].[ContinentId], [c].[CountryCode], [c].[CreatedAt], [c].[CreatedBy], [c].[DeletedAt], [c].[DeletedBy], [c].[ISOCode2], [c].[IsActive], [c].[IsDeleted], [c].[IsOperational], [c].[LocalCurrencyId], [c].[Name], [c].[PhoneCode], [c].[PostCodeProvider], [c].[Regex], [c].[SmsProvider], [c].[UpdatedAt], [c].[UpdatedBy]
FROM [Countries] AS [c]
WHERE [c].[IsDeleted] = 0
) AS [t0] ON [t].[OperationCountryId] = [t0].[Id]
WHERE ([w].[IsDeleted] = 0) AND ((((([w].[FirstName] LIKE @__filter_SearchTerm_0 + N''%'' AND (LEFT([w].[FirstName], LEN(@__filter_SearchTerm_0)) = @__filter_SearchTerm_0)) OR (@__filter_SearchTerm_0 = N'''')) OR (([w].[LastName] LIKE @__filter_SearchTerm_1 + N''%'' AND (LEFT([w].[LastName], LEN(@__filter_SearchTerm_1)) = @__filter_SearchTerm_1)) OR (@__filter_SearchTerm_1 = N''''))) OR (([w].[UserName] LIKE @__filter_SearchTerm_2 + N''%'' AND (LEFT([w].[UserName], LEN(@__filter_SearchTerm_2)) = @__filter_SearchTerm_2)) OR (@__filter_SearchTerm_2 = N''''))) OR EXISTS (
SELECT 1
FROM [Accounts] AS [x]
WHERE (([x].[IsDeleted] = 0) AND (([x].[AccountRef] LIKE @__filter_SearchTerm_3 + N''%'' AND (LEFT([x].[AccountRef], LEN(@__filter_SearchTerm_3)) = @__filter_SearchTerm_3)) OR (@__filter_SearchTerm_3 = N''''))) AND ([t].[Id] = [x].[AccountHolderId])))
ORDER BY [w].[LastActivity] DESC, [w].[Id], [t].[Id]',N'@__p_4 int,@__filter_SearchTerm_0 nvarchar(100),@__filter_SearchTerm_1 nvarchar(100),@__filter_SearchTerm_2 nvarchar(256),@__filter_SearchTerm_3 nvarchar(450)',@__p_4=10,@__filter_SearchTerm_0=N'james',@__filter_SearchTerm_1=N'james',@__filter_SearchTerm_2=N'james',@__filter_SearchTerm_3=N'james'
最后,这是我的 SQL 查询 returns 在不到 100 毫秒内需要什么:
declare @searchTerm varchar(100) = '%james%'
select top 10
u.Id,
u.UserName,
u.FirstName,
u.LastName,
u.LastActivity,
u.CreatedAt,
a.Balance,
a.AccountRef,
ah.AccountHolderLevel,
u.Email,
r.Name
from Users u
join AccountHolders ah on ah.ObjectId = u.Id
join Accounts a on ah.Id = a.AccountHolderId
join UserRoles ur on ur.UserId = u.Id
join Roles r on r.Id = ur.RoleId
where FirstName like @searchTerm or LastName like @searchTerm or u.UserName like @searchTerm or FirstName + ' ' + LastName like @searchTerm or a.AccountRef like @searchTerm
and a.CurrencyId = ah.OperationCountryId
顺便说一句,我正在搜索的列都已建立索引,所以这不是问题。我知道新的 EF-Core 有很多性能改进。不幸的是,由于大量的重大更改,我无法更新。
我不确定将查询拆分为 2 个(一个用于用户,一个用于帐户)是否有效,因为会再次进行联接。如果我找不到解决方案,我计划将我的查询转换为视图,但我想作为最后的手段这样做,因为我们的惯例是尽可能多地使用 EF。而且我拒绝相信 EF 没有解决方案。这实际上根本不是一个复杂的查询,我确信这是一个相当常见的用例。
那么,使用 EF-Core 优化此查询的最佳方法是什么?
So, what is the best way to optimize this query using EF-Core?
自 2.1(3.0、3.1、5.0,现在在 6.0 上工作)以来,EF Core 查询管道发生了很多变化,但可以使用一些通用规则,目标是摆脱客户端查询评估 (完全不支持从 3.0 开始的版本,因此最好开始准备转换 - 对 2.1 的支持将于今年 8 月结束。
首先是删除所有这些 Include
/ ThenInclude
。如果查询在不涉及实体实例的情况下在 DTO 中投影结果,那么所有这些都是 redundant/not 需要的,删除它们将确保查询完全转换为 SQL.
var query = _dbContext.Users.AsQueryable();
// Apply filters...
接下来是Roles
合集。您必须删除 Mapper.Map
调用,否则无法翻译。通常要么使用 AutoMapper 映射和 ProjectTo
来完全处理投影,要么根本不使用它(永远不要将 Map
方法调用放在查询表达式树中)。根据你的SQL,应该是这样的
Roles = user.UserRoles.Select(ur => ur.Role)
.Select(r => new RoleDTO { Name = r.Name })
.ToList(),
实际上 EF Core 会将此作为单独的查询执行(这种行为在 3.x 中被“单一查询模式”打破,并可选择使用 6.0“拆分查询模式”恢复),因此重要的是最后调用 ToList()
,否则你会得到 N + 1 个查询而不是 2 个。
最后,Single()
调用。可以通过使用相关 SelectMany
或其查询语法等价物
from user in query
let ah = user.AccountHolder
from a in ah.Accounts
where a.CurrencyId == ah.OperationCountryId
let
语句不是强制性的,我添加它只是为了便于阅读。现在您可以在最终 select
中使用范围变量 user
、ah
和 a
,类似于 SQL.[=33 中的 table 别名=]
此外,由于您的 SQL 查询并未真正强制执行 单个 帐户匹配,因此 LINQ 查询中也没有此类强制执行。如果需要,则可以使用 SelectMany
+ Where
+ `Take(1) 实现 Single
的等价物,例如
from user in query
let ah = user.AccountHolder
from a in ah.Accounts
.Where(a => a.CurrencyId == ah.OperationCountryId)
.Take(1)
(查询和方法语法的混合,但 LINQ 允许)
所以最终的查询应该是这样的
from user in query
let ah = user.AccountHolder
from a in ah.Accounts
where a.CurrencyId == ah.OperationCountryId
select new //UserDTO
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
Username = user.UserName,
Email = user.Email,
Roles = user.UserRoles.Select(ur => ur.Role)
.Select(r => new RoleDTO { Name = r.Name })
.ToList(),
LastActivity = user.LastActivity,
CreatedAt = user.CreatedAt,
EmailConfirmed = user.EmailConfirmed,
AccountBalance = a.Balance,
AccountReference = a.AccountRef
}
并且应该翻译成与手工制作的非常相似 SQL。并希望执行得更快。