2011-12-26 3 views
98

Я бег сравнения производительности между использованием 1000 INSERT заявления:Несколько операторов INSERT против одного INSERT с несколькими значениями

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0) 
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1) 
... 
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999) 

..versus используя один оператор INSERT с 1000 значений:

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
VALUES 
('db72b358-e9b5-4101-8d11-7d7ea3a0ae7d', 'First 0', 'Last 0', 0), 
('6a4874ab-b6a3-4aa4-8ed4-a167ab21dd3d', 'First 1', 'Last 1', 1), 
... 
('9d7f2a58-7e57-4ed4-ba54-5e9e335fb56c', 'First 999', 'Last 999', 999) 

К моему большому удивлению, результаты противоположны тому, что я думал:

  • 1000 ВСТАВКИ: 290 мс.
  • 1 ЗАЯВКА INSERT с 1000 ЗНАЧЕНИЯМИ: 2800 мс.

Тест выполняется непосредственно в MSSQL Management Studio с SQL Server Profiler используется для измерения (и я получил аналогичные результаты Забегая из C# кода с помощью SqlClient, который еще более удивительно, учитывая все DAL слои туда и обратно)

Может ли это быть разумным или каким-то образом объяснить? Почему, предположительно быстрый метод приводит к 10 раз (!) хуже производительность?

спасибо.

EDIT: Прикрепление планы выполнения как: Exec Plans

+1

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

+1

есть ли какие-либо триггеры? –

+1

Я преобразовал программу в TVP, чтобы преодолеть предел 1000 значений и получил большой прирост производительности. Я проведу сравнение. – Paparazzi

ответ

107

Дополнение SQL Server 2012 показывает некоторые улучшения производительности в этой области, но, кажется, не для решения конкретных проблем, отмеченных ниже. Этот должен apparently be fixed в следующей крупной версии после SQL Server 2012!

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

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

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

Graph

До 250 VALUES положения не представляют компиляции время/количество статей имеет небольшой восходящий тренд, но ничего слишком драматическое.

Graph

Но есть внезапное изменение.

Этот раздел данных показан ниже.

+------+----------------+-------------+---------------+---------------+ 
| Rows | CachedPlanSize | CompileTime | CompileMemory | Duration/Rows | 
+------+----------------+-------------+---------------+---------------+ 
| 245 |   528 |   41 |   2400 | 0.167346939 | 
| 246 |   528 |   40 |   2416 | 0.162601626 | 
| 247 |   528 |   38 |   2416 | 0.153846154 | 
| 248 |   528 |   39 |   2432 | 0.157258065 | 
| 249 |   528 |   39 |   2432 | 0.156626506 | 
| 250 |   528 |   40 |   2448 | 0.16   | 
| 251 |   400 |   273 |   3488 | 1.087649402 | 
| 252 |   400 |   274 |   3496 | 1.087301587 | 
| 253 |   400 |   282 |   3520 | 1.114624506 | 
| 254 |   408 |   279 |   3544 | 1.098425197 | 
| 255 |   408 |   290 |   3552 | 1.137254902 | 
+------+----------------+-------------+---------------+---------------+ 

Кэшированного размер план, который был растет линейно внезапно падает, но CompileTime увеличивается в 7 раз и CompileMemory взмывает вверх. Это точка отсечения между планом, являющимся автоматической параметризованной (с 1000 параметрами), к не параметризованной. После этого он становится линейно менее эффективным (с точки зрения количества предложений стоимости, обработанных за данный момент времени).

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

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

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

Plan

Я попытался посмотреть на это в отладчике, но государственные символы для моей версии SQL Server 2008, кажется, не будут доступны так что вместо этого я должен был смотреть на эквивалентном UNION ALL строительства в SQL Server 2005.

типичный трассировки стека ниже

sqlservr.exe!FastDBCSToUnicode() + 0xac bytes 
sqlservr.exe!nls_sqlhilo() + 0x35 bytes  
sqlservr.exe!CXVariant::CmpCompareStr() + 0x2b bytes 
sqlservr.exe!CXVariantPerformCompare<167,167>::Compare() + 0x18 bytes 
sqlservr.exe!CXVariant::CmpCompare() + 0x11f67d bytes 
sqlservr.exe!CConstraintItvl::PcnstrItvlUnion() + 0xe2 bytes 
sqlservr.exe!CConstraintProp::PcnstrUnion() + 0x35e bytes 
sqlservr.exe!CLogOp_BaseSetOp::PcnstrDerive() + 0x11a bytes  
sqlservr.exe!CLogOpArg::PcnstrDeriveHandler() + 0x18f bytes  
sqlservr.exe!CLogOpArg::DeriveGroupProperties() + 0xa9 bytes 
sqlservr.exe!COpArg::DeriveNormalizedGroupProperties() + 0x40 bytes  
sqlservr.exe!COptExpr::DeriveGroupProperties() + 0x18a bytes 
sqlservr.exe!COptExpr::DeriveGroupProperties() + 0x146 bytes 
sqlservr.exe!COptExpr::DeriveGroupProperties() + 0x146 bytes 
sqlservr.exe!COptExpr::DeriveGroupProperties() + 0x146 bytes 
sqlservr.exe!CQuery::PqoBuild() + 0x3cb bytes 
sqlservr.exe!CStmtQuery::InitQuery() + 0x167 bytes 
sqlservr.exe!CStmtDML::InitNormal() + 0xf0 bytes 
sqlservr.exe!CStmtDML::Init() + 0x1b bytes 
sqlservr.exe!CCompPlan::FCompileStep() + 0x176 bytes 
sqlservr.exe!CSQLSource::FCompile() + 0x741 bytes 
sqlservr.exe!CSQLSource::FCompWrapper() + 0x922be bytes  
sqlservr.exe!CSQLSource::Transform() + 0x120431 bytes 
sqlservr.exe!CSQLSource::Compile() + 0x2ff bytes 

Так происходит от имен в стеке след оказывается тратить много времени сравнения строк.

This KB article указывает на то, что DeriveNormalizedGroupProperties связано с тем, что раньше называли normalization этап обработки запроса

Этот этап теперь называется обязательным или algebrizing и он принимает выходной вал выражение разбора из предыдущей стадии синтаксического анализа, и выводит (дерево процессоров запросов), чтобы перейти к оптимизации (в этом случае оптимизация тривиального плана) [ref].

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

  1. Имя и фамилия Строки длиной 10 символов без дубликатов.
  2. Имя и фамилия Строки длиной 50 символов без дубликатов.
  3. Имя и фамилия Строки длиной 10 символов со всеми дубликатами.

Graph

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

Редактировать

Одно места, где заемные средствами этой информации shown by @Lieven here

SELECT * 
FROM (VALUES ('Lieven1', 1), 
      ('Lieven2', 2), 
      ('Lieven3', 3))Test (name, ID) 
ORDER BY name, 1/ (ID - ID) 

Потому что во время компиляции он может определить, что Name столбца не имеет дубликатов он пропускает заказ вторичного 1/ (ID - ID) выражения в (сортировка в плане имеет только один столбец ORDER BY), и ошибка деления на нуль повышается. Если в таблицу добавлены дубликаты, то оператор сортировки показывает два порядка по столбцам, и ожидаемая ошибка повышается.

+5

Магическое число, которое у вас есть, - NumberOfRows/ColumnCount = 250. Измените свой запрос, чтобы использовать только три столбца, и изменение произойдет на 333. Магическое число 1000 может быть чем-то вроде максимального количества параметров, используемых в кешированном плане.Он «проще» генерировать план с «», чем один с '' ConstantScan> 'list. –

+1

@MikaelEriksson - Согласовано. 250 строк с 1000 значениями автоматически присваиваются параметрам 251 строка не так, как будто это разница. Не знаю, почему. Возможно, он проводит время, сортируя литературные значения, которые ищут дубликаты или что-то, когда они есть. –

+1

Это довольно сумасшедшая проблема, и я просто ее огорчил. Это отличный ответ спасибо –

21

Это не удивительно: план выполнения для крошечного вставки вычисляется один раз, а затем повторно в 1000 раз. Анализ и подготовка плана выполняется быстро, поскольку он имеет только четыре значения. С другой стороны, план 1000 рядов должен иметь дело с 4000 значениями (или 4000 параметров, если вы параметризировали свои тесты C#). Это может легко съесть экономию времени, которую вы получите, исключив 999 обращений к SQL Server, особенно если ваша сеть не слишком медленная.

9

Проблема, вероятно, связана с временем, которое требуется для компиляции запроса.

Если вы хотите ускорить вставки, что вам действительно нужно сделать, это обернуть их в сделке:

BEGIN TRAN; 
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0); 
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1); 
... 
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
    VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999); 
