Производительность bcp / BULK INSERT против параметров с табличным значением

84

Мне вот-вот придется переписать довольно старый код с помощью команды SQL Server, BULK INSERTпотому что схема изменилась, и мне пришло в голову, что, возможно, мне стоит подумать о переключении на хранимую процедуру с TVP, но мне интересно, какой эффект это могло сказаться на производительности.

Некоторая справочная информация, которая может помочь объяснить, почему я задаю этот вопрос:

  • Фактически данные поступают через веб-службу. Веб-служба записывает текстовый файл в общую папку на сервере базы данных, который, в свою очередь, выполняет BULK INSERT. Этот процесс изначально был реализован в SQL Server 2000, и в то время действительно не было альтернативы, кроме передачи нескольких сотен INSERTоператоров на сервер, что на самом деле было исходным процессом и приводило к катастрофе.

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

  • Объем данных для вставки «большой», но не «огромный» - обычно несколько сотен строк, в редких случаях может быть до 5-10 тыс. Строк. Поэтому мое шестое чувство, что BULK INSERTбудучи не-авторизовались операция не будет делать , что большой разницы (но, конечно , я не уверен, отсюда и вопрос).

  • Вставка на самом деле является частью гораздо более крупного конвейерного пакетного процесса и должна происходить много раз подряд; поэтому производительность имеет решающее значение.

Причины, по которым я хотел бы заменить BULK INSERTTVP:

  • Написание текстового файла через NetBIOS, вероятно, уже требует некоторого времени, и это довольно ужасно с архитектурной точки зрения.

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

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

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

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

Итак - каков вердикт для всех, кто достаточно знаком с SQL Server 2008, чтобы попытаться или хотя бы исследовать это? Для вставок, скажем, от нескольких сотен до нескольких тысяч строк, происходящих довольно часто, режут ли TVP горчицу? Есть ли существенная разница в производительности по сравнению с объемными вставками?


Обновление: теперь вопросительных знаков на 92% меньше!

(AKA: Результаты испытаний)

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

  • Извлечение кода из общей папки и SqlBulkCopyпрямое использование класса;
  • Переключение на хранимую процедуру с TVP.

Чтобы читатели могли понять, что именно было протестировано, чтобы развеять любые сомнения относительно надежности этих данных, вот более подробное объяснение того, что на самом деле делает этот процесс импорта :

  1. Начните с временной последовательности данных, которая обычно составляет около 20-50 точек данных (хотя иногда может быть несколько сотен);

  2. Проделайте над ним кучу сумасшедших операций, которые в основном не зависят от базы данных. Этот процесс распараллелен, поэтому около 8-10 последовательностей в (1) обрабатываются одновременно. Каждый параллельный процесс генерирует 3 дополнительных последовательности.

  3. Возьмите все 3 последовательности и исходную последовательность и объедините их в пакет.

  4. Объедините пакеты из всех 8-10 завершенных задач обработки в один большой суперпакет.

  5. Импортируйте его, используя либо BULK INSERTстратегию (см. Следующий шаг), либо стратегию TVP (переходите к шагу 8).

  6. Используйте этот SqlBulkCopyкласс, чтобы выгрузить весь суперпакет в 4 постоянные промежуточные таблицы.

  7. Запустите хранимую процедуру, которая (а) выполняет набор этапов агрегирования для 2 таблиц, включая несколько JOINусловий, а затем (б) выполняет операцию для MERGE6 производственных таблиц, используя как агрегированные, так и неагрегированные данные. (Законченный)

    ИЛИ ЖЕ

  8. Сгенерировать 4 DataTableобъекта, содержащих данные, которые нужно объединить; 3 из них содержат типы CLR, которые, к сожалению, не поддерживаются должным образом ADO.NET TVP, поэтому их приходится использовать как строковые представления, что немного снижает производительность.

  9. Подайте TVP в хранимую процедуру, которая, по сути, выполняет ту же обработку, что и (7), но напрямую с полученными таблицами. (Законченный)

