Несколько операторов INSERT против одного INSERT с несколькими значениями

119

Я провожу сравнение производительности при использовании 1000 операторов 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 операторов INSERT: 290 мсек.
  • 1 оператор INSERT с 1000 VALUES: 2800 мсек.

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

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

Спасибо.

РЕДАКТИРОВАТЬ: прикрепление планов выполнения для обоих: Планы Exec

Борька
источник
1
это чистые тесты, ничего не выполняется параллельно, нет повторяющихся данных (каждый запрос, конечно, с разными данными, чтобы избежать простого кеширования)
Борка
1
задействованы ли какие-либо триггеры?
AK
2
Я преобразовал программу в TVP, чтобы преодолеть ограничение в 1000 значений и получить большой прирост производительности. Проведу сравнение.
paparazzo
1
актуально: simple-talk.com/sql/performance/…
неизвестно

Ответы:

126

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

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

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

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

график

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

график

Но затем происходит внезапное изменение.

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

+------+----------------+-------------+---------------+---------------+
| 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 параметрами) и непараметризованным планом. После этого он, кажется, становится линейно менее эффективным (с точки зрения количества предложений значений, обрабатываемых за заданное время).

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

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

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

План

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

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

В этой статье базы знаний указано, что DeriveNormalizedGroupPropertiesэто связано с тем, что раньше называлось нормализацией. этапом обработки запросов.

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

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

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

график

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

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

Одно место, где используется эта информация, показано здесь @Lieven.

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

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

Мартин Смит
источник
6
У вас есть магическое число NumberOfRows / ColumnCount = 250. Измените свой запрос, чтобы использовать только три столбца, и изменение произойдет на 333. Магическое число 1000 может быть чем-то вроде максимального количества параметров, используемых в кэшированном плане. Это швы , чтобы быть «проще» , чтобы создать план с <ParameterList>чем один с <ConstantScan><Values><Row>списком.
Микаэль Эрикссон
1
@MikaelEriksson - Согласен. 250-строчная с 1000 значениями автоматически параметризуется, а 251-я строка - нет, так что, похоже, это разница. Хотя не знаю почему. Возможно, он тратит время на сортировку буквальных значений в поисках дубликатов или чего-то еще, когда они у него есть.
Мартин Смит
1
Это довольно безумная проблема, я просто огорчился из-за этого. Это отличный ответ, спасибо
не любил
1
@MikaelEriksson Вы имеете в виду, что магическое число NumberOfRows * ColumnCount = 1000?
paparazzo
1
@Blam - Да. Когда общее количество элементов превышает 1000 (NumberOfRows * ColumnCount), план запроса изменяется на использование <ConstantScan><Values><Row>вместо <ParameterList>.
Микаэль Эрикссон
23

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

dasblinkenlight
источник
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 # вы также можете рассмотреть возможность использования параметра со значением таблицы. Выдача нескольких команд в одном пакете путем разделения их точкой с запятой - еще один подход, который также может помочь.

RickNZ
источник
1
Re: "Выполнение нескольких команд в одном пакете": это немного помогает, но не сильно. Но я определенно согласен с двумя другими вариантами либо обертывания в ТРАНЗАКЦИИ (действительно ли TRANS работает, или это просто TRAN?), Либо с использованием TVP.
Соломон Рутцки
1

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

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

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

        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.

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

uceumern
источник