Прежде всего, нужно сказать, что это не тривиальный запрос. Вроде бы у нас:
- 6 уровней рекурсии через вложенное дерево вопросов и ответов
- Таким образом, всего 20 таблиц объединяются через загруженную
.Include
Сначала я бы определился, где этот запрос используется в вашем приложении и как часто он нужен, с особым вниманием к тому, где он используется чаще всего.
Оптимизация YAGNI
Очевидное место для начала - посмотреть, где в вашем приложении используется запрос, и если вам не нужно все время дерево целиком, предложите вам не включаться во вложенные таблицы вопросов и ответов, если они не нужны во всех случаях использования запроса.
Кроме того, можно динамически составлять IQueryable
, поэтому, если для вашего запроса есть несколько вариантов использования (например, на экране «Сводка», для которого не нужны вопросы + ответы, и в дереве подробностей, которое делает нужны они), то можно сделать что-то вроде:
var questionnaireQuery = _myContext.Questionnaires
.Include(q => q.Sections)
.Include(q => q.QuestionnaireCommonFields);
// Conditionally extend the joins
if (mustIncludeQandA)
{
questionnaireQuery = questionnaireQuery
.Include(q => q.Sections.Select(s => s.Questions.Select(q => q.Answers..... etc);
}
// Execute + materialize the query
var questionnaires = await questionnaireQuery
.Where(q => questionnaireIds.Contains(q.Id))
.ToListAsync()
.ConfigureAwait(false);
Оптимизация SQL
Если вам действительно нужно все время извлекать все дерево, посмотрите на структуру и индексирование таблицы SQL.
1) Фильтры
.Where(q => questionnaireIds.Contains(q.Id))
(здесь я предполагаю терминологию SQL Server, но эти концепции применимы и в большинстве других RDBM.)
Я предполагаю, что Questionnaires.Id
является кластеризованным первичным ключом, поэтому будет проиндексирован, но просто проверьте его работоспособность (в SSMS это будет выглядеть как PK_Questionnaires CLUSTERED UNIQUE PRIMARY KEY
)
2) Убедитесь, что у всех дочерних таблиц есть индексы на их внешних ключах обратно к родителю.
например. q => q.Sections
означает, что таблица Sections
имеет внешний ключ обратно к Questionnaires.Id
- убедитесь, что в нем есть хотя бы некластеризованный индекс - EF Code First должен сделать это автоматически, но снова, чтобы убедиться в этом.
Это будет выглядеть как IX_QuestionairreId NONCLUSTERED
в столбце Sections(QuestionairreId)
3) Рассмотрите возможность изменения кластерной индексации дочерних таблиц для кластеризации по внешнему ключу их родителя, например, Кластер Section
по Questions.SectionId
. Это сохранит все дочерние строки, связанные с одним и тем же родителем, и уменьшит количество страниц данных, которые должен получить SQL. Это не тривиально сначала достичь в коде EF, но ваш администратор базы данных может помочь вам в этом, возможно, в качестве специального шага.
Другие комментарии
Если этот запрос используется только для запроса данных, а не для обновления или удаления, то добавление .AsNoTracking()
незначительно уменьшит потребление памяти и производительность EF в памяти.
Не имеет отношения к производительности, но вы смешали слабо типизированные ("Sections") и строго типизированные операторы .Include
(q => q.QuestionnaireCommonFields
). Я бы предложил перейти на строго типизированные включения для дополнительной безопасности времени компиляции.
Обратите внимание, что вам нужно только указать путь включения для самых длинных цепочек, которые загружены с нетерпением - это, очевидно, заставит EF также включить все более высокие уровни. то есть вы можете уменьшить 20 .Include
операторов до 2. Это сделает ту же работу более эффективно:
.Include(q => q.QuestionnaireCommonFields)
.Include(q => q.Sections.Select(s => s.Questions.Select(q => q.Answers .... etc))
Вам понадобится .Select
каждый раз, когда есть отношение 1: Множество, но если навигация 1: 1 (или N: 1), тогда вам не нужен .Select, например, City c => c.Country
Перестройка
И последнее, но не менее важное: если данные фильтруются только с верхнего уровня (т. Е. Questionnaires
), и если все дерево запросов (Aggregate Root) обычно всегда добавляется или обновляется сразу, то вы можете попытаться подойти к моделированию данных дерева вопросов и ответов NoSQL
, например просто моделируя все дерево как XML или JSON, а затем обрабатывая все дерево как длинную строку. Это позволит избежать всех неприятных объединений в целом. Вам потребуется пользовательский этап десериализации на уровне данных. Этот последний подход не будет очень полезным, если вам нужно отфильтровать по узлам в дереве (т. Е. Запрос типа найдет мне все вопросы, где ответ на вопрос 5 "Foo" не будет хорошим подходит)