繁体   English   中英

如何在 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 似乎生成了一个查询。

这是 Entity 框架生成的查询,当我在 SSMS 上运行时运行大约 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 查询,它在 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 优化此查询的最佳方法是什么?

那么,使用 EF-Core 优化此查询的最佳方法是什么?

自 2.1 以来(3.0、3.1、5.0 和现在正在使用 6.0)在 EF Core 查询管道中发生了许多变化,但可以使用一些通用规则,目的是摆脱客户端查询评估(从 3.0 开始是根本不支持,所以最好开始准备切换 - 对 2.1 的支持将于今年 8 月结束)。

首先是删除所有这些Include / ThenInclude 如果查询在 DTO 中投影结果而不涉及实体实例,那么所有这些都是多余的/不需要的,删除它们将确保查询完全转换为 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中使用范围变量useraha ,类似于 SQL 中的表别名。

此外,由于您的 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 非常相似。 并希望与它类似地执行得更快。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM