Почему Entity Framework генерирует JOIN на SELECT - PullRequest
3 голосов
/ 23 января 2020

Я использую Entity Framework в приложении C# и использую отложенную загрузку. Мы заметили, что один запрос чрезвычайно сильно влияет на наш процессор, который просто вычисляет сумму. При отладке запроса, сгенерированного Entity Framework, он создает INNER JOIN (SELECT ..., который не является производительным. Когда я вручную изменяю запрос на правильный JOIN, время запроса изменяется от 1,3 сек c до 0,03 сек c.

Позвольте мне проиллюстрировать это упрощенной версией моего кода.

public decimal GetPortfolioValue(Guid portfolioId)
{
   var value = DbContext.Portfolios
        .Where( x => x.Id.Equals(portfolioId) )
        .SelectMany( p => p.Items
            .Where( i => i.Status == ItemStatusConstants.Subscribed 
                && _activeStatuses.Contains( i.Category.Status ) )
        )
        .Select( i => i.Amount )
        .DefaultIfEmpty(0)
        .Sum();

   return value;
}

Это генерирует запрос, который выбирает сумму, но выполняет внутреннее соединение в SELECT двух таблиц, соединенных вместе. Я создал pastebin здесь , чтобы сгенерированный запрос не загрязнял этот вопрос, но сокращенная версия будет выглядеть так:

SELECT ...
FROM `portfolios` AS `Extent1`
INNER JOIN (SELECT 
               `Extent2`.*,
               `Extent3`.*
            FROM `items` AS `Extent2`
            INNER JOIN `categories` AS `Extent3` ON `Extent3`.`id` = 
`Extent2`.`category_id`) AS `Join1`
ON `Extent1`.`id` = `Join1`.`portfolio_id`
    AND ((`Join1`.`status` = @gp1)
    AND (`Join1`.`STATUS1` IN (@gp2, @gp3, @gp4, @gp5, @gp6)))
WHERE ...

Запрос, который я ожидал бы сгенерировать (и который занимает 0,03). se c вместо 1.3 se c) будет выглядеть примерно так:

SELECT ...
FROM `portfolios` AS `Extent1`
INNER JOIN `items` AS `Extent2` ON `Extent2`.`portfolio_id` = `Extent1`.`id`
INNER JOIN `categories` AS `Extent3` ON `Extent3`.`id` = `Extent2`.`category_id`
    AND ((`Extent2`.`status` = @gp1)
    AND (`Extent3`.`status` IN (@gp2, @gp3, @gp4, @gp5, @gp6)))
WHERE ...

Я подозреваю, что это связано с .SelectMany, но я не понимаю, как мне следует переписать запрос LINQ, чтобы сделать его более эффективным. Что касается сущностей, свойства связывания являются виртуальными и имеют настроенный внешний ключ:

public class Portfolio
{
   public Guid Id { get; set; }
   public virtual ICollection<Item> Items { get; set; }
}

public class Item
{
   public Guid Id { get; set; }
   public Guid PortfolioId { get; set; }
   public Guid CategoryId { get; set; }
   public decimal Amount { get; set; }
   public string Status { get; set; }
   public virtual Portfolio Portfolio { get; set; }
   public virtual Category Category { get; set; }
}

public class Category
{
   public Guid Id { get; set; }
   public string Status { get; set; }
   public virtual ICollection<Item> Items { get; set; }
}

Любая помощь будет принята с благодарностью!

1 Ответ

5 голосов
/ 23 января 2020

Поскольку вам ничего не нужно из Portfolio, просто отфильтруйте по PortfolioId, вы можете напрямую запросить PortfolioItems. Предполагая, что ваш DbContext имеет DbSet со всеми элементами во всех портфелях ios, это может выглядеть примерно так:

var value = DbContext.PortfolioItems
                     .Where(i => i.PortfolioId == portfolioId && i.Status == ItemStatusConstants.Subscribed && _activeStatuses.Contains(i.Category.Status))
                     .Sum(i=>i.Amount);                 

Я считаю, что вам не нужны ни DefaultIfEmpty, ни select, если вы используете непосредственно подходящий Queryable .Sum overload.

EDITED: пробовал два разных запроса LINQ без предоставления DbSet.

Первый запрос в основном совпадает с вашим:

var value2 = dbContext.Portfolios
    .Where(p => p.Id == portfolioId)
    .SelectMany(p => p.Items)
    .Where(i => i.Status == "A" && _activeStatuses.Contains(i.Category.Status))
    .Select(i=>i.Amount)
    .DefaultIfEmpty()
    .Sum();

Профилировал запрос на SQL сервере (без MySql под рукой) и выдает уродливое предложение (параметры заменены и кавычки не экранированы для тестирования):

SELECT [GroupBy1].[a1] AS [C1] 
FROM   (SELECT Sum([Join2].[a1_0]) AS [A1] 
    FROM   (SELECT CASE 
                     WHEN ( [Project1].[c1] IS NULL ) THEN Cast( 
                     0 AS DECIMAL(18)) 
                     ELSE [Project1].[amount] 
                   END AS [A1_0] 
            FROM   (SELECT 1 AS X) AS [SingleRowTable1] 
                   LEFT OUTER JOIN 
                   (SELECT [Extent1].[amount] AS [Amount], 
                           Cast(1 AS TINYINT) AS [C1] 
                    FROM   [dbo].[items] AS [Extent1] 
                           INNER JOIN [dbo].[categories] AS 
                                      [Extent2] 
                                   ON [Extent1].[categoryid] = 
                                      [Extent2].[id] 
                    WHERE  ( N'A' = [Extent1].[status] ) 
                           AND ( [Extent1].[portfolioid] = 
                                 'E2CC0CC2-066F-45C9-9D48-543D92C4C92E' ) 
                           AND ( [Extent2].[status] IN ( N'A', N'B', N'C' ) 
                               ) 
                           AND ( [Extent2].[status] IS NOT NULL )) AS 
                   [Project1] 
                                ON 1 = 1) AS [Join2]) AS [GroupBy1] 

Если мы удалим «Select» и «DefaultIfEmpty» методы и переписать запрос следующим образом:

var value = dbContext.Portfolios
    .Where(p => p.Id == portfolioId)
    .SelectMany(p => p.Items)
    .Where(i => i.Status == "A" && _activeStatuses.Contains(i.Category.Status))
    .Sum(i => i.Amount);

Сгенерированное предложение намного чище:

SELECT [GroupBy1].[a1] AS [C1] 
FROM   (SELECT Sum([Extent1].[amount]) AS [A1] 
    FROM   [dbo].[items] AS [Extent1] 
           INNER JOIN [dbo].[categories] AS [Extent2] 
                   ON [Extent1].[categoryid] = [Extent2].[id] 
    WHERE  ( N'A' = [Extent1].[status] ) 
           AND ( [Extent1].[portfolioid] = 
                 'E2CC0CC2-066F-45C9-9D48-543D92C4C92E' ) 
           AND ( [Extent2].[status] IN ( N'A', N'B', N'C' ) ) 
           AND ( [Extent2].[status] IS NOT NULL )) AS [GroupBy1] 

Заключение: мы не можем полагаться на поставщика LINQ для создания оптимизированных запросов. Запрос linq должен быть проанализирован и оптимизирован еще до того, как подумать в сгенерированном предложении SQL.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...