Самый Быстрый Способ Вставки в Entity Framework
Я ищу самый быстрый способ вставки данных в Entity Framework.
Я задаю этот вопрос в связи со сценарием, когда у вас есть активный TransactionScope
, и объем вставляемых данных очень большой (более 4000 записей). Процесс может занять более 10 минут (это стандартное время ожидания для транзакций), что приведет к незавершенной транзакции.
5 ответ(ов)
В ответ на ваш комментарий к вашему вопросу:
"...Сохранение изменений (для каждой записи)..."
Это худшее, что вы можете сделать! Вызов метода SaveChanges()
для каждой записи значительно замедляет массовую вставку данных. Я бы порекомендовал провести несколько простых тестов, которые, скорее всего, улучшат производительность:
- Вызовите
SaveChanges()
один раз после добавления ВСЕХ записей. - Вызовите
SaveChanges()
после, например, 100 записей. - Вызовите
SaveChanges()
после, например, 100 записей и освободите контекст, создав новый. - Отключите детектирование изменений.
Для массовой вставки я работаю и экспериментирую с шаблоном, который выглядит следующим образом:
using (TransactionScope scope = new TransactionScope())
{
MyDbContext context = null;
try
{
context = new MyDbContext();
context.Configuration.AutoDetectChangesEnabled = false;
int count = 0;
foreach (var entityToInsert in someCollectionOfEntitiesToInsert)
{
++count;
context = AddToContext(context, entityToInsert, count, 100, true);
}
context.SaveChanges();
}
finally
{
if (context != null)
context.Dispose();
}
scope.Complete();
}
private MyDbContext AddToContext(MyDbContext context,
Entity entity, int count, int commitCount, bool recreateContext)
{
context.Set<Entity>().Add(entity);
if (count % commitCount == 0)
{
context.SaveChanges();
if (recreateContext)
{
context.Dispose();
context = new MyDbContext();
context.Configuration.AutoDetectChangesEnabled = false;
}
}
return context;
}
Я провел тестовую программу, которая вставляет 560 000 сущностей (9 скалярных свойств, без навигационных свойств) в базу данных. С помощью этого кода это выполняется менее чем за 3 минуты.
Для повышения производительности важно вызывать SaveChanges()
после "многих" записей ("многое" - это около 100 или 1000). Также улучшает производительность освобождение контекста после SaveChanges
и создание нового. Это очищает контекст от всех сущностей, SaveChanges
этого не делает, сущности все еще прикреплены к контексту в состоянии Unchanged
. Увеличение количества прикрепленных сущностей в контексте замедляет вставку шаг за шагом. Поэтому полезно очищать его спустя некоторое время.
Вот несколько замеров для моих 560 000 сущностей:
- commitCount = 1, recreateContext = false: много часов (это ваша текущая процедура)
- commitCount = 100, recreateContext = false: больше 20 минут
- commitCount = 1000, recreateContext = false: 242 секунды
- commitCount = 10000, recreateContext = false: 202 секунды
- commitCount = 100000, recreateContext = false: 199 секунд
- commitCount = 1000000, recreateContext = false: ошибка недостатка памяти
- commitCount = 1, recreateContext = true: больше 10 минут
- commitCount = 10, recreateContext = true: 241 секунда
- commitCount = 100, recreateContext = true: 164 секунды
- commitCount = 1000, recreateContext = true: 191 секунда
Поведение в первом тесте заключается в том, что производительность очень нелинейная и значительно падает с течением времени. ("Много часов" - это оценка, я никогда не завершал этот тест, остановился на 50 000 сущностей после 20 минут). Эта нелинейность менее значительна в остальных тестах.
Данная комбинация действительно позволяет существенно увеличить скорость работы с контекстом Entity Framework. Отключение функции автоматического обнаружения изменений (AutoDetectChangesEnabled = false
) предотвращает ненужные проверки на изменения сущностей перед сохранением в базу данных, что может значительно сократить время выполнения операций.
Дополнительно отключение проверки на валидность при сохранении (ValidateOnSaveEnabled = false
) также экономит ресурсы, так как устраняется необходимость проверки валидности всех сущностей, что может быть излишним в некоторых случаях, например, если вы уверены в корректности данных.
Таким образом, использование этих настроек может быть оправдано в сценариях, где производительность критична и вы контролируете процесс изменений и валидности данных. Однако, не забывайте о потенциальных рисках, связанных с отключением этих механизмов, так как это может привести к сохранению некорректных данных в базу.
Я согласен с Адамом Рэкисом. SqlBulkCopy
- самый быстрый способ передачи больших объемов записей из одного источника данных в другой. Я использовал этот метод для копирования 20 тысяч записей, и это заняло менее 3 секунд. Посмотрите пример ниже.
public static void InsertIntoMembers(DataTable dataTable)
{
using (var connection = new SqlConnection(@"data source=;persist security info=True;user id=;password=;initial catalog=;MultipleActiveResultSets=True;App=EntityFramework"))
{
SqlTransaction transaction = null;
connection.Open();
try
{
transaction = connection.BeginTransaction();
using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
{
sqlBulkCopy.DestinationTableName = "Members";
sqlBulkCopy.ColumnMappings.Add("Firstname", "Firstname");
sqlBulkCopy.ColumnMappings.Add("Lastname", "Lastname");
sqlBulkCopy.ColumnMappings.Add("DOB", "DOB");
sqlBulkCopy.ColumnMappings.Add("Gender", "Gender");
sqlBulkCopy.ColumnMappings.Add("Email", "Email");
sqlBulkCopy.ColumnMappings.Add("Address1", "Address1");
sqlBulkCopy.ColumnMappings.Add("Address2", "Address2");
sqlBulkCopy.ColumnMappings.Add("Address3", "Address3");
sqlBulkCopy.ColumnMappings.Add("Address4", "Address4");
sqlBulkCopy.ColumnMappings.Add("Postcode", "Postcode");
sqlBulkCopy.ColumnMappings.Add("MobileNumber", "MobileNumber");
sqlBulkCopy.ColumnMappings.Add("TelephoneNumber", "TelephoneNumber");
sqlBulkCopy.ColumnMappings.Add("Deleted", "Deleted");
sqlBulkCopy.WriteToServer(dataTable);
}
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
}
}
}
Этот код создает подключение к базе данных и использует SqlBulkCopy
для вставки данных из DataTable
в таблицу "Members". Также он включает обработку транзакций, что позволяет откатить изменения в случае возникновения ошибок. Это отличный способ эффективно загружать большие объемы данных!
В ответ на ваш вопрос по поводу оптимизации вставки записей в EF Core 3.1, отключение AutoDetectChangesEnabled
действительно может значительно ускорить процесс вставки, как было отмечено в обновлении 2019 года. В вашем случае время вставки было сокращено в 100 раз — с нескольких минут до нескольких секунд при добавлении 10 000 записей с взаимосвязанными таблицами.
Вот обновленный код, который демонстрирует данное улучшение:
context.ChangeTracker.AutoDetectChangesEnabled = false; // Отключаем автообнаружение изменений
foreach (IRecord record in records) {
// Добавляем записи в вашу базу данных
}
context.ChangeTracker.DetectChanges(); // Явно вызываем обнаружение изменений
context.SaveChanges(); // Сохраняем изменения в базе данных
context.ChangeTracker.AutoDetectChangesEnabled = true; // Не забудьте снова включить
Не забывайте, что после выполнения операций вставки важно снова включить AutoDetectChangesEnabled
, чтобы EF Core мог отслеживать изменения в дальнейшем.
Перевод на русский в стиле ответа на вопрос на StackOverflow.com:
Я изучил ответ Slauma (который отличный, спасибо за идею!), и уменьшал размер пакета, пока не достиг оптимальной скорости. Обращая внимание на результаты Slauma:
- commitCount = 1, recreateContext = true: больше 10 минут
- commitCount = 10, recreateContext = true: 241 сек
- commitCount = 100, recreateContext = true: 164 сек
- commitCount = 1000, recreateContext = true: 191 сек
Можно увидеть, что скорость увеличивается при переходе от 1 к 10 и от 10 к 100, но при переходе от 100 к 1000 скорость вставки снова падает.
Поэтому я сосредоточился на том, что происходит, когда вы уменьшаете размер пакета до значения где-то между 10 и 100, и вот мои результаты (я использую разные содержимое строк, поэтому мои времена отличаются):
Количество | Размер пакета | Интервал
1000 1 3
10000 1 34
100000 1 368
1000 5 1
10000 5 12
100000 5 133
1000 10 1
10000 10 11
100000 10 101
1000 20 1
10000 20 9
100000 20 92
1000 27 0
10000 27 9
100000 27 92
1000 30 0
10000 30 9
100000 30 92
1000 35 1
10000 35 9
100000 35 94
1000 50 1
10000 50 10
100000 50 106
1000 100 1
10000 100 14
100000 100 141
Основываясь на моих результатах, фактический оптимум находится около величины 30 для размера пакета. Это меньше чем 10 и 100. Проблема в том, что я не могу понять, почему именно 30 является оптимальным, и не нашел никакого логического объяснения этому.
Обновление данных в одной таблице из другой на основе совпадения ID
Как вывести сырой SQL-запрос в виде строки из билдера запросов?
Получить строки с максимальным значением в одном столбце для каждого уникального значения другого столбца
Как восстановить дамп-файл из mysqldump?
Как вставить перенос строки в строке VARCHAR/NVARCHAR SQL Server