Как изменить медленные параметризованные вставки на быстрое массовое копирование (даже из памяти)

StackOverflow https://stackoverflow.com/questions/127152

Вопрос

У меня было что-то подобное в моем коде (.Net 2.0, MS SQL)

SqlConnection connection = new SqlConnection(@"Data Source=localhost;Initial
Catalog=DataBase;Integrated Security=True");
  connection.Open();

  SqlCommand cmdInsert = connection.CreateCommand();
  SqlTransaction sqlTran = connection.BeginTransaction();
  cmdInsert.Transaction = sqlTran;

  cmdInsert.CommandText =
     @"INSERT INTO MyDestinationTable" +
      "(Year, Month, Day, Hour,  ...) " +
      "VALUES " +
      "(@Year, @Month, @Day, @Hour, ...) ";

  cmdInsert.Parameters.Add("@Year", SqlDbType.SmallInt);
  cmdInsert.Parameters.Add("@Month", SqlDbType.TinyInt);
  cmdInsert.Parameters.Add("@Day", SqlDbType.TinyInt);
  // more fields here
  cmdInsert.Prepare();

  Stream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);

  StreamReader reader = new StreamReader(stream);
  char[] delimeter = new char[] {' '};
  String[] records;
  while (!reader.EndOfStream)
  {
    records = reader.ReadLine().Split(delimeter, StringSplitOptions.None);

    cmdInsert.Parameters["@Year"].Value = Int32.Parse(records[0].Substring(0, 4));
    cmdInsert.Parameters["@Month"].Value = Int32.Parse(records[0].Substring(5, 2));
    cmdInsert.Parameters["@Day"].Value = Int32.Parse(records[0].Substring(8, 2));
    // more here complicated stuff here
    cmdInsert.ExecuteNonQuery()
  }
  sqlTran.Commit();
  connection.Close();

С cmdInsert.ExecuteNonQuery() закомментированный, этот код выполняется менее чем за 2 секунды.При выполнении SQL это занимает 1 мин 20 сек.Всего около 0,5 миллиона записей.Таблица предварительно опустошается.Задача потока данных SSIS с аналогичной функциональностью занимает около 20 секунд.

  • Массовая вставка был не вариант (см. ниже).Во время импорта я сделал несколько причудливых вещей.
  • Моя тестовая машина — Core 2 Duo с 2 ГБ оперативной памяти.
  • При просмотре в диспетчере задач процессор не был полностью загружен.IO, похоже, также не был полностью использован.
  • Схема чертовски проста:одна таблица с AutoInt в качестве основного индекса и менее 10 целых чисел, крошечных целых чисел и символов (10).

После некоторых ответов здесь я обнаружил, что можно выполнить массовое копирование из памяти!Я отказывался использовать массовое копирование, потому что думал, что это нужно делать из файла...

Теперь я использую это, и это занимает около 20 секунд (как задача SSIS).

  DataTable dataTable = new DataTable();

  dataTable.Columns.Add(new DataColumn("ixMyIndex", System.Type.GetType("System.Int32")));   
  dataTable.Columns.Add(new DataColumn("Year", System.Type.GetType("System.Int32")));   
  dataTable.Columns.Add(new DataColumn("Month", System.Type.GetType("System.Int32")));
  dataTable.Columns.Add(new DataColumn("Day", System.Type.GetType("System.Int32")));
 // ... and more to go

  DataRow dataRow;
  object[] objectRow = new object[dataTable.Columns.Count];

  Stream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);

  StreamReader reader = new StreamReader(stream);
  char[] delimeter = new char[] { ' ' };
  String[] records;
  int recordCount = 0;
  while (!reader.EndOfStream)
  {
    records = reader.ReadLine().Split(delimeter, StringSplitOptions.None);

    dataRow = dataTable.NewRow();
    objectRow[0] = null; 
    objectRow[1] = Int32.Parse(records[0].Substring(0, 4));
    objectRow[2] = Int32.Parse(records[0].Substring(5, 2));
    objectRow[3] = Int32.Parse(records[0].Substring(8, 2));
    // my fancy stuf goes here

    dataRow.ItemArray = objectRow;         
    dataTable.Rows.Add(dataRow);

    recordCount++;
  }

  SqlBulkCopy bulkTask = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, null);
  bulkTask.DestinationTableName = "MyDestinationTable"; 
  bulkTask.BatchSize = dataTable.Rows.Count;
  bulkTask.WriteToServer(dataTable);
  bulkTask.Close();
