简体   繁体   English

如何在 Entity Framework Core 2.1 中优化缓慢(不那么)复杂的查询

[英]How can I optimize slow (not-so) complex queries in Entity Framework Core 2.1

I have a LINQ query that makes string search within a few tables.我有一个 LINQ 查询,可以在几个表中进行字符串搜索。 The query however is painfully slow on big tables.然而,在大表上查询速度非常慢。 At my first attempt, I was getting a timeout.在我第一次尝试时,我遇到了超时。 I was able to improve the performance a little.我能够稍微提高性能。 This is the first version of the code:这是代码的第一个版本:

 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
        };
 }

I suspected non-database code in the query (such as Single, and Mapping) was causing a slow query so I made an effort to get rid of them.我怀疑查询中的非数据库代码(例如 Single 和 Mapping)导致查询缓慢,因此我努力摆脱它们。 I am still curious how to get a single Account without calling Single() inside the query.我仍然很好奇如何在查询中不调用Single()来获取单个Account Here's the modified version.这是修改后的版本。

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
            };
}

The SQL query generated by this query runs slow too, whereas if I write a join query in SQL, it completes in a few hundred milliseconds.此查询生成的 SQL 查询运行速度也很慢,而如果我在 SQL 中编写连接查询,它会在几百毫秒内完成。 I am suspecting I am suffering from N+1 Query problem, but not sure since EF seems to generate a single query when I trace in the SQL Server Profiler.我怀疑我遇到了 N+1 查询问题,但不确定,因为当我在 SQL Server Profiler 中跟踪时,EF 似乎生成了一个查询。

This is the query generated by the Entity framework and runs in about 8 seconds when I run on the SSMS:这是 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'

Finally this is my SQL query that returns whatever is necessary in less than 100 ms:最后,这是我的 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

The columns I am searching are all indexed by the way, so that's not a problem.我正在搜索的列都是顺便索引的,所以这不是问题。 I know that the new EF-Core has many performance improvements.我知道新的 EF-Core 有很多性能改进。 Unfortunately, I cannot update due to sheer number of breaking changes.不幸的是,由于大量的重大更改,我无法更新。

I am not sure splitting query into 2 (one for users and one for account) would work well, because there will be joins all over again.我不确定将查询分成 2 个(一个用于用户,一个用于帐户)是否会正常工作,因为会再次出现连接。 If I cannot find a solution using I plan converting my query to a view, but I want to do it as a last resort, since our convention is to use EF as much as possible.如果我找不到解决方案,我计划将我的查询转换为视图,但我想将其作为最后的手段,因为我们的约定是尽可能使用 EF。 And I refuse to believe that EF does not have a solution.而且我拒绝相信 EF 没有解决方案。 This is not actually a complex query at all and I am sure a fairly common use case.这实际上根本不是一个复杂的查询,我确信这是一个相当常见的用例。

So, what is the best way to optimize this query using EF-Core?那么,使用 EF-Core 优化此查询的最佳方法是什么?

So, what is the best way to optimize this query using EF-Core?那么,使用 EF-Core 优化此查询的最佳方法是什么?

Many things have changed in EF Core query pipeline since 2.1 (3.0, 3.1, 5.0 and now working on 6.0), but some general rules can be used, with the goal of getting rid of the client side query evaluation (which starting with 3.0 is not supported at all, so it's good to start preparing for the switch - support for 2.1 ends August this year).自 2.1 以来(3.0、3.1、5.0 和现在正在使用 6.0)在 EF Core 查询管道中发生了许多变化,但可以使用一些通用规则,目的是摆脱客户端查询评估(从 3.0 开始是根本不支持,所以最好开始准备切换 - 对 2.1 的支持将于今年 8 月结束)。

The first would be to remove all these Include / ThenInclude .首先是删除所有这些Include / ThenInclude If the query is projecting the result in DTO without involving entity instances, then all these are redundant/not needed and removing them will ensure the query gets fully translated to SQL.如果查询在 DTO 中投影结果而不涉及实体实例,那么所有这些都是多余的/不需要的,删除它们将确保查询完全转换为 SQL。

var query = _dbContext.Users.AsQueryable();
// Apply filters...

The next is the Roles collection.接下来是Roles集合。 You must remove Mapper.Map call, otherwise it can't be translated.您必须删除Mapper.Map调用,否则无法翻译。 In general either use AutoMapper mappings and ProjectTo to fully handle the projection, or not use it at all (never put Map method calls inside query expression tree).通常,要么使用 AutoMapper 映射和ProjectTo来完全处理投影,要么根本不使用它(永远不要将Map方法调用放在查询表达式树中)。 According to your SQL, it should be something like this根据你的 SQL 应该是这样的

Roles = user.UserRoles.Select(ur => ur.Role)
    .Select(r => new RoleDTO { Name = r.Name })
    .ToList(),

Actually EF Core will execute this as separate query (a behavior broken by "single query mode" in 3.x, and brought back optionally with 6.0 "split query mode"), so it is is important to have ToList() call at the end, otherwise you'll get N + 1 queries rather than 2.实际上 EF Core 会将其作为单独的查询执行(在 3.x 中被“单一查询模式”破坏的行为,并且可以选择在 6.0“拆分查询模式”中恢复ToList() ,因此在结束,否则您将获得 N + 1 个查询而不是 2 个。

Finally, the Single() call.最后是Single()调用。 It can be avoided by flattening the sub collection using correlated SelectMany , or its query syntax equivalent可以通过使用相关的SelectMany或其查询语法等价来展平子集合来避免这种情况

from user in query
let ah = user.AccountHolder
from a in ah.Accounts
where a.CurrencyId == ah.OperationCountryId

The let statement is not mandatory, I've added it just for readability. let语句不是强制性的,我添加它只是为了便于阅读。 Now you can use the range variables user , ah and a in the final select similar to table aliases in SQL.现在您可以在最终的select中使用范围变量useraha ,类似于 SQL 中的表别名。

Also since your SQL query doesn't really enforce single account match, there is no such enforcement in the LINQ query as well.此外,由于您的 SQL 查询并未真正强制执行单一帐户匹配,因此 LINQ 查询中也没有此类强制执行。 If it was needed, then the equivalent of the Single can be achieved with SelectMany + Where + `Take(1), eg如果需要,可以使用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)

(a mixture of query and method syntax, but LINQ allows that) (查询和方法语法的混合,但 LINQ 允许这样做)

So the final query would be something like this所以最终的查询将是这样的

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
}

and should translate to very similar to the handcrafted SQL.并且应该与手工制作的 SQL 非常相似。 And hopefully execute faster similar to it.并希望与它类似地执行得更快。

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

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