Кілька операторів 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 ЦІННОСТІ: 2800 мсек.

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

Чи може це бути розумним чи якось пояснити? Як же, нібито швидший метод призводить до 10-ти разів (!) Гіршої продуктивності?

Дякую.

EDIT: Додавання планів виконання обох: Exec плани


1
це чисті тести, нічого паралельно не виконується, повторних даних немає (кожен запит має різні дані, звичайно, щоб уникнути простого кешування)
Borka

1
чи задіяні тригери?
АК

2
Я перетворив програму на TVP, щоб подолати ліміт 1000 значень і отримав великий приріст продуктивності. Я проведу порівняння.
папараццо

Відповіді:


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   

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

Ця стаття KB вказує, що 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?
папараццо

1
@Blam - Так. Коли загальна кількість елементів перевищує 1000 (NumberOfRows * ColumnCount), план запитів змінився на використання <ConstantScan><Values><Row>замість <ParameterList>.
Мікаель Ерікссон

23

Це не надто дивно: план виконання крихітної вставки обчислюється один раз, а потім повторно використовується 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: "Випуск декількох команд в одній партії": це допомагає мало, але не багато. Але я безумовно погоджуюся з іншими двома варіантами або загортання в ПЕРЕДАКЦІЮ (чи дійсно TRANS працює, чи це просто TRAN?), Або використання TVP.
Соломон Руцький

1

Я зіткнувся з подібною ситуацією, намагаючись перетворити таблицю з декількома рядками 100k за допомогою програми 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.