Улучшение производительности большого EF многоуровневого включения - PullRequest
2 голосов
/ 06 апреля 2019

Я - новичок EF (как я только начал сегодня, я использовал только другие ORM), и я испытываю крещение огнем.

Меня попросили улучшитьпроизводительность этого запроса, созданного другим разработчиком:

      var questionnaires = await _myContext.Questionnaires
            .Include("Sections")
            .Include(q => q.QuestionnaireCommonFields)
            .Include("Sections.Questions")
            .Include("Sections.Questions.Answers")
            .Include("Sections.Questions.Answers.AnswerMetadatas")
            .Include("Sections.Questions.Answers.SubQuestions")
            .Include("Sections.Questions.Answers.SubQuestions.Answers")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.AnswerMetadatas")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.AnswerMetadatas")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.AnswerMetadatas")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.AnswerMetadatas")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers")
            .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.AnswerMetadatas")
        .Where(q => questionnaireIds.Contains(q.Id))
        .ToListAsync().ConfigureAwait(false);

Быстрый веб-серфинг говорит мне, что Include () приводит к продукту cols * row и низкой производительности, если вы запускаете несколько уровней глубоко.

Я видел несколько полезных ответов по SO, но они ограничены менее сложными примерами, и я не могу найти лучший подход для переписывания вышеупомянутого.

Многократное повторение части - «Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers ...» выглядит подозрительно для меня, как будто это можно сделать отдельно, а затем еще один запрос, но я неНе знаю, как это сделать, и может ли такой подход даже улучшить производительность.

Вопросы:

  1. Как мне переписать этот запрос, чтобы улучшить егопроизводительность, обеспечивая при этом одинаковый конечный набор результатов?

  2. Учитывая последнюю строку: .Include("Sections.Questions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.SubQuestions.Answers.AnswerMetadatas")
    Зачем мне нужны все промежуточные строки?(Я полагаю, это потому, что некоторые объединения не могут быть оставлены объединениями?)

EF Информация о версии: package id = "EntityFramework" version = "6.2.0" targetFramework = "net452"

Я понимаю, что этот вопрос - немного чепухи, но я пытаюсь решить так быстро, как только могу, не зная ничего.

Редактировать

После обдумывания этого в течение полдня и благодаря предложениям StuartLC я предложил несколько вариантов:

Плохо - разделить запрос так, чтобы он выполнял многократные обходы для извлечения данных.Это, вероятно, предоставит пользователю немного более медленный опыт, но остановит тайм-аут SQL.(Это не намного лучше, чем просто увеличить время ожидания команды EF).

Хорошо - изменить кластеризованную индексацию дочерних таблиц на кластеризацию по внешнему ключу их родителя (при условии, что у вас мало операций вставки).

Хорошо - измените код так, чтобы он запрашивал только первые несколько уровней и отложенную загрузку (отдельное попадание в БД), что-нибудь ниже этого, то есть удалите все, кроме самых верхних включений, затем измените ICollections - answers.SubQuestionsОтветы. Ответы Метаданных и Вопрос. Ответы должны быть виртуальными.Предположительно, недостатком в создании этих виртуальных является то, что если какой-либо (другой) существующий код в приложении ожидает, что эти свойства ICollection будут загружены с нетерпением, вам, возможно, придется обновить этот код (то есть, если вы хотите / хотите, чтобы они сразу загружались в этом коде).Я буду исследовать этот вариант дальше.Дальнейшее редактирование - к сожалению, это не сработает, если вам нужно сериализовать ответ из-за цикла самоссылки.

Нетривиально - написать sql сохраненный proc / view вручную и построить новый объект EF, направленный на него,

Более длительный срок

Очевидный, лучший, но наиболее трудоемкий вариант - переписать дизайн приложения, чтобы не требовалось всего дерева данных за один разВызовите API, или перейдите с опцией ниже:

Перепишите приложение для хранения данных в стиле NoSQL (например, сохраните дерево объектов как json, чтобы не было соединений).Как сказал Стюарт, это не очень хороший вариант, если вам нужно отфильтровать данные другими способами (с помощью чего-то другого, кроме идентификатора вопросника), что может потребоваться сделать.Другой альтернативой является частичное хранение в стиле NoSQL и частично реляционное при необходимости.

1 Ответ

3 голосов
/ 07 апреля 2019

Прежде всего, нужно сказать, что это не тривиальный запрос. Вроде бы у нас:

  • 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" не будет хорошим подходит)

...