COMMIT TRAN; 

От C#, вы можете также рассмотреть возможность использования таблицы значных параметра. Выдача нескольких команд в одной партии, разделяя их точкой с запятой, является еще одним подходом, который также поможет.

+1

Re: «Выдача несколько команд в одной партии »: это помогает немного, но не много. Но я определенно согласен с двумя другими вариантами либо обертывания в транзакции (действительно ли ТРАНС работает или должен быть просто TRAN?) Или с помощью TVP. –

1

Я столкнулся с аналогичной ситуацией, пытаясь преобразовать таблицу с несколькими 100k строками с помощью C++-программы (MFC/ODBC).

Поскольку эта операция заняла очень много времени, я решил объединить несколько вставок в один (до 1000 из-за MSSQL limitations). Мое предположение, что множество одиночных операторов вставки создавали бы служебные данные, похожие на то, что описано here.

Однако, оказывается, что превращение произошло на самом деле совсем немного дольше:

 Method 1  Method 2  Method 3 
     Single Insert Multi Insert Joined Inserts 
Rows 1000   1000   1000 
Insert 390 ms   765 ms  270 ms 
per Row 0.390 ms  0.765 ms  0.27 ms 

Таким образом, 1000 одиночных вызовов на CDatabase :: ExecuteSQL каждый с одним INSERT заявления (метод 1) примерно в два раза быстрый как один вызов в CDatabase :: ExecuteSql с многострочным оператором INSERT с 1000 кортежами значений (метод 2).

Обновление: Итак, следующая вещь, которую я пробовал, заключалась в том, чтобы объединить 1000 отдельных инструкций INSERT в одну строку и выполнить сервер (метод 3). Оказывается, это даже немного быстрее, чем метод 1.

Edit: Я использую Microsoft SQL Server Express Edition (64-разрядная версия) v10.0.2531.0

Смежные вопросы