Abbiamo un processo che prende i dati dai negozi e aggiorna una tabella di inventario a livello aziendale. Questa tabella contiene righe per ogni negozio per data e articolo. Per i clienti con molti negozi, questa tabella può diventare molto grande, nell'ordine di 500 milioni di righe.
Questo processo di aggiornamento dell'inventario viene in genere eseguito più volte al giorno quando i negozi immettono i dati. Queste corse aggiornano i dati solo da alcuni negozi. Tuttavia, i clienti possono anche eseguire questo per aggiornare, diciamo, tutti i negozi negli ultimi 30 giorni. In questo caso, il processo ruota su 10 thread e aggiorna l'inventario di ogni negozio in un thread separato.
Il cliente si lamenta che il processo sta impiegando molto tempo. Ho profilato il processo e ho scoperto che una query che INSERISCE in questa tabella sta consumando molto più tempo di quanto mi aspettassi. Questo INSERT a volte si completa in 30 secondi.
Quando eseguo un comando INSERT SQL ad hoc su questa tabella (limitato da BEGIN TRAN e ROLLBACK), l'SQL ad hoc viene completato nell'ordine dei millisecondi.
La query con prestazioni lente è di seguito. L'idea è di INSERIRE i record che non ci sono e successivamente di AGGIORNARLI mentre calcoliamo vari bit di dati. Un passaggio precedente nel processo ha identificato gli elementi che devono essere aggiornati, eseguito alcuni calcoli e inserito i risultati nella tabella tempdb Update_Item_Work. Questo processo è in esecuzione in 10 thread separati e ogni thread ha il proprio GUID in 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)
La tabella di inventario ha 42 colonne, la maggior parte delle quali tiene traccia delle quantità e conta per le varie rettifiche di inventario. sys.dm_db_index_physical_stats afferma che ogni riga è di circa 242 byte, quindi mi aspetto che circa 33 righe si adattino a una singola pagina da 8 KB.
La tabella è raggruppata in base al vincolo univoco (Inv_Site_Key, Inv_Item_Key, Inv_Date). Tutte le chiavi sono DECIMAL (15,0) e la data è SMALLDATETIME. Esiste una chiave primaria IDENTITY (non cluster) e altri 4 indici. Tutti gli indici e il vincolo cluster sono definiti con un esplicito (FILLFACTOR = 90, PAD_INDEX = ON).
Ho cercato nel file di registro per contare le divisioni di pagina. Ho misurato circa 1.027 divisioni sull'indice cluster e 1.724 divisioni su un altro indice, ma non ho registrato su quale intervallo si sono verificati. Un'ora e mezza dopo, ho misurato 7.035 divisioni di pagina sull'indice cluster.
Il piano di query che ho acquisito nel profiler è simile al seguente:
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
Osservando le query rispetto ai vari dmv, vedo che la query è in attesa su PAGEIOLATCH_EX per una durata di 0 su una pagina in questa tabella di inventario. Non vedo alcuna attesa o blocco sui blocchi.
Questa macchina ha circa 32 GB di memoria. È in esecuzione SQL Server 2005 Standard Edition, sebbene presto si aggiorneranno a 2008 R2 Enterprise Edition. Non ho numeri su quanto sia grande la tabella di inventario in termini di utilizzo del disco, ma posso ottenerlo, se necessario. È uno dei tavoli più grandi di questo sistema.
Ho eseguito una query su sys.dm_io_virtual_file_stats e ho visto che le attese di scrittura medie contro tempdb erano superiori di 1,1 secondi . Il database in cui è memorizzata questa tabella ha attese di scrittura medie di ~ 350 ms. Ma riavviano il loro server solo ogni 6 mesi circa, quindi non ho idea se queste informazioni siano rilevanti. tempdb si sviluppa su 4 file diversi. Hanno 3 file diversi per il database che contiene la tabella di Inventory.
Perché questa query richiederebbe così tanto tempo per INSERIRE alcune righe quando viene eseguita con molti thread diversi quando un singolo INSERT è molto veloce?
-- AGGIORNARE --
Ecco i numeri di latenza per unità inclusi i byte letti. Come puoi vedere, le prestazioni di tempdb sono discutibili. La tabella di inventario è in PDICompany_252_01.mdf, PDICompany_252_01_Second.ndf o 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