Это было полезно?

Решение

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

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

Другие советы

Требуется транзакция?Для использования транзакции требуется гораздо больше ресурсов, чем для простых команд.

Также, если вы уверены, что вставленные значения верны, вы можете использовать BulkInsert.

1 минута звучит вполне разумно для 0,5 миллиона записей.Это рекорд каждые 0,00012 секунды.

Есть ли у таблицы индексы?Их удаление и повторное применение после массовой вставки улучшит производительность вставок, если это возможно.

Мне не кажется неразумным обрабатывать 8333 записи в секунду... какую пропускную способность вы ожидаете?

Если вам нужна более высокая скорость, вы можете рассмотреть возможность реализации массовой вставки:

http://msdn.microsoft.com/en-us/library/ms188365.aspx

Если какая-либо форма массовой вставки невозможна, другим способом может быть несколько потоков, каждый из которых имеет собственное соединение с базой данных.

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

Если вы сможете разделить работу, возможно, используя какую-то форму настройки производитель/потребитель, вы можете обнаружить, что можете гораздо лучше использовать все ресурсы.

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

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

Я предполагаю, что примерно 58 секунд занимает физическая вставка 500 000 записей, то есть вы получаете около 10 000 вставок в секунду.Не зная характеристик вашего сервера базы данных (я вижу, вы используете localhost, поэтому задержки в сети не должны быть проблемой), трудно сказать, хорошо это, плохо или ужасно.

Я бы посмотрел на вашу схему базы данных: есть ли в таблице куча индексов, которые нужно обновлять после каждой вставки?Это могут быть другие таблицы, внешние ключи которых ссылаются на таблицу, над которой вы работаете.В SQL Server встроены инструменты профилирования SQL и средства мониторинга производительности, но я никогда ими не пользовался.Но они могут обнаруживать такие проблемы, как блокировки и тому подобное.

Сначала выполните сложные действия с данными, со всеми записями.Затем выполните массовую вставку.

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

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

  1. Первичный ключ — это GUID, а первичный индекс — CLUSTERED.
  2. Для набора полей есть какой-то УНИКАЛЬНЫЙ индекс.
  3. В таблице слишком много индексов.

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

Я также отмечу, что если у вас есть возможность преобразовать поля «Год», «Месяц», «День», «Час», «Минута», «Секунда» в одно поле даты и времени2 или отметки времени, вам следует это сделать.Вы усложняете свою архитектуру данных без всякой выгоды.Единственная причина, по которой я бы даже рассмотрел возможность использования подобной структуры с разделенными полями, заключается в том, что вы имеете дело с уже существующей схемой базы данных, которую нельзя изменить по какой-либо причине.В этом случае хреново быть тобой.

У меня была похожая проблема в моем последнем контракте.Вы совершаете 500 000 обращений к SQL для вставки данных.Для значительного увеличения производительности вам необходимо изучить метод BulkInsert в пространстве имен SQL.У меня были процессы «перезагрузки», которые занимали от 2+ часов на восстановление пары десятков таблиц до 31 секунды после того, как я реализовал массовый импорт.

Лучше всего это можно сделать с помощью чего-то вроде команды bcp.Если это недоступно, лучше всего подойдут приведенные выше рекомендации по использованию BULK INSERT.Вы совершаете 500 000 обращений к базе данных и записываете 500 000 записей в файлы журналов, не говоря уже о пространстве, которое необходимо выделить для файла журнала, таблицы и индексов.

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

~10 000 транзакций в секунду не так уж и страшны для отдельных вставок, поступающих из кода/

BULK INSERT = bcp из разрешения

Вы можете оставить вставки, чтобы уменьшить обратные познания sqldataadaptor.updatebatchsize = 10000 дает 50 круглых поездок

Хотя у вас еще есть 500 тысяч вставок...

Статья

MSDN

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top