C # — лямбда-выражение LINQ с использованием GroupBy — Почему вложенные проверки настолько неэффективны?

#c# #performance #linq #.net-core #lambda

#c# #Производительность #linq #.net-core #лямбда

Вопрос:

У меня был неудачный день, когда я пытался повысить производительность следующего запроса на C #, используя Entity Framework (информация хранится на сервере SQL, а структура использует подход Code First — но в данный момент это не имеет значения):

Запрос с плохой производительностью:

 var projectDetail = await _context
    .ProjectDetails
    .Where(pd => projectHeaderIds.Contains(pd.IdProjectHeader))
    .Include(pd => pd.Stage)
    .Include(pd => pd.ProjectTaskStatus)
    .GroupBy(g => new { g.IdProjectHeader, g.IdStage, g.Stage.StageName })
    .Select(pd => new
    {
        pd.Key.IdProjectHeader,
        pd.Key.IdStage,
        pd.Key.StageName,
        TotalTasks = pd.Count(),
        MissingCriticalActivity = pd.Count(t => t.CheckTask.CriticalActivity amp;amp; t.ProjectTaskStatus.Score != 100) > 0,
        Score = Math.Round(pd.Average(a => a.ProjectTaskStatus.Score), 2),
        LastTaskCompleted = pd.Max(p => p.CompletionDate)
    }).ToListAsync();
  

Через несколько часов я разобрался с проблемой и смог исправить производительность (вместо этого требуется более 4 минут, теперь новый запрос занимает всего 1-2 секунды):

Новый запрос

 var groupTotalTasks = await _context
    .ProjectDetails
    .Where(pd => projectHeaderIds.Contains(pd.IdProjectHeader))
    .Select(r => new
    {
        r.IdProjectHeader,
        r.CompletionDate,
        r.IdStage,
        r.ProjectTaskStatus.Score,
        r.CheckTask.CriticalActivity,
        r.Stage.StageName
    })
    .GroupBy(g => new { g.IdProjectHeader, g.IdStage, g.StageName })
    .Select(pd => new
    {
        pd.Key.IdProjectHeader,
        pd.Key.IdStage,
        pd.Key.StageName,
        TotalTasks = pd.Count(),
        MissingCriticalActivity = pd.Count(r => r.CriticalActivity amp;amp; r.Score != 100) > 0,
        Score = Math.Round(pd.Average(a => a.Score), 2),
        LastTaskCompleted = pd.Max(p => p.CompletionDate)
    }).ToListAsync();
  

Шаги по улучшению запроса были следующими:

  • Избегайте nested validations (например Score , используйте MainQuery.ProjectTaskStatus.Score для вычисления average )
  • Избегайте Include в запросах
  • Я использовал a Select только для получения информации, которую я буду использовать после в GroupBy .

Эти изменения устранили мою проблему, но почему?

…и, тем не менее, существует другой способ улучшить этот запрос?

По каким причинам использование вложенных проверок делает запрос чрезвычайно медленным? Другие изменения имеют для меня больше смысла.

Комментарии:

1. Какую версию EF Core вы используете?

2. @StriplingWarrior .Net Core 2.2

Ответ №1:

Недавно я прочитал, что всякий раз, когда EF Core 2 сталкивался с чем-либо, для чего он не мог создать SQL-запрос, он переключался на оценку в памяти. Таким образом, первый запрос в основном будет извлекать все ваши ProjectDetails из базы данных, а затем выполнять всю группировку и тому подобное в памяти вашего приложения. Это, вероятно, самая большая проблема, с которой вы столкнулись.

.Include В этом случае использование оказало большое влияние, потому что вы включали кучу других данных, когда извлекали все эти ProjectDetails. Вероятно, теперь это практически не влияет, поскольку вы избегали выполнения всей этой работы в памяти.

Они поняли ошибку по-своему и изменили поведение, чтобы генерировать исключение в подобных случаях, начиная с EF Core 3.

Чтобы избежать подобных проблем в будущем, вы можете перейти на EF Core 3 или просто быть очень осторожным, чтобы убедиться, что Entity Framework может перевести все в вашем запросе в SQL.

Комментарии:

1. Имеет смысл, но вложенные оценки являются узким местом в моем запросе. Но я обязательно сделаю переход на .Net 3, чтобы избежать дальнейших проблем.