Пакет Entity Framework 7 (EF7) Preview 6 опубликован и доступен для загрузки через nuget.org (в конце поста есть ссылки на индивидуальные компоненты пакета). Этот пост блога раскрывает реализованные в пакете улучшения, направленные на увеличение производительности [запросов]; полный перечень изменений EF7 Preview 6 доступен на этой странице.

От переводчика

Формат блога не характерен для статей Хабра, равно как и для переводов статей. Однако, содержание данного поста настолько интересно, что не смог удержаться. Изложенная информация относится к разряду "Надо знать" для использующих фреймворк, и останется актуальной на длительный срок. Текст, [выделенный квадратными скобками], добавлен мной.

Изменения для улучшения производительности

В EF7 значительно улучшена производительность команды SaveChanges, в основном благодаря удалению избыточных циклов обмена с базой данных по сети . В некоторых случаях мы сократили время запроса до 74% – т.е., получили четырехкратное увеличение производительности!

Предпосылки

Производительность [работы] EF Core всегда находится у нас под высшим приоритетом. В пакете EF Core 6.0 мы сконцентрировались на улучшении производительности запросов, возвращающих сущности без отслеживания (non-tracking entities), достигнув значительного увеличения скорости и сделав EF Core сопоставимым по производительности с прямыми запросами в SQL с использованием Dapper (подробнее см. этот блог). Для EF Core 7.0, мы сконцентрировались на реализации фреймворком “конвейера [процедур] обновления”, конкретнее – [работе] компонента, реализующего SaveChanges и отвечающего за выполнение операций вставки, обновления и удаления записей в базе данных.

Оптимизация запросов в EF Core 6.0 была, по существу, улучшением производительности [фреймворка] в режиме выполнения: целью было уменьшить затраты самого кода EF Core, т.е. время выполнения кода EF Core при выполнении запроса. Оптимизация в реализации запросов на обновление в EF Core 7.0 несколько отличается; оказалось, что есть возможности для улучшения самого SQL запроса, который EF посылает в базу данных, и (что даже более существенно) в уменьшении количества циклов обмена по сети, производимых в процессе выполнения процедуры SaveChanges. Оптимизация обмена по сети особенно важна для улучшения производительности современных приложений:

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

  • Задержка [сетевого ответа] зависит от множества факторов, так что чем больше длительность цикла сетевого обмена, тем лучше эффект от оптимизации.

  • Обычно в среде разработки сервер базы данных расположен локально по отношению к приложению. В облачной среде база данных находится значительно дальше, что увеличивает сетевые задержки.

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

Транзакции и циклы сетевого обмена

Давайте рассмотрим очень простую программу EF, которая вставляет одну строку в базу данных:

var blog = new Blog { Name = "MyBlog" };
ctx.Blogs.Add(blog);
await ctx.SaveChangesAsync();

Выполнение под EF Core 6.0 покажет следующий лог обмена (отфильтрованы самые существенные события):

