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

9

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

Этот процесс обновления инвентаря обычно запускается много раз в день, когда магазины вводят данные. Эти прогоны обновляют данные только из нескольких магазинов. Однако клиенты также могут запустить это, чтобы обновить, скажем, все магазины за последние 30 дней. В этом случае процесс раскручивает 10 потоков и обновляет инвентарь каждого магазина в отдельном потоке.

Заказчик жалуется, что процесс занимает много времени. Я профилировал процесс и обнаружил, что один запрос, который вставляет в эту таблицу, занимает намного больше времени, чем я ожидал. Эта вставка иногда завершается за 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). Все ключи ДЕСЯТИЧНЫЕ (15,0), а дата НЕБОЛЬШАЯ. Существует первичный ключ 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 Standard Edition, хотя вскоре будет обновлен до 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

Времена, которые запускаются как база, вы можете перезапустить с подсказкой слияния («left join» -> «left merge join») в качестве другой возможности. Вы, вероятно, должны иметь индекс для временной таблицы (UpdItemWrk_Site_Key, UpdItemWrk_Item_Key, UpdItemWrk_Date) для подсказки слияния.

Я не знаю, смогут ли более новые неэкспресс-версии SQL Server 2008/2012 автоматически распараллеливать большие объединения этой формы, позволяя вам удалить разбиение на основе GUID.

Чтобы объединение происходило только на отдельных элементах, а не на всех элементах, предложения «выбрать отличные ... из ...» могут быть преобразованы в «выбрать * из (выбрать отдельные ... из ...)» перед продолжая с присоединением. Это может иметь заметное значение, только если отличное фильтрует много строк. Опять оптимизатор может игнорировать это усилие.

crokusek
источник