Несколько операторов INSERT против одного INSERT с несколькими значениями - PullRequest
115 голосов
/ 26 декабря 2011

Я выполняю сравнение производительности между использованием 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)

.. и использованием одного оператора 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 ЗНАЧЕНИЯМИ: 2800 мс.

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

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

Спасибо.

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

Ответы [ 4 ]

121 голосов
/ 27 декабря 2011

Добавление: SQL Server 2012 показывает некоторую улучшенную производительность в этой области, но, похоже, не решает конкретные проблемы, отмеченные ниже. это должно быть исправлено в следующей основной версии после 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   

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

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

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

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

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

Graph

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

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

Единственное место, где эта информация используется, это , показанный @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) и не делитсянулевая ошибка повышена.Если дубликаты добавляются в таблицу, то оператор сортировки отображает два порядка по столбцам и возникает ожидаемая ошибка.

22 голосов
/ 26 декабря 2011

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

9 голосов
/ 27 декабря 2011

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

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

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 голос
/ 01 июня 2016

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

...