Результаты были достаточно близкими, но подход TVP в конечном итоге работал лучше в среднем, даже когда данные превышали 1000 строк на небольшой объем.

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

Первоначально среднее слияние занимало почти 8 секунд (при нормальной нагрузке). Удаление кладжа NetBIOS и переключение на SqlBulkCopyсократили время почти до 7 секунд. Переход на TVP еще больше сократил время до 5,2 секунды на пакет. Это на 35% увеличение пропускной способности для процесса, время выполнения которого измеряется часами - это совсем неплохо. Это также улучшение на ~ 25% SqlBulkCopy.

На самом деле я совершенно уверен, что настоящее улучшение было значительно большим, чем это. Во время тестирования стало очевидно, что окончательное слияние больше не является критическим путем; вместо этого веб-служба, которая выполняла всю обработку данных, начинала сгибаться из-за количества поступающих запросов. Ни ЦП, ни ввод-вывод базы данных не были полностью загружены, и не было значительной активности блокировки. В некоторых случаях мы наблюдали паузу в несколько секунд простоя между последовательными слияниями. Был небольшой промежуток, но намного меньше (полсекунды или около того) при использовании SqlBulkCopy. Но я полагаю, это станет сказкой на другой день.

Вывод. Табличные параметры действительно работают лучше, чем BULK INSERTоперации для сложных процессов импорта + преобразования, работающих с наборами данных среднего размера.


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

Другими словами, у нас уже более чем достаточно информации о процессе и нам не нужна безопасность промежуточного стола; единственная причина , почему мы имели промежуточную таблицу , в первую очередь, чтобы избежать пробуксовки на всех INSERTи UPDATEзаявлений , которые мы должны были бы использовать иначе. В исходном процессе промежуточные данные все равно жили в промежуточной таблице лишь доли секунды, поэтому они не добавляли ценности с точки зрения обслуживания / ремонтопригодности.

Также обратите внимание, что мы не заменили каждую BULK INSERTоперацию на TVP. Некоторые операции, которые имеют дело с большими объемами данных и / или не нуждаются в каких-либо специальных действиях с данными, кроме передачи их в БД, все еще используются SqlBulkCopy. Я не предполагаю, что TVP являются панацеей производительности, только то, что они преуспели SqlBulkCopyв этом конкретном случае, включающем несколько преобразований между начальным этапом и окончательным слиянием.

Вот и все. За поиск наиболее релевантной ссылки следует обратить внимание на TToni, но я также ценю другие ответы. Еще раз спасибо!

Aaronaught
источник
Это удивительный вопрос сам по себе, я чувствую, что в ответе должно быть обновление;)
Marc.2377,

Ответы:

10

Я действительно не имею опыта работы с TVP еще, однако есть хорошее сравнение производительности графика по сравнению с BULK INSERT в MSDN здесь .

Говорят, что BULK INSERT имеет более высокую стоимость запуска, но после этого работает быстрее. В сценарии с удаленным клиентом они рисуют линию примерно из 1000 строк (для «простой» логики сервера). Судя по их описанию, я бы сказал, что у вас должно быть нормально использовать TVP. Падение производительности - если оно вообще есть - вероятно, незначительно, а архитектурные преимущества кажутся очень хорошими.

Изменить: с другой стороны, вы можете избежать локального файла на сервере и по-прежнему использовать массовое копирование с помощью объекта SqlBulkCopy. Просто заполните DataTable и передайте его методу «WriteToServer» экземпляра SqlBulkCopy. Легко использовать и очень быстро.

TToni
источник
Спасибо за ссылку, это действительно очень полезно, поскольку MS, кажется, рекомендует TVP, когда данные подают сложную логику (что она делает), и у нас также есть возможность увеличивать или уменьшать размер пакета, чтобы мы не заходили слишком далеко за пределы 1к рядная болевая точка. Исходя из этого, возможно, стоит потратить время, по крайней мере, на то, чтобы попробовать и увидеть, даже если в конечном итоге это окажется слишком медленным.
Aaronaught
Да ссылка интересная. @Aaronaught - в подобных ситуациях всегда стоит изучить и проанализировать эффективность потенциальных подходов, поэтому мне было бы интересно услышать ваши выводы!
AdaTheDev
7

