8

Самый Быстрый Способ Вставки в Entity Framework

4

Я ищу самый быстрый способ вставки данных в Entity Framework.

Я задаю этот вопрос в связи со сценарием, когда у вас есть активный TransactionScope, и объем вставляемых данных очень большой (более 4000 записей). Процесс может занять более 10 минут (это стандартное время ожидания для транзакций), что приведет к незавершенной транзакции.

5 ответ(ов)

11

В ответ на ваш комментарий к вашему вопросу:

"...Сохранение изменений (для каждой записи)..."

Это худшее, что вы можете сделать! Вызов метода 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 минут). Эта нелинейность менее значительна в остальных тестах.

1

Данная комбинация действительно позволяет существенно увеличить скорость работы с контекстом Entity Framework. Отключение функции автоматического обнаружения изменений (AutoDetectChangesEnabled = false) предотвращает ненужные проверки на изменения сущностей перед сохранением в базу данных, что может значительно сократить время выполнения операций.

Дополнительно отключение проверки на валидность при сохранении (ValidateOnSaveEnabled = false) также экономит ресурсы, так как устраняется необходимость проверки валидности всех сущностей, что может быть излишним в некоторых случаях, например, если вы уверены в корректности данных.

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

0

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

0

В ответ на ваш вопрос по поводу оптимизации вставки записей в 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 мог отслеживать изменения в дальнейшем.

0

Перевод на русский в стиле ответа на вопрос на 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 является оптимальным, и не нашел никакого логического объяснения этому.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь