Повільна продуктивність, вставляючи кілька рядків у величезну таблицю


9

У нас є процес, який забирає дані з магазинів та оновлює таблицю інвентаризацій для всієї компанії. Ця таблиця містить рядки для кожного магазину за датою та за предметом. У клієнтів з багатьма магазинами цей стіл може вийти дуже великим - порядку 500 мільйонів рядів.

Цей процес оновлення запасів зазвичай запускається багато разів на день, коли магазини вводять дані. Ці запуски оновлюють дані лише з кількох магазинів. Однак клієнти також можуть запустити це, щоб оновити, скажімо, всі магазини за останні 30 днів. У цьому випадку процес накручується на 10 потоків і оновлює інвентар кожного магазину окремим потоком.

Замовник скаржиться, що процес займає тривалий час. Я профайлював процес і виявив, що один запит, що ВСТАВКИ в цю таблицю, займає набагато більше часу, ніж я очікував. Цей INSERT іноді завершується за 30 секунд.

Коли я запускаю спеціальну команду SQL INSERT проти цієї таблиці (обмежена BEGIN TRAN і ROLLBACK), спеціальна SQL завершується на порядок мілісекунд.

Нижче показано повільний запит. Ідея полягає в тому, щоб ВСТАВИТИ записи, яких немає, а пізніше ОНОВЛЕННЯ їх, коли ми обчислюємо різні біти даних. Попереднім кроком у процесі виявлено елементи, які потрібно оновити, виконати деякі обчислення та внести результати в таблицю tempdb Update_Item_Work. Цей процес працює в 10 окремих потоках, і кожен потік має свій GUID у Update_Item_Work.

INSERT INTO Inventory
(
    Inv_Site_Key,
    Inv_Item_Key,
    Inv_Date,
    Inv_BusEnt_ID,
    Inv_End_WtAvg_Cost
)
SELECT DISTINCT
    UpdItemWrk_Site_Key,
    UpdItemWrk_Item_Key,
    UpdItemWrk_Date,
    UpdItemWrk_BusEnt_ID,
    (CASE UpdItemWrk_Set_WtAvg_Cost WHEN 1 THEN UpdItemWrk_WtAvg_Cost ELSE 0 END)
FROM tempdb..Update_Item_Work (NOLOCK)
WHERE UpdItemWrk_GUID = @GUID
AND NOT EXISTS
    -- Only insert for site/item/date combinations that don't exist
    (SELECT *
    FROM Inventory (NOLOCK)
    WHERE Inv_Site_Key = UpdItemWrk_Site_Key
    AND Inv_Item_Key = UpdItemWrk_Item_Key
    AND Inv_Date = UpdItemWrk_Date)

Таблиця з інвентаризацією містить 42 стовпці, більшість з яких відстежує кількість та враховує різні коригування запасів. sys.dm_db_index_physical_stats говорить, що кожен рядок становить близько 242 байтів, тому я очікую, що на одній сторінці в 8 кб розміститься близько 33 рядків.

Таблиця кластеризована на унікальному обмеженні (Inv_Site_Key, Inv_Item_Key, Inv_Date). Усі клавіші DECIMAL (15,0), а дата SMALLDATETIME. Є первинний ключ IDENTITY (без кластера) та 4 інші індекси. Всі індекси та кластерне обмеження визначаються явним (FILLFACTOR = 90, PAD_INDEX = ON).

Я заглянув у файл журналу, щоб підрахувати розбиття сторінки. Я виміряв приблизно 1027 розщеплення за кластерним індексом та 1724 розщеплення за іншим індексом, але я не записав, за який інтервал вони відбулися. Через півтори години я виміряв 7 035 розділів сторінки на кластерному індексі.

План запитів, який я захопив у профілі, виглядає приблизно так:

Rows         Executes     StmtText                                                                                                                                             
----         --------     --------                                                                                                                                             
490          1            Sequence                                                                                                                                             
0            1              |--Index Update
0            1              |    |--Collapse
0            1              |         |--Sort
0            1              |              |--Filter
996          1              |                   |--Table Spool                                                                                                                 
996          1              |                        |--Split                                                                                                                  
498          1              |                             |--Assert
0            0              |                                  |--Compute Scalar
498          1              |                                       |--Clustered Index Update(UK_Inventory)
498          1              |                                            |--Compute Scalar
0            0              |                                                 |--Compute Scalar
0            0              |                                                      |--Compute Scalar
498          1              |                                                           |--Compute Scalar
498          1              |                                                                |--Top
498          1              |                                                                     |--Nested Loops
498          1              |                                                                          |--Stream Aggregate
0            0              |                                                                          |    |--Compute Scalar
498          1              |                                                                          |         |--Clustered Index Seek(tempdb..Update_Item_Work)
498          498            |                                                                          |--Clustered Index Seek(Inventory)
0            1              |--Index Update(UX_Inv_Exceptions_Date_Site_Item)
0            1              |    |--Collapse
0            1              |         |--Sort
0            1              |              |--Filter
996          1              |                   |--Table Spool
490          1              |--Index Update(UX_Inv_Date_Site_Item)
490          1                   |--Collapse
980          1                        |--Sort
980          1                             |--Filter
996          1                                  |--Table Spool                                                                                       

Переглядаючи запити та різні dmv, я бачу, що запит очікується на PAGEIOLATCH_EX тривалістю 0 на сторінці цієї таблиці інвентаризації. Я не бачу жодного очікування чи блокування на замки.

Ця машина має близько 32 ГБ пам'яті. У ньому працює стандартне видання SQL Server 2005, хоча вони незабаром оновлюються до версії 2008 R2 Enterprise Edition. У мене немає номерів, наскільки велика таблиця інвентаризації з точки зору використання диска, але я можу отримати це, якщо потрібно. Це одна з найбільших таблиць у цій системі.

Я запустив запит проти sys.dm_io_virtual_file_stats і побачив, що середнє очікування запису проти tempdb буде на 1,1 секунди . База даних, в якій зберігається ця таблиця, має середнє очікування запису ~ 350 мс. Але вони перезапускають сервер лише кожні 6 місяців або близько того, тому я не маю уявлення, чи ця інформація є актуальною. tempdb поширюється на 4 різні файли. У них є 3 різних файли для бази даних, що містить таблицю інвентаризації.

Чому цей запит забирає стільки часу, щоб ВСТАВИТИ декілька рядків, коли він працює з багатьма різними потоками, коли один INSERT дуже швидкий?

- ОНОВЛЕННЯ -

Ось номери затримки на диск, включаючи прочитані байти. Як бачите, продуктивність tempdb викликає сумніви. Таблиця з інвентаризацією знаходиться в PDICompany_252_01.mdf, PDICompany_252_01_Second.ndf, або PDICompany_252_01_Third.ndf.

ReadLatencyWriteLatencyLatencyAvgBPerRead AvgBPerWriteAvgBPerTransferDriveDB                     physical_name
         42        1112    623       62171       67654          65147R:   tempdb                 R:\Microsoft SQL Server\Tempdb\tempdev1.mdf
         38        1101    615       62122       67626          65109S:   tempdb                 S:\Microsoft SQL Server\Tempdb\tempdev2.ndf
         38        1101    615       62136       67639          65123T:   tempdb                 T:\Microsoft SQL Server\Tempdb\tempdev3.ndf
         38        1101    615       62140       67629          65119U:   tempdb                 U:\Microsoft SQL Server\Tempdb\tempdev4.ndf
         25         341     71       92767       53288          87009X:   PDICompany             X:\Program Files\PDI\Enterprise\Databases\PDICompany_Third.ndf
         26         339     71       90902       52507          85345X:   PDICompany             X:\Program Files\PDI\Enterprise\Databases\PDICompany_Second.ndf
         10         231     90       98544       60191          84618W:   PDICompany_FRx         W:\Program Files\PDI\Enterprise\Databases\PDICompany_FRx.mdf
         61         137     68        9120        9181           9125W:   model                  W:\Microsoft SQL Server\MSSQL.3\MSSQL\Data\modeldev.mdf
         36         113     97        9376        5663           6419V:   model                  V:\Microsoft SQL Server\Logs\modellog.ldf
         22          99     34       92233       52112          86304W:   PDICompany             W:\Program Files\PDI\Enterprise\Databases\PDICompany.mdf
          9          20     10       25188        9120          23538W:   master                 W:\Microsoft SQL Server\MSSQL.3\MSSQL\Data\master.mdf
         20          18     19       53419       10759          40850W:   msdb                   W:\Microsoft SQL Server\MSSQL.3\MSSQL\Data\MSDBData.mdf
         23          18     19      947956       58304         110123V:   PDICompany_FRx         V:\Program Files\PDI\Enterprise\Databases\PDICompany_FRx_1.ldf
         20          17     17      828123       55295         104730V:   PDICompany             V:\Program Files\PDI\Enterprise\Databases\PDICompany.ldf
          5          13     13       12308        4868           5129V:   master                 V:\Microsoft SQL Server\Logs\mastlog.ldf
         11          13     13       22233        7598           8513V:   PDIMaster              V:\Program Files\PDI\Enterprise\Databases\PDIMaster.ldf
         14          11     13       13846        9540          12598W:   PDIMaster              W:\Program Files\PDI\Enterprise\Databases\PDIMaster.mdf
         13          11     11       22350        1107           1110V:   msdb                   V:\Microsoft SQL Server\Logs\MSDBLog.ldf
         17           9      9      745437       11821          23249V:   PDIFoundation          V:\Program Files\PDI\Enterprise\Databases\PDIFoundation.ldf
         34           8     31       29490       33725          30031W:   PDIFoundation          W:\Program Files\PDI\Enterprise\Databases\PDIFoundation.mdf
          5           8      8       61560       61236          61237V:   tempdb                 V:\Microsoft SQL Server\Logs\templog.ldf
         13           6     11        8370       35087          16785W:   SAHost_Company01       W:\Program Files\PDI\Enterprise\Databases\SAHostCompany.mdf
          2           6      5       56235       33667          38911W:   SAHost_Company01       W:\Program Files\PDI\Enterprise\Databases\SAHost_Company_01_log.LDF

Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Пол Білий 9

