Моделирование для производительности

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

Денормализация и кэширование

Денормализация — это практика добавления избыточных данных в схему, как правило, для устранения соединений при запросе. Например, для модели с блогами и записями, где каждая запись имеет рейтинг, вам может потребоваться часто отображать средний рейтинг блога. Простой подход к этому может сгруппировать записи по их блогу и вычислить среднее значение в рамках запроса; но для этого требуется дорогостоящее соединение между двумя таблицами. Денормализация добавит вычисляемое среднее значение всех записей в новый столбец блога, чтобы он был немедленно доступен, не присоединяясь или не вычисляя.

Приведенные выше данные можно рассматривать как форму кэширования — агрегированные данные из записей кэшируются в своем блоге. Как и при любом кэшировании, проблема заключается в том, как сохранить кэшированное значение в актуальном состоянии с данными, которые он кэширует. Во многих случаях кэшированные данные могут отстать немного; Например, в приведенном выше примере обычно разумно, чтобы средний рейтинг блога не был полностью обновлен в любой момент. Если это так, вы можете пересчитывать его каждый раз, а затем; в противном случае необходимо настроить более сложную систему, чтобы сохранить кэшированные значения в актуальном состоянии.

Ниже приведены некоторые методы денормализации и кэширования в EF Core, а также приведены соответствующие разделы документации.

Сохраненные вычисляемые столбцы

Если кэшированные данные являются продуктом других столбцов в той же таблице, то сохраненный вычисляемый столбец может быть идеальным решением. Например, Customer может иметь FirstName и LastName столбцы, но нам может потребоваться выполнить поиск по полному имени клиента. Сохраненный вычисляемый столбец автоматически сохраняется базой данных , которая пересчитывает ее при изменении строки, и вы даже можете определить индекс по нему, чтобы ускорить запросы.

Обновление столбцов кэша при изменении входных данных

Если кэшированный столбец должен ссылаться на входные данные извне строки таблицы, вы не можете использовать вычисляемые столбцы. Однако при изменении входных данных столбец по-прежнему можно пересчитывать; Например, вы можете пересчитывать средний рейтинг блога при каждом изменении, добавлении или удалении. Не забудьте определить точные условия, когда требуется пересчет, в противном случае кэшированное значение выйдет из синхронизации.

Один из способов сделать это — выполнить обновление самостоятельно через обычный API EF Core. SaveChangesСобытия или перехватчики можно использовать для автоматического проверка, если какие-либо записи обновляются, и для выполнения пересчета таким образом. Обратите внимание, что обычно это приводит к дополнительным круглым обходам базы данных, так как необходимо отправлять дополнительные команды.

Для более высокочувствительных приложений триггеры базы данных можно определить для автоматического выполнения пересчета в базе данных. Это позволяет сохранять дополнительные циклы базы данных, автоматически происходит в той же транзакции, что и основное обновление, и может быть проще настроить. EF не предоставляет какой-либо конкретный API для создания или обслуживания триггеров, но это совершенно хорошо подходит для создания пустой миграции и добавления определения триггера через необработанный SQL.

Материализованные или индексированные представления

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

Конкретная поддержка материализованных представлений зависит от баз данных. В некоторых базах данных (например , PostgreSQL) материализованные представления необходимо обновить вручную, чтобы их значения были синхронизированы с базовыми таблицами. Обычно это делается с помощью таймера ( в случаях, когда некоторые задержки данных допустимы или через триггер или вызов хранимой процедуры в определенных условиях. С другой стороны, индексированные представления SQL Server автоматически обновляются по мере изменения базовых таблиц. Это гарантирует, что представление всегда отображает последние данные за счет более медленных обновлений. Кроме того, представления индекса SQL Server имеют различные ограничения на то, что они поддерживают; Дополнительные сведения см. в документации .

EF в настоящее время не предоставляет какой-либо конкретный API для создания или поддержания представлений, материализованных или индексированных или иных вариантов; но это прекрасно подходит для создания пустой миграции и добавления определения представления с помощью необработанного SQL.

Сопоставление наследования

Прежде чем продолжить работу с этим разделом, рекомендуется прочитать выделенную страницу на наследование .

EF Core в настоящее время поддерживает три метода сопоставления модели наследования с реляционной базой данных:

  • Таблица на иерархию (TPH), в которой вся иерархия классов .NET сопоставляется с одной таблицей базы данных.
  • Таблица на тип (TPT), в которой каждый тип в иерархии .NET сопоставляется с другой таблицей в базе данных.
  • Таблица на конкретный тип (TPC), в которой каждый конкретный тип в иерархии .NET сопоставляется с другой таблицей в базе данных, где каждая таблица содержит столбцы для всех свойств соответствующего типа.

Выбор метода сопоставления наследования может оказать значительное влияние на производительность приложения— рекомендуется тщательно измерять перед фиксацией к выбору.

Интуитивно, TPT может показаться как "чистый" метод; Отдельная таблица для каждого типа .NET делает схему базы данных похожей на иерархию типов .NET. Кроме того, так как TPH должен представлять всю иерархию в одной таблице, строки имеют все столбцы независимо от типа, который фактически хранится в строке, и несвязанные столбцы всегда пусты и не используются. Помимо казалось бы, является "нечистым" методом сопоставления, многие считают, что эти пустые столбцы занимают значительное место в базе данных и могут также повредить производительность.

Совет

Если ваша система базы данных поддерживает ее (например, SQL Server), попробуйте использовать "разреженные столбцы" для столбцов TPH, которые редко заполняются.

Однако измерение показывает, что TPT в большинстве случаев является более низкой методикой сопоставления с точки зрения производительности; где все данные в TPH приходят из одной таблицы, запросы TPT должны объединять несколько таблиц и соединения являются одним из основных источников проблем с производительностью в реляционных базах данных. Базы данных также обычно хорошо подходят к пустым столбцам, и такие функции, как разреженные столбцы SQL Server, могут снизить эти издержки еще дальше.

TPC имеет аналогичные характеристики производительности для TPH, но немного медленнее при выборе сущностей всех типов, так как это включает несколько таблиц. Однако TPC действительно выполняется при запросе сущностей одного конечного типа. Запрос использует только одну таблицу и не требует фильтрации.

Конкретный пример см. в этом тесте, который настраивает простую модель с иерархией 7 типов. 5000 строк заполняются для каждого типа — всего 35000 строк, и тест просто загружает все строки из базы данных:

Метод Среднее значение Ошибка StdDev 0-го поколения Поколение 1 Распределено
TPH 149.0 мс 3.38 мс 9.80 мс 4000.0000 1000.0000 40 МБ
TPT 312.9 мс 6.17 мс 10.81 мс 9000.0000 3000.0000 75 МБ
TPC 158.2 мс 3.24 мс 8.88 мс 5000.0000 2000.0000 46 МБ

Как видно, TPH и TPC значительно эффективнее, чем TPT для этого сценария. Обратите внимание, что фактические результаты всегда зависят от конкретного выполняемого запроса и количества таблиц в иерархии, поэтому другие запросы могут показать другой разрыв производительности; Этот код теста рекомендуется использовать в качестве шаблона для тестирования других запросов.