Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Дэвид Obando, Эрик Деттингер и другие
Опубликовано: апрель 2012 г.
Последнее обновление: май 2014 г.
1. Введение
Фреймворки объектно-реляционного отображения — это удобный способ абстрагирования доступа к данным в объектно-ориентированном приложении. Для приложений .NET рекомендуется использовать O/RM корпорации Майкрософт в Entity Framework. Однако с любой абстракцией производительность может стать проблемой.
Этот технический документ был написан, чтобы показать рекомендации по производительности при разработке приложений с помощью Entity Framework, чтобы дать разработчикам представление о внутренних алгоритмах Entity Framework, которые могут повлиять на производительность, и предоставить советы по изучению и улучшению производительности в своих приложениях, использующих Entity Framework. Есть ряд хороших тем о производительности, уже доступных в Интернете, и мы также пытались указать на эти ресурсы, где это возможно.
Производительность является сложной темой. Этот технический документ предназначен как ресурс для принятия решений, связанных с производительностью для приложений, использующих Entity Framework. Мы включили некоторые тестовые метрики для демонстрации производительности, но эти метрики не предназначены в качестве абсолютных показателей производительности, которые вы увидите в приложении.
В практических целях в этом документе предполагается, что Entity Framework 4 выполняется в .NET 4.0 и Entity Framework 5 и 6 выполняются в .NET 4.5. Многие улучшения производительности, сделанные для Entity Framework 5, находятся в основных компонентах, которые поставляются с .NET 4.5.
Entity Framework 6 является независимым выпуском и не зависит от компонентов Entity Framework, входящих в состав .NET. Entity Framework 6 работает как с .NET 4.0, так и с .NET 4.5 и может предложить значительное преимущество в производительности для тех, кто не обновился с .NET 4.0, но хотят иметь последние версии Entity Framework в приложении. Когда в этом документе упоминается Entity Framework 6, он ссылается на последнюю версию, доступную во время написания этой статьи: версия 6.1.0.
2. Холодное и теплое выполнение запросов
Самый первый раз, когда любой запрос выполняется в отношении данной модели, Entity Framework выполняет большую работу за кулисами для загрузки и проверки модели. Мы часто называем этот первый запрос "холодным" запросом. Дальнейшие запросы к уже загруженной модели называются "теплыми" запросами и гораздо быстрее.
Давайте рассмотрим высокоуровневое представление о том, где время тратится при выполнении запроса с помощью Entity Framework, и посмотрим, где улучшаются возможности Entity Framework 6.
Выполнение первого запроса — холодный запрос
| Код, написанный пользователем | Действие | Влияние на производительность EF4 | Влияние на производительность EF5 | Влияние на производительность EF6 |
|---|---|---|---|---|
using(var db = new MyContext()) { |
Создание контекста | Средний | Средний | Low |
var q1 = from c in db.Customers where c.Id == id1 select c; |
Создание выражения запроса | Low | Low | Low |
var c1 = q1.First(); |
Выполнение запроса LINQ | — загрузка метаданных: высокая, но кэшированная — Генерация видов: потенциально очень высокая, но кэшируемая — оценка параметров: средний — перевод запросов среднего уровня — генерация материализатора: средняя, но кэшированная — Выполнение запроса базы данных: потенциально высокий + Connection.Open + Command.ExecuteReader + DataReader.Read Материализация объектов: средняя — поиск идентификационных данных: средний |
— загрузка метаданных: высокая, но кэшированная Генерация представления: потенциально очень высокая, но кэшированная — оценка параметров: низкая перевод запроса: средний, но хранится в кэше — генерация материализатора: средняя, но кэшированная — Выполнение запроса базы данных: потенциально высокий (более лучшие запросы в некоторых ситуациях) + Connection.Open + Command.ExecuteReader + DataReader.Read Материализация объектов: Средний поиск идентификации: средний |
— загрузка метаданных: высокая, но кэшированная — Генерация представления: средняя, но кэшированная — оценка параметров: низкая перевод запроса: средний, но хранится в кэше — генерация материализатора: средняя, но кэшированная — Выполнение запросов к базе данных: может быть высоким (лучшие запросы в некоторых ситуациях) + Connection.Open + Command.ExecuteReader + DataReader.Read Материализация объектов: Средний (быстрее, чем EF5) — поиск идентификаций: средний |
} |
Connection.Close | Low | Low | Low |
Выполнение второго запроса — теплый запрос
| Код, написанный пользователем | Действие | Влияние на производительность EF4 | Влияние на производительность EF5 | Влияние на производительность EF6 |
|---|---|---|---|---|
using(var db = new MyContext()) { |
Создание контекста | Средний | Средний | Low |
var q1 = from c in db.Customers where c.Id == id1 select c; |
Создание выражения запроса | Low | Low | Low |
var c1 = q1.First(); |
Выполнение запроса LINQ | — Поиск загрузки метаданных: высокая, но кэшированная низкая - Просмотр подстановки — оценка параметров: средний — - Поиск — Выполнение запроса базы данных: потенциально высокий + Connection.Open + Command.ExecuteReader + DataReader.Read Материализация объектов: средняя сложность поиск идентичности: средний |
— Загрузка метаданных: высокая, но кэшированная. Низкая - Подстановочный просмотр — оценка параметров: низкая — поиск - Поиск — Выполнение запроса базы данных: потенциально высокий (более лучшие запросы в некоторых ситуациях) + Connection.Open + Command.ExecuteReader + DataReader.Read Материализация объектов: средняя — поиск идентификационных данных: средний |
— Загрузка и поиск метаданных: Высокая при кэшировании, низкая без него. — Просмотр подстановки — оценка параметров: низкая — поиск - Поиск — Выполнение запроса базы данных: потенциально высокий (более лучшие запросы в некоторых ситуациях) + Connection.Open + Command.ExecuteReader + DataReader.Read Материализация объектов: Средний (быстрее, чем EF5) — поиск идентификационных данных: средний |
} |
Connection.Close | Low | Low | Low |
Существует несколько способов снижения производительности холодных и теплых запросов, и мы рассмотрим их в следующем разделе. В частности, мы рассмотрим снижение затрат на загрузку модели в холодных запросах с помощью предварительно созданных представлений, которые должны помочь облегчить проблемы производительности, возникающие во время создания представлений. Для теплых запросов мы рассмотрим кэширование плана запросов, запросы без отслеживания и различные параметры выполнения запросов.
2.1 Что такое поколение представлений?
Чтобы понять, что такое создание представлений, необходимо сначала понять, что такое "Представления сопоставления". Представления сопоставления — это исполняемые представления преобразований, указанных в сопоставлении для каждого набора сущностей и связи. Внутренне эти представления сопоставления принимают форму CQTs (канонических деревьев запросов). Существует два типа представлений сопоставления:
- Представления запросов: это преобразование, необходимое для перехода от схемы базы данных к концептуальной модели.
- Представления обновлений: это преобразования, необходимые для перехода от концептуальной модели к схеме базы данных.
Имейте в виду, что концептуальная модель может отличаться от схемы базы данных различными способами. Например, для хранения данных для двух разных типов сущностей может использоваться одна таблица. Наследование и нетривиальные сопоставления играют роль в сложности представлений сопоставления.
Процесс вычисления этих представлений на основе спецификации сопоставления называется генерацией представлений. Создание представлений может происходить динамически при загрузке модели или во время сборки с помощью предварительно созданных представлений; последний сериализуется в виде инструкций Entity SQL в файл C# или VB.
При создании представлений они также проверяются. С точки зрения производительности, подавляющее большинство затрат на создание представления приходятся в основном на проверку представлений, что гарантирует, что соединения между сущностями имеют смысл и имеют правильную кардинальность для всех поддерживаемых операций.
При выполнении запроса по набору сущностей запрос объединяется с соответствующим представлением запроса, а результат этой композиции обрабатывается с помощью компилятора плана, чтобы создать представление запроса, которое может понять основное хранилище. Для SQL Server окончательный результат этой компиляции будет инструкцией T-SQL SELECT. При первом выполнении обновления набора сущностей представление обновления выполняется через аналогичный процесс, чтобы преобразовать его в инструкции DML для целевой базы данных.
2.2 Факторы, влияющие на производительность создания представлений
Производительность шага создания представления зависит не только от размера модели, но и от того, как связана модель. Если две сущности соединены через цепочку наследования или ассоциацию, они считаются подключенными. Аналогично, если две таблицы подключены через внешний ключ, они подключены. По мере увеличения количества подключенных сущностей и таблиц в схемах стоимость создания представления увеличивается.
Алгоритм, используемый для создания и проверки представлений, является экспоненциальным в худшем случае, хотя мы используем некоторые оптимизации для улучшения этого. Самые большие факторы, которые, как представляется, негативно влияют на производительность:
- Размер модели, ссылающийся на количество сущностей и количество связей между этими сущностями.
- Сложность модели, в частности наследование с большим количеством типов.
- Использование независимых ассоциаций вместо ассоциаций внешних ключей.
Для небольших простых моделей стоимость может быть настолько невысокой, что нет необходимости использовать предварительно созданные представления. При увеличении размера и сложности модели существует несколько вариантов, которые позволяют сократить затраты на создание и проверку представления.
2.3. Использование предварительно созданных представлений для уменьшения времени загрузки модели
Подробные сведения об использовании предварительно созданных представлений в Entity Framework 6 см. в предварительно созданных представлениях сопоставления
2.3.1 Предварительно созданные представления с помощью Entity Framework Power Tools Community Edition
Вы можете использовать Entity Framework 6 Power Tools Community Edition для создания представлений моделей EDMX и Code First, щелкнув правой кнопкой мыши файл класса модели, открыв меню Entity Framework и выбрав пункт "Создать представления". Entity Framework Power Tools Community Edition работает только в контекстах, производных от DbContext.
2.3.2 Использование предварительно созданных представлений с моделью, созданной EDMGen
EDMGen — это программа, которая поставляется с .NET и работает с Entity Framework 4 и 5, но не с Entity Framework 6. EDMGen позволяет создавать файл модели, слой объектов и представления из командной строки. Одним из выходных данных будет файл Views на выбранном языке, VB или C#. Это файл кода, содержащий фрагменты кода Entity SQL для каждого набора сущностей. Чтобы включить предварительно созданные представления, просто включите файл в проект.
Если вы вручную вносите изменения в файлы схемы для модели, вам потребуется повторно создать файл представлений. Это можно сделать, запустив EDMGen с флагом /mode:ViewGeneration .
2.3.3. Использование предварительно созданных представлений с файлом EDMX
Вы также можете использовать EDMGen для создания представлений для EDMX-файла. Ранее упоминаемый раздел MSDN описывает, как добавить событие предварительной сборки для этого- но это сложно, и есть некоторые случаи, когда это невозможно. Как правило, проще использовать шаблон T4 для создания представлений, когда модель находится в edmx-файле.
В блоге команды ADO.NET содержится запись, описывающая использование шаблона T4 для создания представлений ( <https://learn.microsoft.com/archive/blogs/adonet/how-to-use-a-t4-template-for-view-generation>). Эта запись содержит шаблон, который можно скачать и добавить в проект. Шаблон был написан для первой версии Entity Framework, поэтому они не гарантированы для работы с последними версиями Entity Framework. Однако вы можете скачать более актуальный набор шаблонов для создания представлений для Entity Framework 4 и 5 из галереи Visual Studio.
- VB.NET: <http://visualstudiogallery.msdn.microsoft.com/118b44f2-1b91-4de2-a584-7a680418941d>
- C#: <http://visualstudiogallery.msdn.microsoft.com/ae7730ce-ddab-470f-8456-1b313cd2c44d>
Если вы используете Entity Framework 6, вы можете получить шаблоны создания представлений T4 из коллекции Visual Studio по адресу <http://visualstudiogallery.msdn.microsoft.com/18a7db90-6705-4d19-9dd1-0a6c23d0751f>.
2.4 Снижение затрат на создание представления
Использование предварительно созданных представлений перемещает стоимость создания представления с загрузки модели (время выполнения) до времени разработки. Хотя это улучшает производительность запуска во время выполнения, вы по-прежнему сталкиваетесь с трудностями при создании представлений во время разработки. Существует несколько дополнительных способов, которые могут помочь сократить затраты на создание представления, как во время компиляции, так и во время выполнения.
2.4.1 Использование связей внешних ключей для снижения затрат на создание представлений
Мы видели ряд случаев, когда переключение ассоциаций в модели с независимых ассоциаций на внешние ключевые ассоциации значительно улучшило время, затраченное на создание представлений.
Чтобы продемонстрировать это улучшение, мы создали две версии модели Navision с помощью EDMGen. Примечание. См. приложение C для описания модели Navision. Модель Navision интересна для этого упражнения из-за его очень большого количества сущностей и связей между ними.
Одна из версий этой очень большой модели была создана с помощью ассоциаций внешних ключей, а другая была создана с независимыми ассоциациями. Затем мы засекли, сколько времени потребовалось для генерации представлений для каждой модели. Тест Entity Framework 5 использовал метод GenerateViews() из класса EntityViewGenerator для создания представлений, а тест Entity Framework 6 использовал метод GenerateViews() из класса StorageMappingItemCollection. Это связано с реструктуризацией кода, которая произошла в базе кода Entity Framework 6.
С помощью Entity Framework 5 представление модели с внешними ключами заняло 65 минут на компьютере лаборатории. Неизвестно, сколько времени потребовалось бы для создания представлений для модели, которая использовала независимые ассоциации. Мы проводили тест больше месяца, прежде чем компьютер был перезагружен в нашей лаборатории для установки ежемесячных обновлений.
С помощью Entity Framework 6 представление модели с внешними ключами заняло 28 секунд на том же компьютере лаборатории. Создание представления для модели, использующей независимые ассоциации, заняло 58 секунд. Улучшения, выполненные в Entity Framework 6 в коде создания представления, означают, что многие проекты не требуют предварительно созданных представлений для быстрого запуска.
Важно отметить, что предварительно созданные представления в Entity Framework 4 и 5 можно сделать с помощью EDMGen или Entity Framework Power Tools. Для генерации представлений в Entity Framework 6 можно использовать Entity Framework Power Tools или создавать их программно, как описано в разделе Предварительно созданные представления сопоставления.
2.4.1.1 Как использовать внешние ключи вместо независимых ассоциаций
При использовании EDMGen или конструктора сущностей в Visual Studio вы получаете FK по умолчанию, а для переключения между FKs и IAs используется только один флажок или флаг командной строки.
Если у вас есть большая модель Code First, использование независимых ассоциаций будет иметь тот же эффект на создание представления. Это влияние можно избежать, включив свойства внешнего ключа в классы для зависимых объектов, хотя некоторые разработчики считают, что это загрязняет свою объектную модель. Дополнительные сведения об этой теме см. в <http://blog.oneunicorn.com/2011/12/11/whats-the-deal-with-mapping-foreign-keys-using-the-entity-framework/>разделе .
| При использовании | Сделай это |
|---|---|
| Конструктор сущностей | После добавления связи между двумя сущностями убедитесь, что у вас есть ссылочное ограничение. Ссылочные ограничения указывают Entity Framework на использование внешних ключей вместо независимых ассоциаций. Дополнительные сведения см. в статье <https://learn.microsoft.com/archive/blogs/efdesign/foreign-keys-in-the-entity-framework>. |
| EDMGen | При использовании EDMGen для генерации ваших файлов из базы данных, внешние ключи будут учтены и добавлены в модель соответственно. Дополнительные сведения о различных параметрах, предоставляемых EDMGen, см. на http://msdn.microsoft.com/library/bb387165.aspx. |
| Первоочередность кода | Дополнительные сведения о том, как включить свойства внешнего ключа для зависимых объектов при использовании Code First, см. в разделе "Соглашение о связи". |
2.4.2 Перемещение модели в отдельную сборку
Если модель включена непосредственно в проект приложения и вы создаете представления с помощью предварительного события сборки или шаблона T4, создание представлений и проверка будут выполняться всякий раз, когда проект перестроен, даже если модель не была изменена. Если вы перемещаете модель в отдельную сборку и ссылаетесь на нее из проекта приложения, вы можете внести другие изменения в приложение без необходимости перестроить проект, содержащий модель.
Примечание. При перемещении модели в отдельные сборки не забудьте скопировать строки подключения для модели в файл конфигурации приложения клиентского проекта.
2.4.3 Отключение проверки модели на основе edmx
Модели EDMX проверяются во время компиляции, даже если модель не изменяется. Если модель уже проверена, можно отключить проверку во время компиляции, установив для свойства Validate on Build значение false в окне свойств. При изменении сопоставления или модели можно временно повторно включить проверку для проверки изменений.
Обратите внимание, что улучшения производительности были сделаны в конструкторе Entity Framework для Entity Framework 6, а стоимость проверки на сборку значительно ниже, чем в предыдущих версиях конструктора.
Кэширование в Entity Framework - раздел 3
Entity Framework имеет следующие формы кэширования:
- Кэширование объектов — ОбъектStateManager, встроенный в экземпляр ObjectContext, отслеживает объекты, полученные с помощью этого экземпляра. Это также называется кэшем первого уровня.
- Кэширование плана запросов — повторное использование сформированной команды для хранения при выполнении запроса более одного раза.
- Кэширование метаданных — совместное использование метаданных для модели в разных соединениях с одной и той же моделью.
Помимо кэшей, предоставляемых EF из коробки, специальный поставщик данных ADO.NET, известный как оборачивающий провайдер, можно также использовать для расширения Entity Framework, добавив кэширование для результатов, полученных из базы данных, иначе называемого кэшированием второго уровня.
Кэширование объектов 3.1
По умолчанию, когда сущность возвращается в результатах запроса, ObjectContext, незадолго до того как EF ее материализует, проверяет, загружена ли сущность с тем же ключом в объект ObjectStateManager. Если сущность с теми же ключами уже присутствует в EF (Entity Framework), то она будет включена в результаты запроса. Хотя EF всё ещё будет выполнять запрос к базе данных, это поведение может значительно сократить затраты, связанные с повторной материализацией сущности.
3.1.1 Получение сущностей из кэша объектов с помощью DbContext Find
В отличие от обычного запроса, метод Find в DbSet (API, включенные впервые в EF 4.1), будет выполнять поиск в памяти, прежде чем даже выдавать запрос к базе данных. Важно отметить, что два разных экземпляра ObjectContext будут иметь два разных экземпляра ObjectStateManager, что означает, что они имеют отдельные кэши объектов.
Поиск использует значение первичного ключа для попытки найти сущность, отслеживаемую контекстом. Если сущность не находится в контексте, запрос будет выполнен и вычисляется в базе данных, а значение NULL возвращается, если сущность не найдена в контексте или в базе данных. Обратите внимание, что Find также возвращает сущности, добавленные в контекст, но еще не сохранены в базе данных.
При использовании find необходимо учитывать производительность. Вызовы к этому методу по умолчанию активируют проверку кэша объектов, чтобы обнаружить изменения, которые по-прежнему ожидают фиксации в базе данных. Этот процесс может быть очень дорогим, если в кэше объектов есть очень большое количество объектов или в большом графе объектов, добавляемом в кэш объектов, но его также можно отключить. В некоторых случаях вы можете заметить разницу в порядке величины при вызове метода Find, если отключите автоматическое обнаружение изменений. Тем не менее, второй порядок величины воспринимается, когда объект фактически находится в кэше и когда объект должен быть извлечен из базы данных. Ниже приведен пример графа с измерениями, сделанными с помощью некоторых микробнчмарков, выраженных в миллисекундах, с нагрузкой в 5000 сущностей:
Пример поиска без автоматического обнаружения изменений:
context.Configuration.AutoDetectChangesEnabled = false;
var product = context.Products.Find(productId);
context.Configuration.AutoDetectChangesEnabled = true;
...
Что необходимо учитывать при использовании метода Find:
- Если объект не находится в кэше, преимущества Find отрицаются, но синтаксис по-прежнему проще, чем запрос по ключу.
- Если автоматическое обнаружение изменений включено, стоимость метода Find может увеличиться на один порядок величины или даже больше в зависимости от сложности модели и объема сущностей в кэше объектов.
Кроме того, помните, что Find возвращает только требуемую сущность, и она не загружает связанные сущности автоматически, если они еще не находятся в кэше объектов. Если необходимо получить связанные сущности, можно использовать запрос по ключу с активной загрузкой. Дополнительные сведения см. в разделе 8.1 Отложенная загрузка и ранняя загрузка.
3.1.2 Проблемы с производительностью, когда кэш объектов имеет множество сущностей
Кэш объектов помогает повысить общую скорость реагирования Entity Framework. Однако если кэш объектов имеет очень большое количество загруженных сущностей, он может повлиять на определенные операции, такие как Добавление, Удаление, Поиск, Запись, SaveChanges и многое другое. В частности, операции, запускающие вызов DetectChanges, будут отрицательно влиять на очень большие кэши объектов. DetectChanges синхронизирует граф объектов с диспетчером состояний объекта, а его производительность определяется непосредственно размером графа объектов. Дополнительные сведения о DetectChanges см. в разделе Отслеживание изменений в сущностях POCO.
При использовании Entity Framework 6 разработчики могут вызывать AddRange и RemoveRange непосредственно в DbSet, а не выполнять итерацию в коллекции и вызывать Add для каждого экземпляра. Преимущество использования методов диапазона заключается в том, что стоимость DetectChanges оплачивается только один раз для всего набора сущностей, а не один раз для каждой добавленной сущности.
Кэширование плана запросов 3.2
При первом выполнении запроса он проходит через внутренний компилятор плана для перевода концептуального запроса в команду хранилища (например, T-SQL, который выполняется при выполнении с SQL Server). Если кэширование плана запросов включено, при следующем выполнении запроса команда хранилища извлекается непосредственно из кэша плана запроса, обходя компилятор плана.
Кэш плана запросов разделяется между экземплярами ObjectContext в одном домене приложения. Нет необходимости удерживать экземпляр ObjectContext, чтобы воспользоваться кэшированием плана запросов.
3.2.1 Некоторые заметки о кэшировании плана запросов
- Кэш плана запросов используется для всех типов запросов: Entity SQL, LINQ to Entities и CompiledQuery.
- По умолчанию кэширование плана запросов включено для запросов Entity SQL, выполняемых через EntityCommand или ObjectQuery. Он также включен по умолчанию для запросов LINQ to Entities в Entity Framework на .NET 4.5 и в Entity Framework 6.
- Кэширование плана запросов можно отключить, установив для свойства EnablePlanCaching (в EntityCommand или ObjectQuery) значение false. Рассмотрим пример.
var query = from customer in context.Customer
where customer.CustomerId == id
select new
{
customer.CustomerId,
customer.Name
};
ObjectQuery oQuery = query as ObjectQuery;
oQuery.EnablePlanCaching = false;
- Для параметризованных запросов изменение значения параметра по-прежнему попадет в кэшированный запрос. Но изменение аспектов параметра (например, размер, точность или масштабирование) приведет к другой записи в кэше.
- При использовании Entity SQL строка запроса является частью ключа. Любое изменение запроса вообще приведет к различным записям кэша, даже если запросы функционально эквивалентны. Сюда входят изменения в регистр или пробелы.
- При использовании LINQ запрос обрабатывается для создания части ключа. Поэтому изменение выражения LINQ приведет к созданию другого ключа.
- Другие технические ограничения могут применяться; Дополнительные сведения см. в разделе "Автокомпилированные запросы".
Алгоритм вытеснения кэша 3.2.2
Понимание того, как работает внутренний алгоритм, поможет определить, когда следует включить или отключить кэширование плана запросов. Алгоритм очистки выглядит следующим образом:
- После того как кэш содержит заданное количество записей (800), мы начинаем таймер, который периодически (один раз в минуту) выполняет очистку кэша.
- Во время очистки кэша записи удаляются из кэша на основе LFRU (наименее часто и недавно используемых). Этот алгоритм учитывает количество попаданий и возраст при принятии решения о том, какие записи удаляются.
- В конце каждой очистки кэша в нём снова содержится 800 записей.
Все записи кэша обрабатываются одинаково при определении элементов, которые необходимо вытеснить. Это означает, что операция хранения для составного запроса имеет те же шансы вытеснения, как и операция хранения для запроса Entity SQL.
Обратите внимание, что таймер очистки кэша запускается, когда в кэше находится 800 сущностей, но кэш очищается только через 60 секунд после запуска этого таймера. Это означает, что до 60 секунд кэш может увеличиться до довольно большого размера.
3.2.3 Тестовые метрики, демонстрирующие производительность кэширования плана запросов
Чтобы продемонстрировать влияние кэширования плана запросов на производительность приложения, мы выполнили тест, в котором мы выполнили ряд запросов Entity SQL в модели Navision. Ознакомьтесь с приложением для описания модели Navision и типов выполненных запросов. В этом тесте мы сначала итерируем по списку запросов и выполняем каждый из них один раз, чтобы добавить их в кэш (если кэширование включено). Этот шаг не имеет ограничения по времени. Далее мы задерживаем основной поток на более чем 60 секунд, чтобы разрешить очистку кэша; Наконец, мы вторично перебираем список для выполнения запросов из кэша. Кроме того, кэш планов SQL Server очищается перед выполнением каждого набора запросов, чтобы время, которое мы получили точно, отражает преимущество, предоставленное кэшом плана запросов.
Результаты теста 3.2.3.1
| Тест | EF5 нет кэша | Кэшированный EF5 | EF6 без кэша | Кэшированный EF6 |
|---|---|---|---|---|
| Перечисление всех 18723 запросов | 124 | 125.4 | 124.3 | 125.3 |
| Избегайте очистки (только первые 800 запросов, независимо от сложности) | 41.7 | 5.5 | 40.5 | 5,4 |
| Только запросы AggregatingSubtotals (всего 178), что позволяет избежать полной проверки). | 39.5 | 4,5 | 38.1 | 4,6 |
Все значения времени указаны в секундах.
Мораль — при выполнении большого количества отдельных запросов (например, динамически созданных запросов) кэширование не помогает, а результирующая очистка кэша может помешать запросам, которые могли бы извлечь наибольшую выгоду из кэширования планов, использовать его.
Запросы AggregatingSubtotals являются наиболее сложными из проверенных запросов. Как ожидается, чем более сложный запрос, тем больше преимуществ вы увидите из кэширования плана запросов.
Поскольку компилированный запрос действительно является запросом LINQ с планом, сохраненным в кэше, сравнение компилированного запроса и эквивалентного запроса на Entity SQL должно давать аналогичные результаты. На самом деле, если у приложения много динамических запросов Entity SQL, заполнение кэша этими запросами также может привести к "раскомпиляции" CompiledQueries, когда они выгружаются из кэша. В этом сценарии производительность может быть улучшена, отключив кэширование для динамических запросов, чтобы отдать приоритет CompiledQueries. Лучше, конечно, будет переписать приложение для использования параметризованных запросов вместо динамических запросов.
3.3 Использование CompiledQuery для повышения производительности с LINQ-запросами
Наши тесты показывают, что использование CompiledQuery может принести преимущество 7% по сравнению с автоматически компилируемыми запросами LINQ; это означает, что вы будете тратить 7% меньше времени выполнения кода из стека Entity Framework; Это не означает, что ваше приложение будет 7% быстрее. Как правило, стоимость написания и обслуживания объектов CompiledQuery в EF 5.0 может не стоить проблем при сравнении с преимуществами. Результаты могут отличаться, поэтому воспользуйтесь этой возможностью, если вашему проекту требуется дополнительное усилие. Обратите внимание, что предварительно скомпилированные запросы совместимы только с моделями, производными от ObjectContext, и несовместимы с моделями, производными от DbContext.
Дополнительные сведения о создании и вызове CompiledQuery см. в разделе "Скомпилированные запросы" (LINQ to Entities).
При использовании CompiledQuery необходимо учитывать два фактора, включая требование использования статических экземпляров, а также проблемы с их совместимостью. Ниже приведено подробное описание этих двух соображений.
3.3.1 Использование статических экземпляров CompiledQuery
Так как компиляция запроса LINQ является трудоемким процессом, мы не хотим делать это каждый раз, когда нам нужно получить данные из базы данных. Экземпляры CompiledQuery позволяют скомпилировать один раз и выполнять несколько раз, но будьте осторожны и следите, чтобы использовать один и тот же экземпляр CompiledQuery каждый раз вместо компиляции его вновь. Требуется использование статических элементов для хранения экземпляров CompiledQuery; в противном случае вы не увидите никакого преимущества.
Например, предположим, что на странице имеется следующий текст метода для обработки отображения продуктов для выбранной категории:
// Warning: this is the wrong way of using CompiledQuery
using (NorthwindEntities context = new NorthwindEntities())
{
string selectedCategory = this.categoriesList.SelectedValue;
var productsForCategory = CompiledQuery.Compile<NorthwindEntities, string, IQueryable<Product>>(
(NorthwindEntities nwnd, string category) =>
nwnd.Products.Where(p => p.Category.CategoryName == category)
);
this.productsGrid.DataSource = productsForCategory.Invoke(context, selectedCategory).ToList();
this.productsGrid.DataBind();
}
this.productsGrid.Visible = true;
В этом случае вы создадите новый экземпляр CompiledQuery при каждом вызове метода. Вместо того чтобы получать преимущества в производительности, извлекая команду хранилища из кэша плана запросов, CompiledQuery проходит через компилятор плана каждый раз при создании нового экземпляра. Фактически, вы будете загрязнять кэш плана запросов, создавая новую запись CompiledQuery каждый раз при вызове метода.
Вместо этого необходимо создать статический экземпляр скомпилированного запроса, поэтому при каждом вызове метода вызывается один и тот же скомпилированный запрос. Одним из способов сделать это является добавление экземпляра CompiledQuery в качестве члена контекста объекта. Затем можно упростить задачу, получая доступ к CompiledQuery через вспомогательный метод.
public partial class NorthwindEntities : ObjectContext
{
private static readonly Func<NorthwindEntities, string, IEnumerable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
(NorthwindEntities context, string categoryName) =>
context.Products.Where(p => p.Category.CategoryName == categoryName)
);
public IEnumerable<Product> GetProductsForCategory(string categoryName)
{
return productsForCategoryCQ.Invoke(this, categoryName).ToList();
}
Этот вспомогательный метод будет вызван следующим образом:
this.productsGrid.DataSource = context.GetProductsForCategory(selectedCategory);
3.3.2 Создание на основе CompiledQuery
Возможность композировать над любым запросом LINQ чрезвычайно полезна, для этого просто вызовите метод после IQueryable, например Skip() или Count(). Однако при этом по существу возвращается новый объект IQueryable. Хотя технически ничто не мешает вам добавлять операции к CompiledQuery, это приведет к созданию нового объекта IQueryable, который должен снова пройти через компилятор плана.
Некоторые компоненты используют составные объекты IQueryable для включения расширенных функциональных возможностей. Например, элемент GridView ASP.NET может быть привязан к объекту IQueryable через свойство SelectMethod. Затем GridView будет оперировать с этим объектом IQueryable для обеспечения сортировки и разбиения на страницы в модели данных. Как видно, использование скомпилированного запроса для GridView не использует непосредственно скомпилированный запрос, а создает новый автоматически компилированный запрос.
Одно место, в котором может возникнуть проблема, заключается в добавлении прогрессивных фильтров в запрос. Например, предположим, что у вас была страница "Клиенты" с несколькими раскрывающимся списками для необязательных фильтров (например, Country и OrdersCount). Эти фильтры можно составить по результатам запроса управляемого IQueryable составного запроса, но это приведет к тому, что новый запрос будет проходить через компилятор плана каждый раз при его выполнении.
using (NorthwindEntities context = new NorthwindEntities())
{
IQueryable<Customer> myCustomers = context.InvokeCustomersForEmployee();
if (this.orderCountFilterList.SelectedItem.Value != defaultFilterText)
{
int orderCount = int.Parse(orderCountFilterList.SelectedValue);
myCustomers = myCustomers.Where(c => c.Orders.Count > orderCount);
}
if (this.countryFilterList.SelectedItem.Value != defaultFilterText)
{
myCustomers = myCustomers.Where(c => c.Address.Country == countryFilterList.SelectedValue);
}
this.customersGrid.DataSource = myCustomers;
this.customersGrid.DataBind();
}
Чтобы избежать повторной компиляции, можно перезаписать скомпилированныйQuery, чтобы принять во внимание возможные фильтры:
private static readonly Func<NorthwindEntities, int, int?, string, IQueryable<Customer>> customersForEmployeeWithFiltersCQ = CompiledQuery.Compile(
(NorthwindEntities context, int empId, int? countFilter, string countryFilter) =>
context.Customers.Where(c => c.Orders.Any(o => o.EmployeeID == empId))
.Where(c => countFilter.HasValue == false || c.Orders.Count > countFilter)
.Where(c => countryFilter == null || c.Address.Country == countryFilter)
);
Что будет вызываться в пользовательском интерфейсе, например:
using (NorthwindEntities context = new NorthwindEntities())
{
int? countFilter = (this.orderCountFilterList.SelectedIndex == 0) ?
(int?)null :
int.Parse(this.orderCountFilterList.SelectedValue);
string countryFilter = (this.countryFilterList.SelectedIndex == 0) ?
null :
this.countryFilterList.SelectedValue;
IQueryable<Customer> myCustomers = context.InvokeCustomersForEmployeeWithFilters(
countFilter, countryFilter);
this.customersGrid.DataSource = myCustomers;
this.customersGrid.DataBind();
}
Торговый компромисс заключается в том, что сгенерированная команда хранилища всегда будет содержать фильтры с проверками на NULL, но они должны быть достаточно простыми для оптимизации сервером базы данных.
...
WHERE ((0 = (CASE WHEN (@p__linq__1 IS NOT NULL) THEN cast(1 as bit) WHEN (@p__linq__1 IS NULL) THEN cast(0 as bit) END)) OR ([Project3].[C2] > @p__linq__2)) AND (@p__linq__3 IS NULL OR [Project3].[Country] = @p__linq__4)
Кэширование метаданных 3.4
Entity Framework также поддерживает кэширование метаданных. Это, по сути, кэширование информации о типах и информации о сопоставлении типов с базой данных для разных подключений к одной и той же модели. Кэш метаданных является уникальным для каждого домена приложения.
Алгоритм кэширования метаданных 3.4.1
Сведения о метаданных для модели хранятся в элементе ItemCollection для каждого EntityConnection.
- В качестве боковой заметки существуют различные объекты ItemCollection для разных частей модели. Например, StoreItemCollections содержит сведения о модели базы данных; ObjectItemCollection содержит сведения о модели данных; EdmItemCollection содержит сведения о концептуальной модели.
Если два подключения используют одну и ту же строку подключения, они будут совместно использовать один и тот же экземпляр ItemCollection.
Функционально эквивалентные, но текстуально разные строки подключения могут привести к разным кэшам метаданных. Мы маркеризируем строки подключения, поэтому просто изменение порядка маркеров должно привести к общим метаданным. Но две строки подключения, которые кажутся функционально одинаковыми, могут не оцениваться как идентичные после токенизации.
ItemCollection периодически проверяется на использование. Если определено, что рабочая область недавно не была доступна, она будет помечена для очистки при следующем очистке кэша.
Простое создание EntityConnection приведет к созданию кэша метаданных (хотя коллекции элементов в нем не будут инициализированы до открытия подключения). Эта рабочая область останется в памяти, пока алгоритм кэширования не определяет, что он не используется.
Группа консультантов по клиентам написала запись в блоге, описывающую удержание ссылки на ItemCollection для избежания «устаревания» при использовании больших моделей: <https://learn.microsoft.com/archive/blogs/appfabriccat/holding-a-reference-to-the-ef-metadataworkspace-for-wcf-services>.
3.4.2 Связь между кэшированием метаданных и кэшированием плана запросов
Экземпляр кэша плана запросов находится в коллекции элементов типов хранилища пространства метаданных. Это означает, что кэшированные команды хранилища будут использоваться для запросов к любому контексту, созданному с помощью заданного пространства MetadataWorkspace. Это также означает, что если у вас есть две строки подключений, которые немного отличаются и не совпадают после маркеризации, у вас будут разные экземпляры кэша плана запросов.
Кэширование результатов 3.5
При кэшировании результатов (также называемом "кэшированием второго уровня"), результаты запросов сохраняются в локальном кэше. При выполнении запроса сначала проверяете, доступны ли результаты на месте, прежде чем выполнять запрос в хранилище. Хотя кэширование результатов не поддерживается непосредственно Entity Framework, можно добавить кэш второго уровня с помощью поставщика упаковки. Примером поставщика для обёртки с кэшем второго уровня является Entity Framework Second Level Cache Alachisoft на основе NCache.
Эта реализация кэширования второго уровня — это внедренная функциональность, которая выполняется после вычисления выражения LINQ (и funcletized), а план выполнения запроса вычисляется или извлекается из кэша первого уровня. Кэш второго уровня будет хранить только необработанные результаты базы данных, поэтому конвейер материализации по-прежнему выполняется после этого.
3.5.1 Дополнительные ссылки на кэширование результатов с поставщиком упаковки
- Джули Лерман написала статью MSDN о втором уровне кэширования в Entity Framework и Windows Azure, которая включает в себя обновление примера поставщика оберток для использования системы кэширования Windows Server AppFabric: https://msdn.microsoft.com/magazine/hh394143.aspx
- Если вы работаете с Entity Framework 5, в блоге команды есть запись, описывающая, как работать с поставщиком кэширования для Entity Framework 5: <https://learn.microsoft.com/archive/blogs/adonet/ef-caching-with-jarek-kowalskis-provider> Он также включает шаблон T4, помогающий автоматизировать добавление кэширования на уровне 2-го уровня в проект.
4 автоматически компилированных запросов
При выполнении запроса к базе данных с помощью Entity Framework необходимо выполнить ряд шагов, прежде чем фактически материализовать результаты; одним из таких шагов является компиляция запросов. Запросы Entity SQL, как известно, имеют хорошую производительность, так как они автоматически кэшируются, поэтому второй или третий раз, когда выполняется тот же запрос, он может пропустить компилятор плана и использовать кэшированный план.
Entity Framework 5 также представила автоматическое кэширование для запросов LINQ to Entity. В предыдущих версиях Entity Framework создание CompiledQuery для улучшения производительности было распространенной практикой, так как это делало ваш запрос LINQ to Entities кэшируемым. Так как кэширование выполняется автоматически без использования компилятораQuery, мы называем эту функцию "автоматически компилируемыми запросами". Дополнительные сведения о кэше плана запросов и его механике см. в разделе "Кэширование плана запросов".
Entity Framework обнаруживает, когда запрос требует повторной компиляции, и делает это при вызове запроса даже в том случае, если он был скомпилирован раньше. Распространенные условия, которые приводят к повторной компиляции запроса:
- Изменение параметра 'MergeOption' для вашего запроса. Кэшированный запрос не будет использоваться, вместо этого компилятор плана будет выполняться снова, и только что созданный план кэшируется.
- Изменение значения ContextOptions.UseCSharpNullComparisonBehavior. Вы получаете тот же эффект, что и изменение MergeOption.
Другие условия могут запретить запросу использовать кэш. Ниже приведены распространенные примеры.
- Использование IEnumerable<T>. <>Содержит (T-значение).
- Использование функций, которые создают запросы с константами.
- Использование свойств не сопоставленного объекта.
- Связывание запроса с другим запросом, который требуется перекомпилировать.
4.1 С помощью IEnumerable<T>. Содержит<значение T>(T)
Entity Framework не кэширует запросы, вызывающие IEnumerable<T>. Содержит<значение T>(T) для коллекции в памяти, так как значения коллекции считаются переменными. Следующий пример запроса не будет кэширован, поэтому он всегда будет обрабатываться компилятором плана:
int[] ids = new int[10000];
...
using (var context = new MyContext())
{
var query = context.MyEntities
.Where(entity => ids.Contains(entity.Id));
var results = query.ToList();
...
}
Обратите внимание, что размер IEnumerable, для которого выполняется Contains, определяет, тем быстрее или медленнее будет компилироваться ваш запрос. Производительность может значительно снизиться при использовании больших коллекций, таких как показанная в приведенном выше примере.
Entity Framework 6 содержит оптимизации в способе работы IEnumerable<T>.Contains<(T value) при выполнении запросов. Сгенерированный код SQL создается значительно быстрее и более читаем, а в большинстве случаев также быстрее выполняется на сервере.
4.2 Использование функций, которые создают запросы с константами
Операторы LINQ skip(), Take(), Contains() и DefautIfEmpty() не создают запросы SQL с параметрами, а помещают значения, передаваемые им в виде констант. Из-за этого запросы, которые могут быть идентичными, в конечном итоге загрязняют кэш плана запросов, как на стеке EF, так и на сервере базы данных, и не используются повторно, если только те же константы не используются в последующем выполнении запроса. Рассмотрим пример.
var id = 10;
...
using (var context = new MyContext())
{
var query = context.MyEntities.Select(entity => entity.Id).Contains(id);
var results = query.ToList();
...
}
В этом примере каждый раз, когда этот запрос выполняется с другим значением для идентификатора, запрос будет скомпилирован в новый план.
В частности, обратите внимание на использование "Skip" и "Take" при выполнении постраничной разбивки. В EF6 эти методы имеют лямбда-перегрузку, которая эффективно делает кэшированный план запросов повторно используемым, так как EF может записывать переменные, передаваемые этим методам, и переводить их в SQLparameters. Это также помогает обеспечить очистку кэша, так как в противном случае каждый запрос с другой константой для Skip и Take получит собственную запись кэша плана запросов.
Рассмотрим следующий код, который является неоптимальным, но предназначен только для примера этого класса запросов:
var customers = context.Customers.OrderBy(c => c.LastName);
for (var i = 0; i < count; ++i)
{
var currentCustomer = customers.Skip(i).FirstOrDefault();
ProcessCustomer(currentCustomer);
}
Более быстрая версия этого же кода будет включать вызов Skip с лямбда-кодом:
var customers = context.Customers.OrderBy(c => c.LastName);
for (var i = 0; i < count; ++i)
{
var currentCustomer = customers.Skip(() => i).FirstOrDefault();
ProcessCustomer(currentCustomer);
}
Второй фрагмент кода может выполняться до 11% быстрее, так как один и тот же план запросов используется каждый раз при выполнении запроса, что позволяет сэкономить время ЦП и избежать загрязнения кэша запросов. Кроме того, так как параметр для Skip находится в замыкании, код теперь может выглядеть следующим образом:
var i = 0;
var skippyCustomers = context.Customers.OrderBy(c => c.LastName).Skip(() => i);
for (; i < count; ++i)
{
var currentCustomer = skippyCustomers.FirstOrDefault();
ProcessCustomer(currentCustomer);
}
4.3 Использование свойств не сопоставленного объекта
Если запрос использует свойства не сопоставленного типа объекта в качестве параметра, запрос не будет кэширован. Рассмотрим пример.
using (var context = new MyContext())
{
var myObject = new NonMappedType();
var query = from entity in context.MyEntities
where entity.Name.StartsWith(myObject.MyProperty)
select entity;
var results = query.ToList();
...
}
В этом примере предположим, что класс NonMappedType не является частью модели сущности. Этот запрос можно легко изменить, чтобы не использовать не сопоставленный тип и вместо этого использовать локальную переменную в качестве параметра для запроса:
using (var context = new MyContext())
{
var myObject = new NonMappedType();
var myValue = myObject.MyProperty;
var query = from entity in context.MyEntities
where entity.Name.StartsWith(myValue)
select entity;
var results = query.ToList();
...
}
В этом случае запрос сможет кэшироваться и воспользоваться кэшем плана запросов.
4.4 Связывание с запросами, требующими повторной компиляции
После приведенного выше примера, если у вас есть второй запрос, основанный на запросе, который должен быть перекомпилирован, весь второй запрос также будет перекомпилирован. Ниже приведен пример для иллюстрации этого сценария:
int[] ids = new int[10000];
...
using (var context = new MyContext())
{
var firstQuery = from entity in context.MyEntities
where ids.Contains(entity.Id)
select entity;
var secondQuery = from entity in context.MyEntities
where firstQuery.Any(otherEntity => otherEntity.Id == entity.Id)
select entity;
var results = secondQuery.ToList();
...
}
Пример является универсальным, но он иллюстрирует, как связывание с firstQuery приводит к невозможности кэширования secondQuery. Если бы firstQuery не был запросом, требующим повторной компиляции, то второйQuery был кэширован.
5 Запросов NoTracking
5.1 Отключение отслеживания изменений для уменьшения затрат на управление состоянием
Если вы находитесь в сценарии только для чтения и хотите избежать затрат на загрузку объектов в ObjectStateManager, вы можете выдавать запросы "Без отслеживания". Отслеживание изменений можно отключить на уровне запроса.
Обратите внимание, что при отключении отслеживания изменений вы фактически отключаете кэш объектов. При запросе сущности невозможно избежать материализации путем извлечения ранее материализованных результатов запроса из ObjectStateManager. Если вы неоднократно запрашиваете одни и те же сущности в одном контексте, возможно, вы на самом деле увидите преимущество производительности при включении отслеживания изменений.
При выполнении запросов с использованием ObjectContext экземпляры ObjectQuery и ObjectSet будут запоминать значение MergeOption после его установки, а запросы, основания на них, наследуют соответствующее значение MergeOption родительского запроса. При использовании DbContext отслеживание можно отключить, вызвав модификатор AsNoTracking() в DbSet.
5.1.1 Отключение отслеживания изменений для запроса при использовании DbContext
Вы можете переключить режим запроса на NoTracking, применив вызов метода AsNoTracking() в запросе. В отличие от ObjectQuery, классы DbSet и DbQuery в API DbContext не имеют изменяемого свойства для MergeOption.
var productsForCategory = from p in context.Products.AsNoTracking()
where p.Category.CategoryName == selectedCategory
select p;
5.1.2 Отключение отслеживания изменений на уровне запроса с помощью ObjectContext
var productsForCategory = from p in context.Products
where p.Category.CategoryName == selectedCategory
select p;
((ObjectQuery)productsForCategory).MergeOption = MergeOption.NoTracking;
5.1.3 Отключение отслеживания изменений для всего набора сущностей с помощью ObjectContext
context.Products.MergeOption = MergeOption.NoTracking;
var productsForCategory = from p in context.Products
where p.Category.CategoryName == selectedCategory
select p;
5.2 Тестовые метрики, демонстрирующие преимущество производительности запросов NoTracking
В этом тесте мы рассматриваем стоимость заполнения ObjectStateManager, сравнивая запросы с отслеживанием (Tracking) и без отслеживания (NoTracking) для модели Navision. Ознакомьтесь с приложением для описания модели Navision и типов выполненных запросов. В этом тесте мы выполняем итерацию по списку запросов и выполняем каждый из них один раз. Мы выполнили два варианта теста, один раз с запросами NoTracking и один раз с параметром слияния по умолчанию "AppendOnly". Мы выполняли каждый вариант 3 раза и берем среднее значение запусков. Между тестами мы очищаем кэш запросов в SQL Server и сжимаем tempdb, выполнив следующие команды:
- DBCC DROPCLEANBUFFERS
- DBCC FREEPROCCACHE
- DBCC SHRINKDATABASE (tempdb, 0)
Результаты теста, медиана по 3 запускам:
| НЕТ ОТСЛЕЖИВАНИЯ — РАБОЧИЙ НАБОР | НЕТ ОТСЛЕЖИВАНИЯ — ВРЕМЯ | ТОЛЬКО ДОБАВЛЕНИЕ — РАБОЧИЙ НАБОР ДАННЫХ | ТОЛЬКО ДОБАВЛЕНИЕ — ВРЕМЯ | |
|---|---|---|---|---|
| Entity Framework 5 | 460361728 | 1163536 мс | 596545536 | 1273042 мс |
| Entity Framework 6 | 647127040 | 190228 мс | 832798720 | 195521 мс |
Entity Framework 5 будет иметь меньше памяти в конце выполнения, чем Entity Framework 6. Дополнительная память, потребляемая Entity Framework 6, является результатом дополнительных структур памяти и кода, которые обеспечивают новые функции и более высокую производительность.
При использовании ObjectStateManager также существует четкое различие в занимаемом объеме памяти. Entity Framework 5 увеличил масштабы использования на 30% при отслеживании всех сущностей, которые мы материализовали из базы данных. Entity Framework 6 увеличил свой объем на 28% при этом.
С точки зрения времени, Entity Framework 6 опережает Entity Framework 5 в этом тесте с большим отрывом. Entity Framework 6 завершил тест примерно в 16% времени, потребляемого Entity Framework 5. Кроме того, Entity Framework 5 занимает 9% больше времени для завершения использования ObjectStateManager. При сравнении, Entity Framework 6 требует на 3% больше времени при использовании ObjectStateManager.
6 Варианты выполнения запросов
Entity Framework предлагает несколько различных способов запроса. Мы рассмотрим следующие варианты, сравните плюсы и минусы каждого из них и рассмотрим их характеристики производительности:
- LINQ to Entities.
- Отслеживание отключено для LINQ to Entities.
- Entity SQL для ObjectQuery.
- Использование Entity SQL через EntityCommand.
- ExecuteStoreQuery.
- SqlQuery.
- Скомпилированный запрос.
6.1 запросы LINQ to Entities
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
Преимущества
- Подходит для операций CUD.
- Полностью материализованные объекты.
- Самый простой для записи с помощью синтаксиса, встроенного в язык программирования.
- Хорошая производительность.
Недостатки
- Некоторые технические ограничения, такие как:
- Шаблоны, использующие DefaultIfEmpty для запросов OUTER JOIN, приводят к более сложным запросам, чем простые инструкции OUTER JOIN в Entity SQL.
- Вы по-прежнему не можете использовать LIKE с общим сопоставлением шаблонов.
6.2 Запросы LINQ to Entities без отслеживания
Если контекст производится от ObjectContext:
context.Products.MergeOption = MergeOption.NoTracking;
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
Когда контекст наследует DbContext:
var q = context.Products.AsNoTracking()
.Where(p => p.Category.CategoryName == "Beverages");
Преимущества
- Улучшена производительность по сравнению с обычными запросами LINQ.
- Полностью материализованные объекты.
- Самый простой для записи с помощью синтаксиса, встроенного в язык программирования.
Недостатки
- Не подходит для операций CUD.
- Некоторые технические ограничения, такие как:
- Шаблоны, использующие DefaultIfEmpty для запросов OUTER JOIN, приводят к более сложным запросам, чем простые инструкции OUTER JOIN в Entity SQL.
- Вы по-прежнему не можете использовать LIKE для общего сопоставления шаблонов.
Обратите внимание, что запросы, проецирующие скалярные свойства, не отслеживаются, даже если не указан режим NoTracking. Рассмотрим пример.
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages").Select(p => new { p.ProductName });
Этот конкретный запрос не указывает явно на отсутствие отслеживания (NoTracking), но поскольку он не материализует тип, который известен диспетчеру состояния объектов, результат материализации не отслеживается.
6.3 Entity SQL на основе ObjectQuery
ObjectQuery<Product> products = context.Products.Where("it.Category.CategoryName = 'Beverages'");
Преимущества
- Подходит для операций CUD.
- Полностью материализованные объекты.
- Поддерживает кэширование плана запросов.
Недостатки
- Включает текстовые строки запроса, которые более подвержены ошибке пользователя, чем конструкции запросов, встроенные в язык.
6.4 Entity SQL с помощью команды Entity
EntityCommand cmd = eConn.CreateCommand();
cmd.CommandText = "Select p From NorthwindEntities.Products As p Where p.Category.CategoryName = 'Beverages'";
using (EntityDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
{
while (reader.Read())
{
// manually 'materialize' the product
}
}
Преимущества
- Поддерживает кэширование плана запросов в .NET 4.0 (кэширование планов поддерживается всеми другими типами запросов в .NET 4.5).
Недостатки
- Включает текстовые строки запроса, которые более подвержены ошибке пользователя, чем конструкции запросов, встроенные в язык.
- Не подходит для операций CUD.
- Результаты не материализуются автоматически и должны быть прочитаны из средства чтения данных.
6.5 SqlQuery и ExecuteStoreQuery
SqlQuery в базе данных:
// use this to obtain entities and not track them
var q1 = context.Database.SqlQuery<Product>("select * from products");
SqlQuery в DbSet:
// use this to obtain entities and have them tracked
var q2 = context.Products.SqlQuery("select * from products");
ExecuteStoreQuery:
var beverages = context.ExecuteStoreQuery<Product>(
@" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued, P.DiscontinuedDate
FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
WHERE (C.CategoryName = 'Beverages')"
);
Преимущества
- Как правило, наивысшая скорость выполнения, поскольку компиляция плана обходится.
- Полностью материализованные объекты.
- Подходит для операций CUD при использовании из DbSet.
Недостатки
- Запрос является текстовым и подверженным ошибкам.
- Запрос привязан к определенной серверной части с помощью семантики хранилища вместо концептуальной семантики.
- Когда наследование присутствует, ручной запрос должен учитывать условия сопоставления для запрошенного типа.
6.6 Скомпилированный Запрос
private static readonly Func<NorthwindEntities, string, IQueryable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
(NorthwindEntities context, string categoryName) =>
context.Products.Where(p => p.Category.CategoryName == categoryName)
);
…
var q = context.InvokeProductsForCategoryCQ("Beverages");
Преимущества
- Обеспечивает до 7% улучшения производительности по сравнению с обычными запросами LINQ.
- Полностью материализованные объекты.
- Подходит для операций CUD.
Недостатки
- Повышенная сложность и затраты на программирование.
- Улучшение производительности теряется при создании на основе скомпилированного запроса.
- Некоторые запросы LINQ не могут быть записаны как скомпилированный запрос, например проекции анонимных типов.
6.7 Сравнение производительности различных параметров запроса
Простые микробенчмарки, в которых время создания контекста не учитывалось, были протестированы. Мы измеряли запросы 5000 раз для набора не кэшированных сущностей в управляемой среде. Эти числа следует принимать с осторожностью: они не отражают фактические данные, созданные приложением, а представляют собой очень точное измерение различий в производительности при прямом сравнении различных вариантов запросов, исключая затраты на создание нового контекста.
| EF | Тест | Время (мс) | Память |
|---|---|---|---|
| EF5 | ObjectContext ESQL | 2414 | 38801408 |
| EF5 | Запрос ObjectContext Linq | 2692 | 38277120 |
| EF5 | Запросы Linq в DbContext без отслеживания | 2818 | 41840640 |
| EF5 | Запрос LINQ с использованием DbContext | 2930 | 41771008 |
| EF5 | Объектный контекст без отслеживания Linq-запроса | 3013 | 38412288 |
| EF6 | ObjectContext ESQL | 2059 | 46039040 |
| EF6 | Linq-запрос ObjectContext | 3074 | 45248512 |
| EF6 | DbContext Linq Query без отслеживания | 3125 | 47575040 |
| EF6 | Запрос LINQ с использованием DbContext | 3420 | 47652864 |
| EF6 | Объектный контекст без отслеживания Linq-запроса | 3593 | 45260800 |
Микробенчмарки очень чувствительны к небольшим изменениям в коде. В этом случае разница между затратами Entity Framework 5 и Entity Framework 6 обусловлена добавлением перехватов и улучшений транзакций. Эти микробенчмарки, однако, представляют собой углубленный взгляд на очень небольшой фрагмент того, что выполняет Entity Framework. Реальные сценарии теплых запросов не должны видеть регрессию производительности при обновлении с Entity Framework 5 до Entity Framework 6.
Чтобы сравнить реальную производительность различных вариантов запроса, мы создали 5 отдельных вариантов теста, где мы используем другой вариант запроса для выбора всех продуктов, название категории которых — "Напитки". Каждая итерация включает затраты на создание контекста и стоимость материализации всех возвращаемых сущностей. 10 итераций выполняются без учета времени, после чего выполняется сумма 1000 отсчитываемых итераций. Показанные результаты — это медианные результаты, вычисленные из 5 запусков каждого теста. Дополнительные сведения см. в приложении B, в котором содержится код для теста.
| EF | Тест | Время (мс) | Память |
|---|---|---|---|
| EF5 | Команда объекта ObjectContext | 621 | 39350272 |
| EF5 | Запрос SQL DbContext к базе данных | 825 | 37519360 |
| EF5 | Запрос к универсальному хранилищу ObjectContext | 878 | 39460864 |
| EF5 | Объектный контекст без отслеживания Linq-запроса | 969 | 38293504 |
| EF5 | ObjectContext Entity Sql с помощью запроса объекта | 1089 | 38981632 |
| EF5 | Скомпилированный запрос ObjectContext | 1099 | 38682624 |
| EF5 | Запрос ObjectContext Linq | 1152 | 38178816 |
| EF5 | DbContext Linq Query без отслеживания | 1208 | 41803776 |
| EF5 | Sql запрос DbContext на DbSet | 1414 | 37982208 |
| EF5 | Запрос LINQ с использованием DbContext | 1574 | 41738240 |
| EF6 | Команда объекта ObjectContext | 480 | 47247360 |
| EF6 | Запрос к базе данных ObjectContext | 493 | 46739456 |
| EF6 | SQL-запрос с использованием DbContext к базе данных | 614 | 41607168 |
| EF6 | Объектный контекст без отслеживания Linq-запроса | 684 | 46333952 |
| EF6 | ObjectContext Entity Sql с использованием Object Query | 767 | 48865280 |
| EF6 | Скомпилированный запрос ObjectContext | 788 | 48467968 |
| EF6 | DbContext Linq Query без отслеживания | 878 | 47554560 |
| EF6 | Запрос ObjectContext Linq | 953 | 47632384 |
| EF6 | Sql запрос DbContext на DbSet | 1023 | 41992192 |
| EF6 | Запрос LINQ с использованием DbContext | 1290 | 47529984 |
Замечание
Для полноты мы включили вариант, в котором выполняется запрос Entity SQL в EntityCommand. Однако, поскольку результаты не материализуются для таких запросов, сравнение не всегда корректно. Тест включает близкое приближение к материализации, чтобы попытаться сделать сравнение более справедливым.
В этом комплексном случае Entity Framework 6 превосходит Entity Framework 5 благодаря улучшениям производительности, внесенным в несколько частей стека, включая значительно упрощенную инициализацию DbContext и более быстрый поиск в MetadataCollection<T>.
7 Рекомендации по производительности времени разработки
Стратегии наследования 7.1
Еще одно соображение производительности при использовании Entity Framework — это используемая стратегия наследования. Entity Framework поддерживает 3 основных типа наследования и их сочетания:
- Таблица по иерархии (TPH) — где каждый набор наследования сопоставляется с таблицей со столбцом дискриминатора, чтобы указать, какой тип в иерархии представлен в строке.
- Таблица на тип (TPT) — где каждый тип имеет собственную таблицу в базе данных; Дочерние таблицы определяют только столбцы, которые не содержит родительская таблица.
- Таблица на класс (TPC) — где каждый тип имеет собственную полную таблицу в базе данных; Дочерние таблицы определяют все их поля, включая те, которые определены в родительских типах.
Если в модели используется наследование TPT, создаваемые запросы будут более сложными, чем созданные с помощью других стратегий наследования, что может привести к более длительному времени выполнения в хранилище. Как правило, для создания запросов по модели TPT и материализации результирующих объектов потребуется больше времени.
См. "Рекомендации по производительности при использовании наследования TPT (таблица на тип) в Entity Framework" в блоге MSDN: <https://learn.microsoft.com/archive/blogs/adonet/performance-considerations-when-using-tpt-table-per-type-inheritance-in-the-entity-framework>.
7.1.1 Избегайте TPT в приложениях Model First или Code First
При создании модели по существующей базе данных, имеющей схему TPT, у вас нет много вариантов. Но при создании приложения с использованием Model First или Code First следует избегать наследования TPT из-за проблем с производительностью.
При использовании Model First в мастере проектирования сущностей будет создан TPT для любого наследования в модели. Если вы хотите переключиться на стратегию наследования TPH с помощью модели First, можно использовать пакет Power Pack создания базы данных Конструктора сущностей, доступный из коллекции Visual Studio ( <http://visualstudiogallery.msdn.microsoft.com/df3541c3-d833-4b65-b942-989e7ec74c87/>).
При использовании code First для настройки сопоставления модели с наследованием EF будет использовать TPH по умолчанию, поэтому все сущности в иерархии наследования будут сопоставлены с одной таблицей. Дополнительные сведения см. в разделе "Сопоставление с Fluent API" статьи "Code First in Entity Framework 4.1" в MSDN Magazine (http://msdn.microsoft.com/magazine/hh126815.aspx).
7.2 Обновление с EF4 для улучшения времени создания модели
В Entity Framework 5 и 6 доступно улучшение алгоритма, специфичное для SQL Server, для генерации уровня хранилища (SSDL) модели, а также в качестве обновления для Entity Framework 4 при установке Visual Studio 2010 с пакетом обновления 1 (SP1). Следующие результаты теста демонстрируют улучшение при создании очень большой модели, в этом случае модель Navision. Дополнительные сведения см. в приложении C.
Модель содержит 1005 наборов сущностей и 4227 наборов ассоциаций.
| Конфигурация | Разбивка времени, потребляемого |
|---|---|
| Visual Studio 2010, Entity Framework 4 | Поколение SSDL: 2 часа 27 минут Создание сопоставления: 1 секунда Поколение CSDL: 1 секунда ObjectLayer Generation: 1 секунда Генерация представлений: 2 ч 14 мин |
| Visual Studio 2010 с пакетом обновления 1 (SP1), Entity Framework 4 | Поколение SSDL: 1 секунда Создание сопоставления: 1 секунда Поколение CSDL: 1 секунда ObjectLayer Generation: 1 секунда Поколение представлений: 1 час 53 мин |
| Visual Studio 2013, Entity Framework 5 | Поколение SSDL: 1 секунда Создание сопоставления: 1 секунда Поколение CSDL: 1 секунда ObjectLayer Generation: 1 секунда Создание представления: 65 минут |
| Visual Studio 2013, Entity Framework 6 | Поколение SSDL: 1 секунда Создание сопоставления: 1 секунда Поколение CSDL: 1 секунда ObjectLayer Generation: 1 секунда Генерация представления: 28 секунд. |
Стоит отметить, что при создании SSDL нагрузка почти полностью ложится на SQL Server, в то время как машина разработчика простаивает, ожидая, когда результаты вернутся с сервера. Базы данных должны особенно ценить это улучшение. Кроме того, стоит отметить, что теперь практически все затраты на создание моделей происходят в процессе генерации представлений.
7.3 Разделение больших моделей с использованием Database First и Model First
По мере увеличения размера модели интерфейс конструктора становится захламленным и трудным в использовании. Обычно мы считаем модель с более чем 300 сущностями слишком большими, чтобы эффективно использовать конструктор. В следующей записи блога описывается несколько вариантов разделения больших моделей: <https://learn.microsoft.com/archive/blogs/adonet/working-with-large-models-in-entity-framework-part-2>
Запись была написана для первой версии Entity Framework, но шаги по-прежнему применяются.
7.4 Вопросы производительности при использовании элемента управления Entity Data Source Control
Мы видели случаи в многопоточных тестах производительности и стресс-тестах, где производительность веб-приложения с помощью Элемента управления EntityDataSource значительно ухудшается. Основная причина заключается в том, что EntityDataSource неоднократно вызывает MetadataWorkspace.LoadFromAssembly на те сборки, на которые ссылается веб-приложение, для обнаружения типов, используемых в качестве сущностей.
Решение заключается в том, чтобы назначить свойству ContextTypeName объекта EntityDataSource имя типа вашего производного класса ObjectContext. Это отключает механизм проверки всех ссылочных сборок для типов сущностей.
Задание поля ContextTypeName также предотвращает функциональную проблему, из-за которой EntityDataSource в .NET 4.0 вызывает исключение ReflectionTypeLoadException, если он не может загрузить тип из сборки с помощью рефлексии. Эта проблема устранена в .NET 4.5.
7.5 сущности POCO и прокси для отслеживания изменений
Entity Framework позволяет использовать пользовательские классы данных вместе с моделью данных без внесения изменений в сами классы данных. Это означает, что можно использовать "старые добрые" объекты CLR (POCO), например, существующие объекты предметной области, с вашей моделью данных. Эти классы данных POCO (также известные как объекты, не осведомленные о сохранении), которые сопоставляются с сущностями, определенными в модели данных, поддерживают в основном те же функции запроса, вставки, обновления и удаления, как и типы сущностей, созданные средствами инструментов модели данных сущностей.
Entity Framework также может создавать прокси-классы, производные от типов POCO, которые используются при включении таких функций, как отложенная загрузка и автоматическое отслеживание изменений в сущностях POCO. Классы POCO должны соответствовать определенным требованиям, чтобы разрешить Entity Framework использовать прокси-серверы, как описано ниже http://msdn.microsoft.com/library/dd468057.aspx.
Прокси отслеживания изменений уведомляют менеджер состояния объектов каждый раз, когда одно из свойств ваших сущностей изменяет своё значение, чтобы Entity Framework всегда знал фактическое состояние ваших сущностей. Это делается путем добавления событий уведомлений в тело методов-постановщиков ваших свойств и обработки этих событий диспетчером состояния объекта. Обратите внимание, что создание прокси-сущности обычно будет более затратным, чем создание обычной сущности POCO, из-за дополнительных событий, создаваемых Entity Framework.
Если у сущности POCO нет прокси для отслеживания изменений, изменения выявляются путем сравнения содержимого ваших сущностей с копией ранее сохраненного состояния. Это глубокое сравнение станет длительным процессом, если у вас есть много сущностей в контексте, или когда сущности имеют очень большое количество свойств, даже если ни один из них не изменился с момента последнего сравнения.
Вкратце: создание прокси-сервера для отслеживания изменений приводит к потере производительности, но отслеживание изменений поможет ускорить процесс обнаружения изменений, если у ваших сущностей много свойств или если в вашей модели много сущностей. Для сущностей с небольшим количеством свойств, где количество сущностей не увеличивается слишком много, наличие прокси-серверов отслеживания изменений может оказаться не очень полезным.
8 Загрузка связанных сущностей
8.1 Отложенная загрузка и жадная загрузка
Entity Framework предлагает несколько различных способов загрузки сущностей, связанных с целевой сущностью. Например, при запросе на продукты существуют различные способы загрузки связанных заказов в диспетчер состояний объектов. С точки зрения производительности, самый большой вопрос, который следует учитывать при загрузке связанных сущностей, использовать ли отложенную загрузку или неторопливую загрузку.
При использовании жадной загрузки связанные сущности загружаются вместе с целевым набором сущностей. Вы используете инструкцию Include в запросе, чтобы указать, какие связанные сущности необходимо добавить.
При использовании отложенной загрузки ваш первоначальный запрос получает только целевой набор сущностей. Но всякий раз, когда вы обращаетесь к свойству навигации, другой запрос выполняется на хранилище для загрузки связанной сущности.
После загрузки сущности все дальнейшие запросы для сущности будут загружаться непосредственно из диспетчера состояний объектов, независимо от того, используете ли вы отложенную загрузку или неустранимую загрузку.
8.2 Как выбрать между отложенной загрузкой и жадной загрузкой
Важно понимать разницу между отложенной загрузкой и активной загрузкой, чтобы сделать правильный выбор для вашего приложения. Это поможет оценить компромисс между несколькими запросами к базе данных и одним запросом, который может содержать большие полезные данные. Можно использовать жадную загрузку в некоторых частях вашего приложения и ленивую загрузку в других частях.
Как пример того, что происходит под капотом, предположим, вы хотите запросить список клиентов, живущих в Великобритании, а также количество их заказов.
Использование активной загрузки
using (NorthwindEntities context = new NorthwindEntities())
{
var ukCustomers = context.Customers.Include(c => c.Orders).Where(c => c.Address.Country == "UK");
var chosenCustomer = AskUserToPickCustomer(ukCustomers);
Console.WriteLine("Customer Id: {0} has {1} orders", customer.CustomerID, customer.Orders.Count);
}
Использование отложенной загрузки
using (NorthwindEntities context = new NorthwindEntities())
{
context.ContextOptions.LazyLoadingEnabled = true;
//Notice that the Include method call is missing in the query
var ukCustomers = context.Customers.Where(c => c.Address.Country == "UK");
var chosenCustomer = AskUserToPickCustomer(ukCustomers);
Console.WriteLine("Customer Id: {0} has {1} orders", customer.CustomerID, customer.Orders.Count);
}
При использовании загрузки с предварительным выполнением вы выполните один запрос, который возвращает всех клиентов и все заказы. Команда хранилища выглядит следующим образом:
SELECT
[Project1].[C1] AS [C1],
[Project1].[CustomerID] AS [CustomerID],
[Project1].[CompanyName] AS [CompanyName],
[Project1].[ContactName] AS [ContactName],
[Project1].[ContactTitle] AS [ContactTitle],
[Project1].[Address] AS [Address],
[Project1].[City] AS [City],
[Project1].[Region] AS [Region],
[Project1].[PostalCode] AS [PostalCode],
[Project1].[Country] AS [Country],
[Project1].[Phone] AS [Phone],
[Project1].[Fax] AS [Fax],
[Project1].[C2] AS [C2],
[Project1].[OrderID] AS [OrderID],
[Project1].[CustomerID1] AS [CustomerID1],
[Project1].[EmployeeID] AS [EmployeeID],
[Project1].[OrderDate] AS [OrderDate],
[Project1].[RequiredDate] AS [RequiredDate],
[Project1].[ShippedDate] AS [ShippedDate],
[Project1].[ShipVia] AS [ShipVia],
[Project1].[Freight] AS [Freight],
[Project1].[ShipName] AS [ShipName],
[Project1].[ShipAddress] AS [ShipAddress],
[Project1].[ShipCity] AS [ShipCity],
[Project1].[ShipRegion] AS [ShipRegion],
[Project1].[ShipPostalCode] AS [ShipPostalCode],
[Project1].[ShipCountry] AS [ShipCountry]
FROM ( SELECT
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[CompanyName] AS [CompanyName],
[Extent1].[ContactName] AS [ContactName],
[Extent1].[ContactTitle] AS [ContactTitle],
[Extent1].[Address] AS [Address],
[Extent1].[City] AS [City],
[Extent1].[Region] AS [Region],
[Extent1].[PostalCode] AS [PostalCode],
[Extent1].[Country] AS [Country],
[Extent1].[Phone] AS [Phone],
[Extent1].[Fax] AS [Fax],
1 AS [C1],
[Extent2].[OrderID] AS [OrderID],
[Extent2].[CustomerID] AS [CustomerID1],
[Extent2].[EmployeeID] AS [EmployeeID],
[Extent2].[OrderDate] AS [OrderDate],
[Extent2].[RequiredDate] AS [RequiredDate],
[Extent2].[ShippedDate] AS [ShippedDate],
[Extent2].[ShipVia] AS [ShipVia],
[Extent2].[Freight] AS [Freight],
[Extent2].[ShipName] AS [ShipName],
[Extent2].[ShipAddress] AS [ShipAddress],
[Extent2].[ShipCity] AS [ShipCity],
[Extent2].[ShipRegion] AS [ShipRegion],
[Extent2].[ShipPostalCode] AS [ShipPostalCode],
[Extent2].[ShipCountry] AS [ShipCountry],
CASE WHEN ([Extent2].[OrderID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
FROM [dbo].[Customers] AS [Extent1]
LEFT OUTER JOIN [dbo].[Orders] AS [Extent2] ON [Extent1].[CustomerID] = [Extent2].[CustomerID]
WHERE N'UK' = [Extent1].[Country]
) AS [Project1]
ORDER BY [Project1].[CustomerID] ASC, [Project1].[C2] ASC
При использовании отложенной загрузки сначала вы получите следующий запрос:
SELECT
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[CompanyName] AS [CompanyName],
[Extent1].[ContactName] AS [ContactName],
[Extent1].[ContactTitle] AS [ContactTitle],
[Extent1].[Address] AS [Address],
[Extent1].[City] AS [City],
[Extent1].[Region] AS [Region],
[Extent1].[PostalCode] AS [PostalCode],
[Extent1].[Country] AS [Country],
[Extent1].[Phone] AS [Phone],
[Extent1].[Fax] AS [Fax]
FROM [dbo].[Customers] AS [Extent1]
WHERE N'UK' = [Extent1].[Country]
Каждый раз, когда вы обращаетесь к навигационному свойству Orders клиента, в базе данных выполняется запрос, аналогичный представленному ниже.
exec sp_executesql N'SELECT
[Extent1].[OrderID] AS [OrderID],
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[EmployeeID] AS [EmployeeID],
[Extent1].[OrderDate] AS [OrderDate],
[Extent1].[RequiredDate] AS [RequiredDate],
[Extent1].[ShippedDate] AS [ShippedDate],
[Extent1].[ShipVia] AS [ShipVia],
[Extent1].[Freight] AS [Freight],
[Extent1].[ShipName] AS [ShipName],
[Extent1].[ShipAddress] AS [ShipAddress],
[Extent1].[ShipCity] AS [ShipCity],
[Extent1].[ShipRegion] AS [ShipRegion],
[Extent1].[ShipPostalCode] AS [ShipPostalCode],
[Extent1].[ShipCountry] AS [ShipCountry]
FROM [dbo].[Orders] AS [Extent1]
WHERE [Extent1].[CustomerID] = @EntityKeyValue1',N'@EntityKeyValue1 nchar(5)',@EntityKeyValue1=N'AROUT'
Дополнительные сведения см. в разделе "Загрузка связанных объектов".
8.2.1 Отложенная загрузка и активная загрузка краткое руководство
Нет универсального способа выбора между ранней загрузкой и отложенной загрузкой. Сначала попробуйте понять различия между обеими стратегиями, чтобы вы могли сделать хорошо информированное решение; кроме того, рассмотрите, соответствует ли код любому из следующих сценариев:
| Сценарий | Наше предложение |
|---|---|
| Требуется ли получить доступ ко многим свойствам навигации из полученных сущностей? |
Неважно - Оба варианта, вероятно, подойдут. Тем не менее, если полезные данные, которые ваш запрос приносит, не слишком велики, вы можете воспользоваться преимуществами производительности, используя жадную загрузку, так как для материализации объектов потребуется меньше сетевых обходов. Да . Если вам нужно получить доступ ко многим свойствам навигации из сущностей, это можно сделать с помощью нескольких инструкций включения в запрос с помощью охотной загрузки. Чем больше сущностей вы включаете, тем больше данных возвращает ваш запрос. После включения трех или более сущностей в запрос рассмотрите возможность переключения на отложенную загрузку. |
| Знаете ли вы точно, какие данные потребуются во время выполнения? |
Нет - Отложенная загрузка будет лучше для вас. В противном случае вы можете запросить данные, которые вам не понадобятся. Да - Жадная загрузка, вероятно, ваш лучший выбор; это поможет быстрее загружать целые наборы. Если ваш запрос требует получения очень большого объема данных, и это становится слишком медленным, то попробуйте ленивую загрузку. |
| Выполняется ли ваш код далеко от базы данных? (увеличение задержки сети) |
Нет . Если задержка в сети не является проблемой, использование отложенной загрузки может упростить код. Помните, что топология вашего приложения может измениться, поэтому не принимайте близость базы данных как должное. Да . Если сеть является проблемой, только вы можете решить, что лучше подходит для вашего сценария. Как правило, жадная загрузка будет лучше, так как она требует меньше сетевых запросов. |
8.2.2. Проблемы с производительностью при использовании нескольких Include
Когда мы слышим вопросы о производительности, связанные с проблемами времени отклика сервера, источником проблемы часто являются запросы с несколькими операторами Include. Несмотря на то, что связанные сущности в запросе мощны, важно понять, что происходит за кулисами.
Для запроса с несколькими операторами Include в запросе требуется относительно длительное время, чтобы пройти через наш внутренний компилятор плана для создания команды хранилища. Большая часть этого времени тратится на оптимизацию результирующего запроса. Созданная команда хранения будет содержать Outer Join (внешнее соединение) или объединение для каждого включения в зависимости от вашей схемы сопоставления. Запросы такого рода будут извлекать большие связанные графы из вашей базы данных в одном пакете данных, что усугубит проблемы с пропускной способностью, особенно когда в пакете много избыточности (например, при использовании нескольких уровней include для обхода связей в сторону "один ко многим").
Вы можете проверить случаи, когда ваши запросы возвращают слишком большие объёмы данных, получив доступ к исходному TSQL запроса с помощью ToTraceString и использования команды STORE в SQL Server Management Studio, чтобы определить их размер. В таких случаях можно попытаться уменьшить количество инструкций Include в запросе, чтобы получить только необходимые данные. Или вы можете разбить запрос на меньшую последовательность подзапросов, например:
Перед разбиением запроса:
using (NorthwindEntities context = new NorthwindEntities())
{
var customers = from c in context.Customers.Include(c => c.Orders)
where c.LastName.StartsWith(lastNameParameter)
select c;
foreach (Customer customer in customers)
{
...
}
}
После разбивки запроса:
using (NorthwindEntities context = new NorthwindEntities())
{
var orders = from o in context.Orders
where o.Customer.LastName.StartsWith(lastNameParameter)
select o;
orders.Load();
var customers = from c in context.Customers
where c.LastName.StartsWith(lastNameParameter)
select c;
foreach (Customer customer in customers)
{
...
}
}
Это будет работать только для отслеживаемых запросов, так как мы используем способность контекста автоматически выполнять разрешение идентификации и исправление ассоциаций.
Как и при отложенной загрузке, обмен будет в большем числе запросов для меньших нагрузок. Вы также можете использовать проекции отдельных свойств для явного выбора только данных, необходимых для каждой сущности, но в этом случае вы не будете загружать сущности, а обновления не будут поддерживаться.
8.2.3 Обходной путь для отложенной загрузки свойств
Entity Framework в настоящее время не поддерживает отложенную загрузку скалярных или сложных свойств. Однако в случаях, когда у вас есть таблица, содержащая объект большого размера, например BLOB (Большой Двоичный Объект), можно использовать разделение таблицы, чтобы вынести большие свойства в отдельную сущность. Например, предположим, что у вас есть таблица Product, содержащая столбец с фотографией типа varbinary. Если вам не нужно часто обращаться к этому свойству в запросах, можно использовать разделение таблицы, чтобы перенести только части сущности, которую обычно требуется. Сущность, представляющая фотографию продукта, будет загружена только при явной необходимости.
Хороший ресурс, показывающий, как включить разделение таблиц, — это запись блога Гил Финка "Разделение таблиц в Entity Framework". <http://blogs.microsoft.co.il/blogs/gilf/archive/2009/10/13/table-splitting-in-entity-framework.aspx>
9 Другие вопросы
Сборка мусора сервера 9.1
Некоторые пользователи могут столкнуться с конкуренцией за ресурсы, ограничивающей ожидаемый параллелизм, поскольку сборщик мусора неправильно настроен. Если EF используется в многопоточных сценариях или в любом приложении, которое напоминает серверную систему, обязательно включите сборку мусора сервера. Это делается с помощью простого параметра в файле конфигурации приложения:
<?xmlversion="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<gcServer enabled="true" />
</runtime>
</configuration>
Это должно уменьшить конкуренцию потоков и увеличить пропускную способность до 30% при высокой загрузке ЦП. Как правило, вам всегда следует тестировать, как ваше приложение ведет себя с использованием классического сборщика мусора (который лучше подходит для сценариев пользовательского интерфейса и клиентской стороны), а также серверного сборщика мусора.
9.2 AutoDetectChanges
Как упоминалось ранее, Entity Framework может отображать проблемы с производительностью, когда кэш объектов имеет множество сущностей. Некоторые операции, такие как Add, Remove, Find, Entry и SaveChanges, активируют вызовы DetectChanges, которые могут потреблять значительное количество ресурсов процессора в зависимости от размера кэша объектов. Причиной этого является то, что кэш объектов и диспетчер состояний объектов пытаются оставаться синхронизированными по мере возможности для каждой операции, выполняемой в контексте, чтобы созданные данные гарантированно были исправлены в широком массиве сценариев.
Как правило, рекомендуется оставить автоматическое обнаружение изменений Entity Framework включено для всей жизни приложения. Если ваш сценарий подвергается негативному влиянию из-за высокого использования ЦП, и ваши профили указывают, что причиной является вызов DetectChanges, попробуйте временно отключить AutoDetectChanges в критической части вашего кода.
try
{
context.Configuration.AutoDetectChangesEnabled = false;
var product = context.Products.Find(productId);
...
}
finally
{
context.Configuration.AutoDetectChangesEnabled = true;
}
Прежде чем отключить AutoDetectChanges, рекомендуется понять, что это может привести к тому, что Entity Framework потеряет возможность отслеживать определенные сведения об изменениях, происходящих на сущностях. При неправильной обработке это может привести к несоответствию данных в приложении. Дополнительные сведения об отключении AutoDetectChanges см. в статье <http://blog.oneunicorn.com/2012/03/12/secrets-of-detectchanges-part-3-switching-off-automatic-detectchanges/>.
9.3 Контекст для каждого запроса
Контексты Entity Framework предназначены для использования в качестве кратковременных экземпляров, чтобы обеспечить наиболее оптимальную производительность. Ожидается, что контексты будут кратковременными и подлежащими удалению, и, следовательно, они реализованы как очень легковесные и обеспечивают повторное использование метаданных, где это возможно. В веб-сценариях важно помнить об этом и не иметь контекста в течение большей продолжительности одного запроса. В аналогичных сценариях, не связанных с вебом, контекст следует удалять на основе понимания различных уровней кэширования в Entity Framework. Как правило, следует избегать использования экземпляров контекста на протяжении всей жизни приложения, а также контекстов для каждого потока и статических контекстов.
9.4 Семантика базы данных NULL
Entity Framework по умолчанию создает код SQL, имеющий семантику сравнения значений NULL C#. Рассмотрим следующий пример запроса:
int? categoryId = 7;
int? supplierId = 8;
decimal? unitPrice = 0;
short? unitsInStock = 100;
short? unitsOnOrder = 20;
short? reorderLevel = null;
var q = from p incontext.Products
where p.Category.CategoryName == "Beverages"
|| (p.CategoryID == categoryId
|| p.SupplierID == supplierId
|| p.UnitPrice == unitPrice
|| p.UnitsInStock == unitsInStock
|| p.UnitsOnOrder == unitsOnOrder
|| p.ReorderLevel == reorderLevel)
select p;
var r = q.ToList();
В этом примере мы сравниваем ряд переменных, допускающих значение NULL, с свойствами, допускающими значение NULL, в сущности, например SupplierID и UnitPrice. Созданный SQL для этого запроса запрашивает, совпадает ли значение параметра со значением столбца, или если значения столбца и параметр имеют значение NULL. Это позволит скрыть способ обработки значений NULL сервера базы данных и обеспечит согласованный интерфейс C# null для разных поставщиков баз данных. С другой стороны, созданный код немного запутанный и может не работать хорошо, когда количество сравнений в условии where в запросе становится большим.
Одним из способов решения этой ситуации является использование семантики null базы данных. Обратите внимание, что это может отличаться от семантики null в C#, так как теперь Entity Framework будет генерировать более простой SQL, который показывает, как ядро СУБД обрабатывает значения NULL. Семантику null базы данных можно активировать в пределах контекста путем добавления одной строки в конфигурацию контекста.
context.Configuration.UseDatabaseNullSemantics = true;
При использовании семантики нулевых значений в базе данных, запросы от небольших до средних размеров не будут показывать заметного улучшения производительности, но разница станет более заметной в запросах с большим количеством потенциальных сравнений значений NULL.
В приведенном выше примере запроса разница производительности составила менее 2% в микробнчмарке, работающей в управляемой среде.
9.5 Асинхронная синхронизация
Entity Framework 6 представила поддержку асинхронных операций при запуске в .NET 4.5 или более поздней версии. В большинстве случаев приложения, испытывающие конфликт доступа к операциям ввода-вывода, получат наибольшую выгоду от использования асинхронных запросов и операций сохранения. Если ваше приложение не страдает от конкуренции ввода-вывода, использование асинхронности в лучших случаях выполняется синхронно и возвращает результат за то же время, что и синхронный вызов, или в худшем случае просто отложит выполнение, переведя его на асинхронную задачу, и увеличит время завершения сценария.
Сведения о том, как работает асинхронное программирование, которое поможет вам решить, улучшит ли асинхронное программирование приложения, см. в статье "Асинхронное программирование" и "Ожидание". Дополнительные сведения об использовании асинхронных операций в Entity Framework см. в статье "Асинхронный запрос" и "Сохранить".
9.6 NGEN
Entity Framework 6 не входит в стандартную установку .NET Framework. Таким образом, сборки Entity Framework по умолчанию не обрабатываются с помощью NGEN, что означает, что весь код Entity Framework подвергается тем же затратам на JIT, что и любая другая сборка MSIL. Это может ухудшить качество работы с системой F5 при разработке, а также замедлить холодный запуск вашего приложения в рабочих средах. Чтобы сократить затраты ЦП и памяти на JIT, рекомендуется воспользоваться NGEN для изображений Entity Framework по мере необходимости. Дополнительные сведения о том, как повысить производительность запуска Entity Framework 6 с NGEN, см. в статье "Повышение производительности запуска с помощью NGen".
9.7 Code First и EDMX
Entity Framework решает проблему несоответствия импеданса между объектно-ориентированным программированием и реляционными базами данных, предоставляя представление концептуальной модели (объектов) в памяти, схемы хранилища (базы данных) и сопоставление между ними. Эти метаданные называются моделью данных сущности или EDM. Из этого EDM Entity Framework получает представления для переноса данных из объектов в памяти в базу данных и обратно.
Если Entity Framework используется с файлом EDMX, который официально указывает концептуальную модель, схему хранения и сопоставление, то этап загрузки модели должен проверить правильность EDM (например, убедиться, что ни одно сопоставление не отсутствует), затем создать представления, далее проверить эти представления и убедиться, что метаданные готовы для использования. Только после этого запрос можно выполнить или сохранить новые данные в хранилище данных.
Первый подход к коду — это в его основе сложный генератор модели данных сущностей. Entity Framework должен создать EDM из предоставленного кода; Это делается путем анализа классов, участвующих в модели, применения соглашений и настройки модели с помощью API Fluent. После создания EDM платформа Entity Framework по сути ведет себя так же, как если бы в проекте присутствовал EDMX-файл. Таким образом, создание модели из Code First добавляет дополнительную сложность, которая преобразуется в более медленное время запуска для Entity Framework по сравнению с EDMX. Стоимость полностью зависит от размера и сложности создаваемой модели.
При выборе использования EDMX и Code First важно знать, что гибкость, представленная code First, увеличивает стоимость создания модели в первый раз. Если ваше приложение может выдержать затраты на эту первую загрузку, обычно code first будет предпочтительным методом.
10 Исследование производительности
10.1 С помощью Профилировщика Visual Studio
Если у вас возникли проблемы с производительностью в Entity Framework, можно использовать профилировщик, например встроенный в Visual Studio, чтобы узнать, где ваше приложение тратит свое время. Это инструмент, который мы использовали для создания круговых диаграмм в записи блога "Изучение производительности ADO.NET Entity Framework — часть 1" (<https://learn.microsoft.com/archive/blogs/adonet/exploring-the-performance-of-the-ado-net-entity-framework-part-1>), в которой показано, где Entity Framework использует свое время во время холодных и теплых запросов.
Запись блога "Профилирование Entity Framework с помощью профилировщика Visual Studio 2010", написанная группой консультантов по анализу данных и моделирования клиентов, показывает реальный пример использования профилировщика для исследования проблемы производительности. <https://learn.microsoft.com/archive/blogs/dmcat/profiling-entity-framework-using-the-visual-studio-2010-profiler>. Эта запись была написана для приложения Windows. Если вам нужно профилировать веб-приложение, средства записи производительности Windows (WPR) и анализатора производительности Windows (WPA) могут работать лучше, чем работать из Visual Studio. WPR и WPA являются частью набора средств производительности Windows, включенных в комплект средств оценки и развертывания Windows.
10.2 Профилирование приложений и баз данных
Такие инструменты, как профилировщик, встроенный в Visual Studio, сообщают вам, где ваше приложение тратит время. Другой тип профилировщика доступен, который выполняет динамический анализ запущенного приложения в рабочей или предварительной среде в зависимости от потребностей и ищет распространенные ошибки и антишаблоны доступа к базам данных.
Два коммерчески доступных профилировщика — Entity Framework Profiler ( <http://efprof.com>) и ORMProfiler ( <http://ormprofiler.com>).
Если ваше приложение — это MVC-приложение, использующее Code First, вы можете использовать MiniProfiler от StackExchange. Скотт Ханселман описывает это средство в своем блоге: <http://www.hanselman.com/blog/NuGetPackageOfTheWeek9ASPNETMiniProfilerFromStackExchangeRocksYourWorld.aspx>
Дополнительные сведения о профилировании активности базы данных приложения см. в статье журнала MSDN Magazine Джули Лерман под названием "Профилирование активности базы данных в Entity Framework".
10.3 Средство ведения журнала базы данных
Если вы используете Entity Framework 6, также рассмотрите возможность использования встроенных функций ведения журнала. Свойство Базы данных контекста может быть настроено для регистрации активности с помощью простой однострочной конфигурации.
using (var context = newQueryComparison.DbC.NorthwindEntities())
{
context.Database.Log = Console.WriteLine;
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
q.ToList();
}
В этом примере действие базы данных будет зарегистрировано в консоли, но свойство Log можно настроить для вызова любого делегата Action
Если вы хотите включить ведение журнала базы данных без повторной компиляции, и вы используете Entity Framework 6.1 или более поздней версии, это можно сделать, добавив перехватчик в web.config или app.config файл приложения.
<interceptors>
<interceptor type="System.Data.Entity.Infrastructure.Interception.DatabaseLogger, EntityFramework">
<parameters>
<parameter value="C:\Path\To\My\LogOutput.txt"/>
</parameters>
</interceptor>
</interceptors>
Дополнительные сведения о добавлении логирования без повторной компиляции см. в разделе <http://blog.oneunicorn.com/2014/02/09/ef-6-1-turning-on-logging-without-recompiling/>.
Приложение 11
11.1 А. Тестовая среда
Эта среда использует 2-компьютерную настройку с базой данных на отдельном компьютере от клиентского приложения. Компьютеры находятся в одной стойке, поэтому задержка сети относительно низкая, но более реалистичная, чем среда с одним компьютером.
Сервер приложений 11.1.1
11.1.1.1 Программное обеспечение
- Среда программного обеспечения Entity Framework 4
- Имя ОС: Windows Server 2008 R2 Корпоративная с пакетом обновления 1 (SP1).
- Visual Studio 2010 — Ultimate.
- Visual Studio 2010 с пакетом обновления 1 (SP1) (только для некоторых сравнений).
- Среда программного обеспечения Entity Framework 5 и 6
- Имя ОС: Windows 8.1 Корпоративная
- Visual Studio 2013 — Ultimate.
11.1.1.2 Аппаратная среда
- Двойной процессор: Процессор Intel(R) Xeon(R) ЦП L5520 W3530 @ 2,27GГц, 2261 ГГц8 ГГц, 4 ядра, 84 логических процессора.
- 2412 ГБ RamRAM.
- 136 ГБ SCSI250GB SATA 7200 rpm 3GB/s диск разделен на 4 секции.
Сервер базы данных 11.1.2
11.1.2.1 Программное обеспечение
- Имя ОС: Windows Server 2008 R28.1 Корпоративная с пакетом обновления 1 (SP1).
- SQL Server 2008 R22012.
Среда оборудования 11.1.2.2
- Один процессор: Intel(R) Xeon(R) ЦП L5520 @ 2,27 ГГц, 2261 MhzES-1620 0 @ 3,60GГц, 4 ядра, 8 логических процессоров.
- 824 ГБ RamRAM.
- 465 ГБ ATA, диск на 500 ГБ SATA 7200 об/мин 6 Гбит/с, разделён на 4 раздела.
11.2 B. Тесты сравнения производительности запросов
Модель Northwind использовалась для выполнения этих тестов. Он был создан из базы данных с помощью конструктора Entity Framework. Затем для сравнения производительности параметров выполнения запроса использовался следующий код:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure;
using System.Data.EntityClient;
using System.Data.Objects;
using System.Linq;
namespace QueryComparison
{
public partial class NorthwindEntities : ObjectContext
{
private static readonly Func<NorthwindEntities, string, IQueryable<Product>> productsForCategoryCQ = CompiledQuery.Compile(
(NorthwindEntities context, string categoryName) =>
context.Products.Where(p => p.Category.CategoryName == categoryName)
);
public IQueryable<Product> InvokeProductsForCategoryCQ(string categoryName)
{
return productsForCategoryCQ(this, categoryName);
}
}
public class QueryTypePerfComparison
{
private static string entityConnectionStr = @"metadata=res://*/Northwind.csdl|res://*/Northwind.ssdl|res://*/Northwind.msl;provider=System.Data.SqlClient;provider connection string='data source=.;initial catalog=Northwind;integrated security=True;multipleactiveresultsets=True;App=EntityFramework'";
public void LINQIncludingContextCreation()
{
using (NorthwindEntities context = new NorthwindEntities())
{
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
q.ToList();
}
}
public void LINQNoTracking()
{
using (NorthwindEntities context = new NorthwindEntities())
{
context.Products.MergeOption = MergeOption.NoTracking;
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
q.ToList();
}
}
public void CompiledQuery()
{
using (NorthwindEntities context = new NorthwindEntities())
{
var q = context.InvokeProductsForCategoryCQ("Beverages");
q.ToList();
}
}
public void ObjectQuery()
{
using (NorthwindEntities context = new NorthwindEntities())
{
ObjectQuery<Product> products = context.Products.Where("it.Category.CategoryName = 'Beverages'");
products.ToList();
}
}
public void EntityCommand()
{
using (EntityConnection eConn = new EntityConnection(entityConnectionStr))
{
eConn.Open();
EntityCommand cmd = eConn.CreateCommand();
cmd.CommandText = "Select p From NorthwindEntities.Products As p Where p.Category.CategoryName = 'Beverages'";
using (EntityDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
{
List<Product> productsList = new List<Product>();
while (reader.Read())
{
DbDataRecord record = (DbDataRecord)reader.GetValue(0);
// 'materialize' the product by accessing each field and value. Because we are materializing products, we won't have any nested data readers or records.
int fieldCount = record.FieldCount;
// Treat all products as Product, even if they are the subtype DiscontinuedProduct.
Product product = new Product();
product.ProductID = record.GetInt32(0);
product.ProductName = record.GetString(1);
product.SupplierID = record.GetInt32(2);
product.CategoryID = record.GetInt32(3);
product.QuantityPerUnit = record.GetString(4);
product.UnitPrice = record.GetDecimal(5);
product.UnitsInStock = record.GetInt16(6);
product.UnitsOnOrder = record.GetInt16(7);
product.ReorderLevel = record.GetInt16(8);
product.Discontinued = record.GetBoolean(9);
productsList.Add(product);
}
}
}
}
public void ExecuteStoreQuery()
{
using (NorthwindEntities context = new NorthwindEntities())
{
ObjectResult<Product> beverages = context.ExecuteStoreQuery<Product>(
@" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued
FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
WHERE (C.CategoryName = 'Beverages')"
);
beverages.ToList();
}
}
public void ExecuteStoreQueryDbContext()
{
using (var context = new QueryComparison.DbC.NorthwindEntities())
{
var beverages = context.Database.SqlQuery\<QueryComparison.DbC.Product>(
@" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued
FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
WHERE (C.CategoryName = 'Beverages')"
);
beverages.ToList();
}
}
public void ExecuteStoreQueryDbSet()
{
using (var context = new QueryComparison.DbC.NorthwindEntities())
{
var beverages = context.Products.SqlQuery(
@" SELECT P.ProductID, P.ProductName, P.SupplierID, P.CategoryID, P.QuantityPerUnit, P.UnitPrice, P.UnitsInStock, P.UnitsOnOrder, P.ReorderLevel, P.Discontinued
FROM Products AS P INNER JOIN Categories AS C ON P.CategoryID = C.CategoryID
WHERE (C.CategoryName = 'Beverages')"
);
beverages.ToList();
}
}
public void LINQIncludingContextCreationDbContext()
{
using (var context = new QueryComparison.DbC.NorthwindEntities())
{
var q = context.Products.Where(p => p.Category.CategoryName == "Beverages");
q.ToList();
}
}
public void LINQNoTrackingDbContext()
{
using (var context = new QueryComparison.DbC.NorthwindEntities())
{
var q = context.Products.AsNoTracking().Where(p => p.Category.CategoryName == "Beverages");
q.ToList();
}
}
}
}
11.3. Модель Navision
База данных Navision — это большая база данных, используемая для демонстрации Microsoft Dynamics — NAV. Созданная концептуальная модель содержит наборы сущностей 1005 и 4227 наборов ассоциаций. Модель, используемая в тесте, является "плоской" — к ней не добавлено наследование.
11.3.1 Запросы, используемые для тестов Navision
Список запросов, используемый с моделью Navision, содержит 3 категории запросов Entity SQL:
11.3.1.1 Поиск
Простой запрос поиска без агрегаций
- Число: 16232
- Пример:
<Query complexity="Lookup">
<CommandText>Select value distinct top(4) e.Idle_Time From NavisionFKContext.Session as e</CommandText>
</Query>
11.3.1.2 SingleAggregating
Обычный запрос бизнес-аналитики с несколькими агрегациями, но без промежуточных итогов (один запрос)
- Количество: 2313
- Пример:
<Query complexity="SingleAggregating">
<CommandText>NavisionFK.MDF_SessionLogin_Time_Max()</CommandText>
</Query>
Где MDF_SessionLogin_Time_Max() определяется в модели следующим образом:
<Function Name="MDF_SessionLogin_Time_Max" ReturnType="Collection(DateTime)">
<DefiningExpression>SELECT VALUE Edm.Min(E.Login_Time) FROM NavisionFKContext.Session as E</DefiningExpression>
</Function>
11.3.1.3 Агрегирование Субтоталов
Запрос бизнес-аналитики с агрегатами и промежуточными итогами (через UNION ALL)
- Число: 178
- Пример:
<Query complexity="AggregatingSubtotals">
<CommandText>
using NavisionFK;
function AmountConsumed(entities Collection([CRONUS_International_Ltd__Zone])) as
(
Edm.Sum(select value N.Block_Movement FROM entities as E, E.CRONUS_International_Ltd__Bin as N)
)
function AmountConsumed(P1 Edm.Int32) as
(
AmountConsumed(select value e from NavisionFKContext.CRONUS_International_Ltd__Zone as e where e.Zone_Ranking = P1)
)
----------------------------------------------------------------------------------------------------------------------
(
select top(10) Zone_Ranking, Cross_Dock_Bin_Zone, AmountConsumed(GroupPartition(E))
from NavisionFKContext.CRONUS_International_Ltd__Zone as E
where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed
group by E.Zone_Ranking, E.Cross_Dock_Bin_Zone
)
union all
(
select top(10) Zone_Ranking, Cast(null as Edm.Byte) as P2, AmountConsumed(GroupPartition(E))
from NavisionFKContext.CRONUS_International_Ltd__Zone as E
where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed
group by E.Zone_Ranking
)
union all
{
Row(Cast(null as Edm.Int32) as P1, Cast(null as Edm.Byte) as P2, AmountConsumed(select value E
from NavisionFKContext.CRONUS_International_Ltd__Zone as E
where AmountConsumed(E.Zone_Ranking) > @MinAmountConsumed))
}</CommandText>
<Parameters>
<Parameter Name="MinAmountConsumed" DbType="Int32" Value="10000" />
</Parameters>
</Query>