簡體   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