Відповіді:


4

Схоже, ваші кластеризовані розбиття індексної сторінки будуть болісними, оскільки кластерний індекс містить фактичні дані, і для цього потрібно буде виділити нові сторінки та перемістити дані до них. Це, ймовірно, може призвести до блокування сторінки та, таким чином, до блокування.

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

Якщо ви розглядали можливість створення стовпчика ідентичності основного ключа вашим кластерним індексом, це не тільки зменшить розмір інших ваших індексів, але також означає, що ви зменшите кількість розбитків сторінки у вашому кластерному індексі. Варто спробувати, якщо вам вдасться відновити свої показники.


1

При багатопотоковому підході я з обережністю ставляться до вставки в таблицю, з якої потрібно спочатку перевірити наявність ключа. Такий вид каже мені, що в цьому індексі PK до цієї таблиці виникає проблема одночасності незалежно від того, скільки ниток є. З цієї ж причини мені не подобається натяк NOLOCK на таблицю інвентаризації, тому що, здається, помилка виникла, якщо різні потоки зможуть записати один і той же ключ (чи схема розділення видаляє таку можливість?). Мені цікаво, наскільки велика швидкість була при первинному введенні декількох ниток, оскільки, мабуть, вона спрацювала добре в якийсь момент.

Щось спробувати - це зробити запит більш схожим на групову операцію та перетворити "там, де немає", у "антиз'єднання". (зрештою, оптимізатор може вирішити ігнорувати ці зусилля). Як було сказано вище, я б видалив підказку NOLOCK з таблиці призначення, якщо, можливо, розділення не гарантувало ключових зіткнень між потоками.

 INSERT INTO i (...)
 SELECT DISTINCT ...             
   FROM tempdb..Update_Item_Work t (NOLOCK) -- nolock okay on read table
   left join Inventory i -- use without NOLOCK because PK is written inter-thread
     on i.Inv_Site_Key = t.UpdItemWrk_Site_Key
    and i.Inv_Item_Key = t.UpdItemWrk_Item_Key
    and i.Inv_Date = t.UpdItemWrk_Date
  where i.Inv_Site_Key is null   -- where not exist in inventory
    and UpdItemWrk_GUID = @GUID  -- for this thread

Визначивши час, який працює як базовий, ви можете повторно запустити з підказкою злиття ("ліве з'єднання" -> "ліве об'єднання об'єднання") як іншу можливість. Ви, ймовірно, повинні мати індекс у таблиці temp (UpdItemWrk_Site_Key, UpdItemWrk_Item_Key, UpdItemWrk_Date) для підказки про злиття.

Я не знаю, чи змогли б новіші неекспрес-версії SQL Server 2008/2012 автоматично паралельно збільшити більші об'єднання цієї форми, що дозволяє видалити розділ на основі GUID.

Для того, щоб заохочувати приєднання відбуватися лише на окремих елементах, а не на всіх елементах, пункти "виділити різний ... від ..." можна перетворити в "вибирати * з (вибирати окремо ... від ...)" раніше продовжуючи приєднання. Це може помітно змінитись лише тоді, коли окремий фільтрує багато рядків. Знову оптимізатор може ігнорувати це зусилля.

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