Таблицу, упомянутую в отношении ссылки, предоставленной в ответе @ TToni, необходимо рассматривать в контексте. Я не уверен, сколько фактических исследований было посвящено этим рекомендациям (также обратите внимание, что диаграмма, похоже, доступна только в версиях 2008и 2008 R2этой документации).

С другой стороны, есть технический документ от группы консультирования клиентов SQL Server: Максимизация пропускной способности с помощью TVP.

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

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

Что касается тестирования, проведенного в Вопросе, я думаю, что оно может быть даже быстрее, чем было обнаружено изначально:

  1. Вы не должны использовать DataTable, если ваше приложение не использует его за пределами отправки значений в TVP. Использование IEnumerable<SqlDataRecord>интерфейса происходит быстрее и требует меньше памяти, поскольку вы не дублируете коллекцию в памяти только для отправки ее в БД. Я зарегистрировал это в следующих местах:
  2. TVP являются табличными переменными и поэтому не поддерживают статистику. Это означает, что они сообщают оптимизатору запросов только одну строку. Итак, в вашей процедуре:
    • Используйте перекомпиляцию на уровне операторов для любых запросов, использующих TVP для чего-либо, кроме простого SELECT: OPTION (RECOMPILE)
    • Создайте локальную временную таблицу (т.е. одиночную #) и скопируйте содержимое TVP во временную таблицу
Соломон Руцки
источник
4

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

Вы не упоминаете, используете ли вы .NET, но подход, который я использовал для оптимизации предыдущих решений, заключался в выполнении массовой загрузки данных с использованием класса SqlBulkCopy - вам не нужно сначала записывать данные в файл, прежде чем загрузки, просто дайте классу SqlBulkCopy (например) DataTable - это самый быстрый способ вставки данных в БД. 5-10К строк - это немного, я использовал это до 750К строк. Я подозреваю, что в целом с несколькими сотнями строк это не будет иметь большого значения при использовании TVP. Но масштабирование было бы ограниченным ИМХО.

Может быть, вам принесет пользу новая функция MERGE в SQL 2008?

Кроме того, если ваша существующая промежуточная таблица представляет собой единую таблицу, которая используется для каждого экземпляра этого процесса, и вы беспокоитесь о разногласиях и т. Д., Рассматривали ли вы каждый раз создание новой «временной», но физической промежуточной таблицы, а затем отбрасывать ее, когда она закончил с?

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

AdaTheDev
источник
Я использую .NET, и этот процесс произошел SqlBulkCopyраньше и просто никогда не менялся. Спасибо, что напомнили мне об этом, возможно, стоит вернуться к нему. MERGEтакже уже широко используется, и временные таблицы уже пробовали однажды, но оказалось, что они медленнее и сложнее в управлении. Спасибо за вклад!
Aaronaught
-2

Столы для постановки хороши! На самом деле я бы не хотел этого делать по-другому. Почему? Поскольку импорт данных может неожиданно измениться (и часто способами, которые вы не можете предвидеть, например, когда столбцы по-прежнему назывались именем и фамилией, но имели данные имени в столбце фамилии, например, чтобы выбрать пример, не наугад.) Легко исследовать проблему с помощью промежуточной таблицы, чтобы вы могли точно видеть, какие данные были в столбцах, обработанных импортом. Я думаю, что труднее найти, когда вы используете таблицу в памяти. Я знаю много людей, которые зарабатывают на жизнь импортом, и все они рекомендуют использовать промежуточные таблицы. Я подозреваю, что этому есть причина.

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

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

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

HLGEM
источник
27
SQL 2008 был вокруг в течение 2 -х лет , и этот процесс был вокруг в течение веков, и это первый раз , когда я даже рассмотрел изменения. Был ли нужен язвительный комментарий в конце?
Aaronaught