Подтвердить что ты не робот

Код Entity-framework медленный при использовании Include() много раз

Я отлаживал некоторый медленный код, и кажется, что виновником является код EF, указанный ниже. Это займет 4-5 секунд, когда запрос оценивается на более позднем этапе. Я пытаюсь заставить его работать менее чем за 1 секунду.

Я тестировал это с помощью SQL Server Profiler, и кажется, что выполняется куча SQL-скриптов. Он также подтверждает, что он занимает 3-4 секунды, прежде чем SQL-сервер будет выполнен с выполнением.

Я прочитал другие подобные вопросы об использовании Include(), и кажется, что при его использовании существует штраф за производительность. Я попытался разбить приведенный ниже код на несколько разных запросов, но это не имеет большого значения.

Любая идея, как я могу получить ниже, чтобы выполнить быстрее?

В настоящее время веб-приложение, над которым я работаю, просто показывает пустой iframe, ожидая завершения ниже. Если я не могу получить более быстрое время выполнения, я должен разбить его и частично загрузить iframe с данными или перейти к другому асинхронному решению. Любые идеи здесь также будут оценены!

using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted }))
        {
            formInstance = context.FormInstanceSet
                                .Includes(x => x.Include(fi => fi.FormDefinition).Include(fd => fd.FormSectionDefinitions).Include(fs => fs.FormStateDefinitionEditableSections))
                                .Includes(x => x.Include(fi => fi.FormDefinition).Include(fd => fd.FormStateDefinitions))
                                .Includes(x => x.Include(fi => fi.FormSectionInstances).Include(fs => fs.FormFieldInstances).Include(ff => ff.FormFieldDefinition).Include(ffd => ffd.FormFieldMetaDataDefinition).Include(ffmdd => ffmdd.ComplexTypePropertyNames))
                                .Include(x => x.CurrentFormStateInstance)      
                                .Include(x => x.Files)
                                .FirstOrDefault(x => x.FormInstanceIdentifier == formInstanceIdentifier);

            scope.Complete();
        }
4b9b3361

Ответ 1

кажется, что при использовании Включить есть снижение производительности

Это преуменьшение! Множественное Include быстро увеличивает результат запроса SQL как по ширине, так и по длине. Это почему?

tl; dr Multiple Include взорвать набор результатов SQL. Вскоре становится дешевле загружать данные с помощью нескольких обращений к базе данных вместо выполнения одного мега-оператора. Попробуйте найти лучшее сочетание операторов Include и Load.

Стимулятор роста Include с

Скажем у нас есть

  • корневая сущность Root
  • родительская сущность Root.Parent
  • дочерние объекты Root.Children1 и Root.Children2
  • оператор LINQ Root.Include("Parent").Include("Children1").Include("Children2")

Это создает инструкцию SQL, которая имеет следующую структуру:

SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children1

UNION

SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children2

Эти <PseudoColumns> состоят из выражений типа CAST(NULL AS int) AS [C2], и служат для того, чтобы иметь одинаковое количество столбцов во всех запросах UNION -ed. Первая часть добавляет псевдостолбцы для Child2, вторая часть добавляет псевдостолбцы для Child1.

Вот что это значит для размера набора результатов SQL:

  • Количество столбцов в предложении SELECT является суммой всех столбцов в четырех таблицах.
  • Количество строк - это сумма записей во включенных дочерних коллекциях.

Так как общее количество точек данных равно columns * rows, каждое дополнительное Include экспоненциально увеличивает общее количество точек данных в наборе результатов. Позвольте мне продемонстрировать это, взяв Root снова, теперь с дополнительной коллекцией Children3. Если все таблицы имеют 5 столбцов и 100 строк, мы получим:

Одно Include (Root + 1 дочерняя коллекция): 10 столбцов * 100 строк = 1000 точек данных.
Два Include (Root + 2 дочерние коллекции): 15 столбцов * 200 строк = 3000 точек данных.
Три Include (Root + 3 дочерние коллекции): 20 столбцов * 300 строк = 6000 точек данных.

С 12 Includes это составит 78000 точек данных!

И наоборот, если вы получите все записи для каждой таблицы отдельно, а не 12 Includes, у вас будет 13 * 5 * 100 точек данных: 6500, менее 10%!

Теперь эти числа несколько преувеличены, так как многие из этих точек данных будут null, поэтому они не сильно влияют на фактический размер набора результатов, отправляемого клиенту. Но размер запроса и задача оптимизатора запросов, безусловно, будут отрицательно сказываться на увеличении числа Include объектов.

Остаток средств

Таким образом, использование Includes - это тонкий баланс между стоимостью вызовов базы данных и объемом данных. Это трудно дать правило большого пальца, но теперь вы можете себе представить, что объем данных обычно быстро перерастает стоимость дополнительных вызовов, если есть больше чем ~ 3 Includes для детских коллекций (но совсем немного больше для родителей Includes в Includes, что только расширить набор результатов).

альтернатива

Альтернативой к Include является загрузка данных в отдельных запросах:

context.Configuration.LazyLoadingEnabled = false;
var rootId = 1;
context.Children1.Where(c => c.RootId == rootId).Load();
context.Children2.Where(c => c.RootId == rootId).Load();
return context.Roots.Find(rootId);

Это загружает все необходимые данные в кэш контекста. В ходе этого процесса EF выполняет исправление отношений, с помощью которого автоматически заполняет свойства навигации (Root.Children и т.д.) Загруженными объектами. Конечный результат идентичен оператору с " Include, за исключением одного важного различия: дочерние коллекции не помечаются как загруженные в диспетчере состояний сущностей, поэтому EF будет пытаться инициировать отложенную загрузку, если вы обращаетесь к ним. Вот почему так важно отключить ленивую загрузку.

В действительности вам придется выяснить, какая комбинация операторов Include и Load лучше всего подойдет вам.

Ответ 2

Правильно ли вы настроили отношения между всеми объектами, которые вы пытаетесь включить? Если хотя бы один объект не имеет отношения к некоторым другим объектам, EF не сможет построить один сложный запрос, используя синтаксис SQL-соединения, вместо этого он выполнит столько запросов, сколько у вас есть "включает". И, конечно же, это приведет к проблемам с производительностью. Не могли бы вы отправить точный запрос (-es), который генерирует EF для получения данных?

Ответ 3

У меня похожая проблема с запросом, который содержал операторы 15+ "Включить" и генерировал строки 2M+ за 7 минут.

Решение, которое работало для меня, было:

  1. Отключена ленивая загрузка
  2. Отключено автоматическое обнаружение изменений
  3. Разделить большой запрос на маленькие кусочки

Образец можно найти ниже:

public IQueryable<CustomObject> PerformQuery(int id) 
{
 ctx.Configuration.LazyLoadingEnabled = false;
 ctx.Configuration.AutoDetectChangesEnabled = false;

 IQueryable<CustomObject> customObjectQueryable = ctx.CustomObjects.Where(x => x.Id == id);

 var selectQuery = customObjectQueryable.Select(x => x.YourObject)
                                                  .Include(c => c.YourFirstCollection)
                                                  .Include(c => c.YourFirstCollection.OtherCollection)
                                                  .Include(c => c.YourSecondCollection);

 var otherObjects = customObjectQueryable.SelectMany(x => x.OtherObjects);

 selectQuery.FirstOrDefault();
 otherObjects.ToList();

 return customObjectQueryable;
 }

IQueryable необходим для того, чтобы выполнить всю фильтрацию на стороне сервера. IEnumerable будет выполнять фильтрацию в памяти, и это очень трудоемкий процесс. Entity Framework исправит любые ассоциации в памяти.