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

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

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

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

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

Ниже приведены некоторые методы денормализации и кэширования в 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 должен представлять всю иерархию в одной таблице, строки имеют все столбцы независимо от типа, который фактически хранится в строке, и несвязанные столбцы всегда пусты и не используются. Помимо того, что этот метод сопоставления кажется "нечистым", многие считают, что эти пустые столбцы занимают значительное место в базе данных и могут отрицательно сказаться на производительности.

Совет

Если ваша система базы данных поддерживает ее (e.g. 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 для этого сценария. Обратите внимание, что фактические результаты всегда зависят от конкретного выполняемого запроса и количества таблиц в иерархии, поэтому другие запросы могут показать другой разрыв производительности; Этот код теста рекомендуется использовать в качестве шаблона для тестирования других запросов.