Rallentamento delle prestazioni Inserimento di poche righe in una tabella enorme


9

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

I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Paul White 9

Risposte:


4

Sembra che le suddivisioni della pagina dell'indice in cluster siano dolorose perché l'indice in cluster contiene i dati effettivi e ciò richiederà l'assegnazione di nuove pagine e il trasferimento dei dati in questi. Ciò potrebbe causare il blocco della pagina e quindi il blocco.

Ricorda anche che la tua chiave di indice cluster è di 21 byte e questo dovrà essere archiviato in tutti gli indici secondari come segnalibro.

Hai preso in considerazione la possibilità di rendere la colonna dell'identità della chiave primaria un indice cluster, non solo ciò ridurrà la dimensione degli altri indici, ma significherà anche che ridurrai il numero di divisioni di pagina nel tuo indice cluster. Vale la pena provare se riesci a ricostruire lo stomaco con gli indici.


1

Con l'approccio multi-thread, sono diffidente dell'inserzione in una tabella da cui devi prima controllare l'esistenza precedente di una chiave. Questo tipo di mi dice che c'è un problema di concorrenza su quell'indice PK a quella tabella, non importa quanti thread ci siano. Per lo stesso motivo, non mi piace il suggerimento NOLOCK nella tabella dell'inventario perché sembra che si verificherebbe un errore se thread diversi sono in grado di scrivere la stessa chiave (lo schema di partizionamento rimuove tale possibilità?). Sono curioso di sapere quanto sia stato grande lo speedup sull'introduzione iniziale di più thread perché a un certo punto deve aver funzionato bene.

Qualcosa da provare è rendere la query più simile a un'operazione di massa e convertire il "dove non esiste" in un "anti-join". (alla fine l'ottimizzatore può scegliere di ignorare questo sforzo). Come accennato in precedenza, rimuoverei il suggerimento NOLOCK sulla tabella di destinazione a meno che forse il partizionamento non abbia garantito alcuna collisione tra i thread.

 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

Tempi che vengono eseguiti come base, è possibile ripetere l'esecuzione con il suggerimento di unione ("join sinistro" -> "join di unione sinistro") come un'altra possibilità. Probabilmente dovresti avere un indice nella tabella temporanea (UpdItemWrk_Site_Key, UpdItemWrk_Item_Key, UpdItemWrk_Date) per il suggerimento di unione.

Non so se le nuove versioni non espresse di SQL Server 2008/2012 sarebbero in grado di parallelizzare automaticamente l'unione più ampia di questo modulo consentendo di rimuovere il partizionamento basato su GUID.

Per incoraggiare l'unione a verificarsi solo sugli elementi distinti anziché su tutti gli elementi, le clausole "seleziona distinto ... da ..." possono essere convertite in "seleziona * da (seleziona distinto ... da ...)" prima continuando con il join. Questo potrebbe fare una differenza evidente solo se il distinto sta filtrando molte righe. Anche in questo caso l'ottimizzatore può ignorare questo sforzo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.