dbug: 2022-07-10 17:10:48.450 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 2022-07-10 17:10:48.521 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (30ms) [Parameters=[@p0='Foo' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      VALUES (@p0);
      SELECT [Id]
      FROM [Blogs]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

dbug: 2022-07-10 17:10:48.549 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Основная команда – которая занимает 30 миллисекунд – содержит два SQL оператора (за исключением оператора NOCOUNT, который несущественен): ожидаемого оператора INSERT, с последующим оператором SELECT для получения ID вставленной строки. В EF Core, когда ключ сущности является значением типа int, EF обычно настроена на получение генерируемого базой данных значения ключа; для SQL Server это столбец с признаком IDENTITY. Поскольку после вставки строки может потребоваться выполнить последующие операции с сущностью, EF должна вернуть значение ID и присвоить его экземпляру сущности blog.

Вроде всё хорошо; однако здесь выполняется ещё кое что: перед выполнением команды открывается транзакция, которая фиксируется после выполнения команд. При разборе исполнения в моём средстве анализа производительности, эта транзакция стоит нам двух дополнительных циклов обмена с базой данных – первый при её старте, и второй при фиксации. На данный момент [в Ef Core 6.0] транзакция применяется по следующей причине: SaveChanges может иметь необходимость в выполнении нескольких операций изменения, и мы бы хотели, чтобы эти операции были обернуты в транзакцию – так, чтобы в случае отказа база данных вернулась в консистентное состояние. Но что происходит, если требуется только одна операция, как в примере выше?

Оказывается, что база данных гарантирует атомарность для (большинства) единичных SQL операторов; если происходит ошибка, вы не должны беспокоиться о частично выполненном состоянии. Это замечательно – значит, мы можем полностью удалить транзакцию при выполнении единичного оператора. Учитывая это, вот что выдает тот же самый код с EF Core 7.0:

info: 2022-07-10 17:24:28.740 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (52ms) [Parameters=[@p0='Foo' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@p0);

Намного короче – и без транзакции! Посмотрите, что эта оптимизация дает при тестировании с помощью BenchmarkDotNet (вы же не выполняете тестирование вручную с помощью Stopwatch, не так ли?).

Method

EF Version

Server

Mean

Error

StdDev

Insert_one_row

6.0

Localhost

522.9 us

5.76 us

5.10 us

Insert_one_row

7.0

Localhost

390.0 us

6.78 us

8.82 us

Прекрасно, имеем улучшение на 133 микросекунды, или 25%! Но поскольку мы обсуждаем издержки на сетевой обмен, вы должны спросить себя: где расположена база данных, и каковы задержки при обмене с ней? В таблице выше SQL сервер выполняется на локальной машине (там же, где и само приложение). Однако, так вы никогда не должны делать при анализе производительности: размещение базы данных на одной машине с приложением может вызвать влияние на работу приложения и исказить результат; в конце концов, вы бы не сделали такую конфигурацию на производстве, не так ли? Но что для нас более важно, сетевые задержки при подключении на localhost гораздо, гораздо ниже [чем при подключении на удаленную машину]– мы увидели нижний предел улучшения производительности.

Выполним ещё один запуск, на удаленной машине. В этом тесте, я подключаюсь с моего ноутбука на мой десктоп, через соединение WiFi. Это также не совсем реалистично: WiFi не лучший транспорт для такого рода вещей, ведь точно так же как вы не запускаете в производстве приложение на одной машине с базой данных, так и вы не подключаетесь к базе данных по WiFi, не так ли? Не хочу дискуссий о том, насколько близко это имитирует подключение к базе данных в производстве, например, к базе данных в облаке – вы можете легко выполнить тесты в своем реальном окружении. Вот мои результаты:

Method

EF Version

Server

Mean

Error

StdDev

Insert_one_row

6.0

Remote

8.418 ms

0.1668 ms

0.4216 ms

Insert_one_row

7.0

Remote

4.593 ms

0.0913 ms

0.2531 ms

Здесь совсем другая картина: мы выполнение сократили на 3.8 миллисекунды, или 45%. Величина в 3.8 мс уже считается значительным сокращением времени для отзывчивых веб приложений или API, так что это весомое достижение.

Прежде чем мы двинемся дальше, вы могли заметить и другие изменения запроса SQL в примере выше, помимо устранения команд транзакции:

  • Появилась новая команда SET IMPLICIT_TRANSACTIONS OFF. Если вдруг параметр “IMPLICIT_TRANSACTIONS” в SQL Server установлен в значение ON, система находится в неявном режиме транзакции; выполнение единичного оператора вне транзакции неявно начинает новую транзакцию и не завершается автоматической фиксацией. Мы отключаем такое поведения явно, чтобы быть уверенными в том, что изменения зафиксированы. Накладные расходы на это незначительны.

  • Вместо вставки строки и последующей выборки сгенерированного базой значения ID, оператор SQL использует предложение OUTPUT, чтобы вернуть значение непосредственно из оператора INSERT. Кроме того, что эта команда SQL короче, это необходимо для получения транзакционной гарантии без необходимости явного включения транзакции, как это было рассмотрено выше. Это здорово, что по сравнению с EF Core 6 получается сократить 2 оператора в случаях, когда значение идентификатора последней вставленной строки является локальным по отношению к [сетевому] соединению (scope_identity), и значение ID не изменяется из EF. Впрочем, может быть множество случаев, когда это условие не выполняется (например: если, помимо идентификатора, были другие сгенерированные базой значения).

Вставка нескольких строк

Рассмотрим, что происходит, когда мы вставляем несколько строк:

for (var i = 0; i < 4; i++)
{
    var blog = new Blog { Name = "Foo" + i };
    ctx.Blogs.Add(blog);
}
await ctx.SaveChangesAsync();

Под EF Core 6.0 видим следующий лог выполнения:

dbug: 2022-07-10 18:46:39.583 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 2022-07-10 18:46:39.677 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (52ms) [Parameters=[@p0='Foo0' (Size = 4000), @p1='Foo1' (Size = 4000), @p2='Foo2' (Size = 4000), @p3='Foo3' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
      MERGE [Blogs] USING (
      VALUES (@p0, 0),
      (@p1, 1),
      (@p2, 2),
      (@p3, 3)) AS i ([Name], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name])
      VALUES (i.[Name])
      OUTPUT INSERTED.[Id], i._Position
      INTO @inserted0;

      SELECT [i].[Id] FROM @inserted0 i
      ORDER BY [i].[_Position];

dbug: 2022-07-10 18:46:39.705 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Это немного… неожиданно (и не интуитивно). В SQL Server существует оператор MERGE, который обычно предназначен для объединения двух таблиц, однако может быть использован и для других целей. Оказывается, использование MERGE для вставки четырех строк значительно быстрее чем 4 отдельных оператора INSERT – даже в пакетном режиме. Так что, выше происходит следующее:

  1. Создается вре́менная таблица (оператор DECLARE @inserted0).

  2. Используется MERGE для вставки в таблицу четырёх строк, сформированных из посланных в теле запроса параметров. Предложение OUTPUT (помните о нём?) выводит сгенерированные базой ID во временную таблицу.

  3. Оператор SELECT извлекает IDs из временной таблицы [в порядке, соответствующем порядку входных параметров].

Замечу, что такая продвинутая, специфичная для SQL Server техника является хорошим примером того, как ORM, такая как EF Core, может помочь вам создать более эффективный запрос, чем написанный вручную. Конечно, вы можете использовать рассмотренную технику самостоятельно без использования EF Core, но в реальности немногие пользователи погружаются настолько глубоко в процесс оптимизации; а вот в EF Core вам даже не обязательно знать об этом.

Теперь сравним с тем, что происходит в EF Core 7.0:

info: 2022-07-10 18:46:56.530 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (25ms) [Parameters=[@p0='Foo0' (Size = 4000), @p1='Foo1' (Size = 4000), @p2='Foo2' (Size = 4000), @p3='Foo3' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Blogs] USING (
      VALUES (@p0, 0),
      (@p1, 1),
      (@p2, 2),
      (@p3, 3)) AS i ([Name], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name])
      VALUES (i.[Name])
      OUTPUT INSERTED.[Id], i._Position;

Транзакции больше нет; как указано выше – MERGE также является единичным оператором, выполнение которого защищено неявной транзакцией. Заметьте, что если бы мы использовали 4 отдельных оператора INSERT, мы бы не могли бы опустить операции начала и завершения транзакции (вместе со всеми накладными расходами); так что здесь использовано ещё одно преимущество использования оператора MERGE, в сочетании с базовой лучшей производительностью [задействованной в EF Core 6.0].

Но и другие шаги изменились: нет временной таблицы, и теперь сгенерированные базой IDs посылаются на клиент непосредственно из предложения OUTPUT. Посмотрим на тесты производительности этих двух вариантов:

Method

EF Version

Server

Mean

Error

StdDev

Insert_four_rows

6.0

Remote

12.93 ms

0.258 ms

0.651 ms

Insert_four_rows

7.0

Remote

4.985 ms

0.0981 ms

0.1981 ms

Insert_four_rows

6.0

Local

1.679 ms

0.0331 ms

0.0368 ms

Insert_four_rows

7.0

Local

435.8 us

7.85 us

6.96 us

Сценарий с удаленной базой почти на 8 миллисекунд быстрее, или на 61% производительнее. Сценарий с локальной базой впечатляет ещё больше: ведь экономия в 1.243 миллисекунды означает улучшение производительности на 74%; операция выполняется на EF Core 7.0 в четыре раза быстрее!

Заметим, что результат получен за счет двух разных оптимизаций: отказа от явной транзакции, как это обсуждалось выше, и оптимизации запроса MERGE, заключающейся в исключении использования временной таблицы.

Небольшое отступление: [MS] SQL Server и предложение OUTPUT

На этом этапе вам может стать интересно, почему EF Core до сих пор [т.е., до EF Core 7.0] не использовал предложение OUTPUT – без использования временной таблицы. В конце концов, новая команда SQL и проще, и быстрее.

К сожалению, SQL Server имеет некоторые ограничения, которые не позволяют использование OUTPUT в определенных сценариях. Наиболее важно то, что использование классификатора OUTPUT на таблице, в которой определены триггеры, не поддерживается и вызывает ошибку (см. документацию MS SQL Server); а вот OUTPUT совместно с INTO (как использовано в MERGE в пакете EF Core 6.0) поддерживается. Первоначально, когда мы впервые разрабатывали EF Core, у нас была цель — чтобы всё работало во всех сценариях, чтобы сделать взаимодействие с пользователем максимально удобным; также мы не подозревали, как много накладных расходов добавляет вре́менная таблица. При внесении изменений в EF Core 7.0, мы рассматривали следующие варианты:

  1. По умолчанию сохранить имеющееся медленное поведение, дав возможность пользователям использовать новую, более эффективную технологию [там, где это возможно] как опцию.

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

Это непростое решение – при внесении улучшений мы всегда стараемся не сломать поведение на стороне пользователя. Однако, учитывая огромный прирост производительности, и тот факт, что [в варианте 1] пользователи [скорее всего] даже не будут знать о возможности улучшения производительности [включением какой-то там опции], мы остановились на варианте номер 2. Пользователи, использующие в таблице триггеры, при обновлении до EF Core 7.0 получат информативное исключение, которое укажет им на вариант исправления, а все остальные получат значительное улучшение производительности без необходимости что-либо знать дополнительно [т.е., без знаний о подводных камнях на быстром течении. Извините, не смог удержаться].

Дальнейшие оптимизации операций обмена: родительские и дочерние сущности

Давайте рассмотрим ещё один сценарий. На этот раз мы вставляем сущность (Blog) совместно с дочерней сущностью (Post):

ctx.Blogs.Add(new Blog
{
    Name = "MyBlog",
    Posts = new()
    {
        new Post { Title = "My first post" }
    }
});
await ctx.SaveChangesAsync();

Операция генерирует следующее:

dbug: 2022-07-10 19:39:32.826 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.

info: 2022-07-10 19:39:32.890 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (22ms) [Parameters=[@p0='MyBlog' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      VALUES (@p0);
      SELECT [Id]
      FROM [Blogs]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

info: 2022-07-10 19:39:32.929 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (3ms) [Parameters=[@p1='1', @p2='My first post' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Post] ([BlogId], [Title])
      VALUES (@p1, @p2);
      SELECT [Id]
      FROM [Post]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

dbug: 2022-07-10 19:39:32.932 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Мы имеем четыре операции обмена: две в связи с обслуживанием транзакции, одну при вставке сущности Blog, и одну при вставке сущности Post (обратите внимание, каждое выполнение DbCommand приводит к циклу [сетевого] обмена). На сегодня EF Core [6.0] в процедуре SaveChanges чаще всего формирует пакетную обработку, что означает что множественные изменения посылаются в одной команде – для лучшей эффективности. Однако, в данном случае это невозможно: поскольку ключ родительской сущности Blog является столбцом IDENTITY, значение которого генерируется базой данных, мы должны получить сформированный ключ обратно прежде, чем мы сможем отправить запрос на вставку дочерней сущности Post, поскольку она должна содержать значение ключа родительской сущности. Это нормальное состояние дел, и мы мало что можем с этим поделать.

Давайте изменим наши сущности Blog и Post и используем GUID ключ вместо Int. По умолчанию, EF Core выполняет генерацию ключа GUID на клиенте, что означает что новый GUID генерируется [в EF Core] самостоятельно, вместо того чтобы это сделать в базе данных, как в случае со столбцом IDENTITY. При использовании EF Core 6.0, мы получаем следующее:

dbug: 2022-07-10 19:47:51.176 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.

info: 2022-07-10 19:47:51.273 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (36ms) [Parameters=[@p0='7c63f6ac-a69a-4365-d1c5-08da629c4f43', @p1='MyBlog' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Id], [Name])
      VALUES (@p0, @p1);

info: 2022-07-10 19:47:51.284 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p2='d0e30140-0f33-4435-e165-08da629c4f4d', @p3='0', @p4='7c63f6ac-a69a-4365-d1c5-08da629c4f43' (Nullable = true), @p5='My first post' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Post] ([Id], [BlogId], [BlogId1], [Title])
      VALUES (@p2, @p3, @p4, @p5);

dbug: 2022-07-10 19:47:51.296 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

К сожалению, Blog и Post всё ещё вставляются посредством отдельных команд. EF Core 7.0 покончил с этим [расточительством] и выполняет следующее:

dbug: 2022-07-10 19:40:30.259 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.

info: 2022-07-10 19:40:30.293 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (26ms) [Parameters=[@p0='ce67f663-221a-4a86-3d5b-08da629b4875', @p1='MyBlog' (Size = 4000), @p2='127329d1-5c31-4001-c6a6-08da629b487b', @p3='0', @p4='ce67f663-221a-4a86-3d5b-08da629b4875' (Nullable = true), @p5='My first post' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Id], [Name])
      VALUES (@p0, @p1);
      INSERT INTO [Post] ([Id], [BlogId], [BlogId1], [Title])
      VALUES (@p2, @p3, @p4, @p5);

dbug: 2022-07-10 19:40:30.302 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Поскольку ключ сущности Blog генерируется на клиенте, нет необходимости ждать каких-либо сгенерированных базой значений, и два оператора INSERT объединяются в одну команду, что уменьшает количество циклов обмена.

Я знаю, что вы думаете: “для получения этой оптимизации, с учетом данных обстоятельств, потребуется переход от Int IDs с авто-приращением к GUIDs”. Прежде, чем вы ушли делать это, вы должны узнать, что EF Core также имеет фишку под именем HiLo, которая предоставляет аналогичные результаты с ключом типа Int. Когда HiLo сконфигурирована, EF создает в базе последовательность (database sequence), и получает интервал номеров из неё (по умолчанию 10 значений); эти заблаговременно полученные значения кэшируются в недрах EF Core, и используются всякий раз, когда необходимо вставить новую строку. Эффект от этого равнозначен применению GUID в примере выше: поскольку значение ключа из последовательности уже кэшировано локально, нет необходимости получать сгенерированное базой значение при вставке. Когда EF исчерпает полученные значения, она выполнит один запрос для получения следующего интервала значений, и т.д.

HiLo может быть установлено для свойства следующим образом:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().Property(b => b.Id).UseHiLo();
}

После этого, наш SaveChanges формирует такой же эффективный сценарий работы с базой, как и в случае с GUID:

dbug: 2022-07-10 19:54:25.862 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.

info: 2022-07-10 19:54:25.890 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (20ms) [Parameters=[@p0='1', @p1='MyBlog' (Size = 4000), @p2='1', @p3='My first post' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Id], [Name])
      VALUES (@p0, @p1);
      INSERT INTO [Post] ([BlogId], [Title])
      OUTPUT INSERTED.[Id]
      VALUES (@p2, @p3);

dbug: 2022-07-10 19:54:25.909 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Обратите внимание, что оптимизация с сокращением операций обмена также выполняется и в некоторых других сценариях, включая шаблон сопоставления «таблица на тип» (TPT), случай, когда в одном вызове SaveChanges производится удаление и вставка строк в одну таблицу, и некоторые другие.

Заключение

В этом посте мы рассмотрели три оптимизации в конвейере обновления EF Core 7.0:

  1. Исключение явного оператора транзакции, когда SaveChanges выполняет единичный оператор (уменьшение на две операции обмена).

  2. Оптимизация операции вставки нескольких строк, исключающая использование временной таблицы.

  3. Исключение лишних операций обмена при выполнении вставки родительской и дочерней сущности в одной операции SaveChanges и в некоторых других случаях.

Мы считаем, что это важные улучшения, и надеемся, что они принесут пользу вашему приложению. Поделитесь своим опытом [применения], хорошим или плохим!

Требования

  • EF7 требует .NET 6.

  • EF7 не работает под .NET Framework.

EF7 является преемником EF Core 6.0, не путайте с EF6. Если вы собираетесь переходить с EF6, пожалуйста прочитайте наше руководство Перенос приложений из EF6 в EF Core.

Как загрузить EF7 previews

EF7 распространяется исключительно в виде набора пакетов NuGet. Например, чтобы добавить SQL Server provider в проект, вы должны выполнить из консоли следующую команду dotnet:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 7.0.0-preview.6.22329.4

В следующей таблице собраны ссылки на пакеты EF Core 7 версии preview 6 и объяснения, для чего они используются.

Пакет

Назначение

Microsoft.EntityFrameworkCore

Основной EF Core пакет, независимый от провайдеров базы данных

Microsoft.EntityFrameworkCore.SqlServer

Провайдер базы данных для Microsoft SQL Server и SQL Azure

Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite

Поддержка SQL Server пространственных (географических) данных

Microsoft.EntityFrameworkCore.Sqlite

Провайдер базы данных SQLite, включая исполняемые компоненты самой БД

Microsoft.EntityFrameworkCore.Sqlite.Core

Провайдер базы данных SQLite без исполняемых компонентов самой БД

Microsoft.EntityFrameworkCore.Sqlite.NetTopologySuite

Поддержка пространственных данных в SQLite

Microsoft.EntityFrameworkCore.Cosmos

Провайдер базы данных Azure Cosmos DB

Microsoft.EntityFrameworkCore.InMemory

Провайдер резидентной БД (in-memory database)

Microsoft.EntityFrameworkCore.Tools

Команды EF Core PowerShell для консоли диспетчера пакетов Visual Studio; используйте это для интеграции таких инструментов, как формирование шаблонов и миграция, с Visual Studio.

Microsoft.EntityFrameworkCore.Design

Общие компоненты времени разработки для инструментов EF Core

Microsoft.EntityFrameworkCore.Proxies

Прокси-серверы с отложенной загрузкой и отслеживанием изменений

Microsoft.EntityFrameworkCore.Abstractions

Вынесенные в изолированный пакет абстракции EF Core. Используйте его для добавления своей функциональности, например для расширения аннотаций данных, определенных в EF Core.

Microsoft.EntityFrameworkCore.Relational

Совместно используемые компоненты для провайдеров реляционных баз данных.

Microsoft.EntityFrameworkCore.Analyzers

Анализаторы C# для EF Core

Мы также опубликовали 7.0 preview 6 релиз провайдера Microsoft.Data.Sqlite.Core для ADO.NET.

Установка интерфейса командной строки EF7 (CLI)

Прежде чем вы сможете выполнить команды EF7 Core migration или scaffolding, необходимо установить пакет командной строки (CLI package) как локальный или глобальный инструмент.

Для установки EF7 CLI в качестве глобального инструмента, выполните:

dotnet tool install --global dotnet-ef --version 7.0.0-preview.6.22329.4 

Если пакет уже имеется, можно выполнить обновление следующей командой:

dotnet tool update --global dotnet-ef --version 7.0.0-preview.6.22329.4 

Пакет новой версии EF7 CLI можно использовать также при работе с существующими проектами, использующими старшие версии EF Core runtime.

Ежедневные сборки

Предварительные версии EF7 согласованы с предварительными версиями .NET 7. Эти предварительные версии, как правило, отстают от последней работы над EF7. Рассмотрите возможность использования ежедневных сборок, чтобы получить самые последние функции EF7 и исправления ошибок.

Как и в случае с предварительными версиями, для ежедневных сборок требуется .NET 6.

Полезные ссылки

Благодарности от команды

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

Welcome to EF7.

Shay Rojansky
Shay Rojansky

Shay Rojansky

Комментарии (7)


  1. gdsmiler
    25.07.2022 10:31
    -1

    Оказывается, использование MERGE для вставки четырех строк значительно быстрее чем 4 отдельных оператора INSERT – даже в пакетном режиме

    Это как то противоречит документации MS SQL:

    Performance Tip: The conditional behavior described for the MERGE statement works best when the two tables have a complex mixture of matching characteristics. For example, inserting a row if it doesn't exist, or updating a row if it matches. When simply updating one table based on the rows of another table, improve the performance and scalability with basic INSERT, UPDATE, and DELETE statements

    https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver16


    1. Dotarev Автор
      25.07.2022 11:02
      +3

      Разве?

      Performance Tip: The conditional behavior described for the MERGE statement works best when the two tables have a complex mixture of matching characteristics. For example, inserting a row if it doesn't exist, or updating a row if it matches.


      1. gdsmiler
        25.07.2022 12:26

        complex mixture of matching characteristics

        То есть когда есть и совпадения и новые сроки


        1. qw1
          25.07.2022 22:42

          Я это понимаю так, что внутри сервера (например, внутри хранимой процедуры) выгоднее использовать чистый INSERT вместо MERGE. Но EF оптимизирует сетевой трафик, и он на порядки дороже по времени, чем экономия микросекунд на логике внутри сервера.


          1. Dotarev Автор
            26.07.2022 06:56
            +3

            Оптимизацией сетевого трафика занялись в EF7, а MERGE использовали уже в EF6.

            И кстати, в обсуждениях поста был вопрос - почему не использовали INSERT для нескольких записей, типа: "INSERT MyTable VALUES (row1c1, row1c2), (row2c1, row2c2) "

            Ответ Shay Rojansky таков: "Да, такой INSERT эффективнее, однако он не гарантирует порядок возврата значений сформированных в БД идентификаторов и теряется возможность сопоставить их с переданными на добавление строками.


  1. md_backend_binance
    25.07.2022 12:51
    +2

    Постояно упоминается SQLSERVER (в том числе только его настройки), статья справедлива для SQL SERVER или для любой совместимой базы, например постгри?


    1. Dotarev Автор
      25.07.2022 13:01
      +2

      Это был первый вопрос к посту. Ответ автора:

      The 2nd optimization (bulk update) is indeed only for SQL Server, but the other two (the roundtrip reduction) do apply for other providers

      т.е., оптимизация массового обновления реализована только в модуле Microsoft.EntityFrameworkCore.SqlServer. Оптимизации, связанные с отказом от явных транзакций, применимы и для других SQL провайдеров. А вот выполнены они реально или нет - автор не уточняет.