Langsame Leistung Einfügen weniger Zeilen in einen riesigen Tisch

9

Wir haben einen Prozess, der Daten aus Filialen entnimmt und eine unternehmensweite Inventartabelle aktualisiert. Diese Tabelle enthält Zeilen für jedes Geschäft nach Datum und Artikel. Bei Kunden mit vielen Geschäften kann diese Tabelle sehr groß werden - in der Größenordnung von 500 Millionen Zeilen.

Dieser Inventaraktualisierungsprozess wird normalerweise mehrmals täglich ausgeführt, wenn die Geschäfte Daten eingeben. Diese Läufe aktualisieren Daten von nur wenigen Filialen. Die Kunden können dies jedoch auch ausführen, um beispielsweise alle Geschäfte in den letzten 30 Tagen zu aktualisieren. In diesem Fall dreht der Prozess 10 Threads und aktualisiert das Inventar jedes Geschäfts in einem separaten Thread.

Der Kunde beschwert sich, dass der Prozess lange dauert. Ich habe den Prozess profiliert und festgestellt, dass eine Abfrage, die in diese Tabelle EINFÜGT, viel mehr Zeit in Anspruch nimmt als erwartet. Dieses INSERT wird manchmal in 30 Sekunden abgeschlossen.

Wenn ich einen Ad-hoc-SQL-INSERT-Befehl für diese Tabelle ausführe (begrenzt durch BEGIN TRAN und ROLLBACK), wird das Ad-hoc-SQL in der Größenordnung von Millisekunden abgeschlossen.

Die langsame Abfrage ist unten. Die Idee ist, Datensätze einzufügen, die nicht vorhanden sind, und sie später zu aktualisieren, während wir verschiedene Datenbits berechnen. In einem vorherigen Schritt des Prozesses wurden die Elemente identifiziert, die aktualisiert werden müssen, einige Berechnungen durchgeführt und die Ergebnisse in die Tempdb-Tabelle Update_Item_Work eingefügt. Dieser Prozess wird in 10 separaten Threads ausgeführt, und jeder Thread verfügt über eine eigene 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)

Die Inventartabelle enthält 42 Spalten, von denen die meisten Mengen und Zählungen für verschiedene Inventaranpassungen verfolgen. sys.dm_db_index_physical_stats sagt, dass jede Zeile ungefähr 242 Bytes groß ist, also erwarte ich, dass ungefähr 33 Zeilen auf eine einzelne 8-KB-Seite passen.

Die Tabelle ist nach der eindeutigen Einschränkung (Inv_Site_Key, Inv_Item_Key, Inv_Date) gruppiert. Alle Schlüssel sind DECIMAL (15,0) und das Datum ist SMALLDATETIME. Es gibt einen IDENTITY-Primärschlüssel (nicht gruppiert) und 4 weitere Indizes. Alle Indizes und die Cluster-Einschränkung werden explizit definiert (FILLFACTOR = 90, PAD_INDEX = ON).

Ich habe in der Protokolldatei nachgesehen, um Seitenaufteilungen zu zählen. Ich habe ungefähr 1.027 Teilungen im Clustered-Index und 1.724 Teilungen im anderen Index gemessen, aber ich habe nicht aufgezeichnet, über welches Intervall diese aufgetreten sind. Eineinhalb Stunden später habe ich 7.035 Seitenaufteilungen im Clustered-Index gemessen.

Der Abfrageplan, den ich im Profiler erfasst habe, sieht folgendermaßen aus:

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                                                                                       

Wenn ich Abfragen im Vergleich zu verschiedenen dmvs betrachte, sehe ich, dass die Abfrage auf PAGEIOLATCH_EX für eine Dauer von 0 auf einer Seite in dieser Inventartabelle wartet. Ich sehe keine Wartezeiten oder Blockierungen an Schlössern.

Dieser Computer verfügt über ca. 32 GB Arbeitsspeicher. Es wird SQL Server 2005 Standard Edition ausgeführt, obwohl bald ein Upgrade auf 2008 R2 Enterprise Edition durchgeführt wird. Ich habe keine Zahlen darüber, wie groß die Inventartabelle in Bezug auf die Festplattennutzung ist, aber ich kann diese bei Bedarf erhalten. Es ist eine der größten Tabellen in diesem System.

Ich habe eine Abfrage für sys.dm_io_virtual_file_stats ausgeführt und festgestellt, dass die durchschnittliche Schreibwartezeit für tempdb über 1,1 Sekunden liegt . Die Datenbank, in der diese Tabelle gespeichert ist, hat durchschnittliche Schreibwartezeiten von ~ 350 ms. Sie starten ihren Server jedoch nur etwa alle 6 Monate neu, sodass ich keine Ahnung habe, ob diese Informationen relevant sind. tempdb ist auf 4 verschiedene Dateien verteilt. Sie haben 3 verschiedene Dateien für die Datenbank, in der sich die Inventartabelle befindet.

Warum sollte diese Abfrage so lange dauern, um einige Zeilen einzufügen, wenn sie mit vielen verschiedenen Threads ausgeführt wird, wenn ein einzelnes INSERT sehr schnell ist?

- UPDATE -

Hier sind die Latenzzahlen pro Laufwerk einschließlich der gelesenen Bytes. Wie Sie sehen können, ist die Tempdb-Leistung fraglich. Die Inventartabelle befindet sich entweder in PDICompany_252_01.mdf, PDICompany_252_01_Second.ndf oder 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
Paul Williams
quelle
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Paul White 9

Antworten:

4

Es sieht so aus, als ob die Aufteilung Ihrer Clustered-Index-Seiten schmerzhaft sein wird, da der Clustered-Index die tatsächlichen Daten enthält und dafür neue Seiten zugewiesen und die Daten in diese verschoben werden müssen. Dies führt wahrscheinlich zu einer Sperrung der Seite und damit zu einer Blockierung.

Denken Sie auch daran, dass Ihr Clustered-Indexschlüssel 21 Byte groß ist und in allen Sekundärindizes als Lesezeichen gespeichert werden muss.

Haben Sie darüber nachgedacht, Ihre Primärschlüsselidentitätsspalte zu Ihrem Clustered-Index zu machen? Dadurch wird nicht nur die Größe Ihrer anderen Indizes verringert, sondern auch die Anzahl der Seitenteilungen in Ihrem Clustered-Index verringert. Es ist einen Versuch wert, wenn Sie Ihre Indizes wieder aufbauen können.

Steve
quelle
1

Beim Multithread-Ansatz bin ich vorsichtig beim Einfügen in eine Tabelle, aus der Sie zuerst die vorherige Existenz eines Schlüssels überprüfen müssen. Diese Art sagt mir, dass es ein Parallelitätsproblem für diesen PK-Index zu dieser Tabelle gibt, egal wie viele Threads es gibt. Aus dem gleichen Grund gefällt mir der NOLOCK-Hinweis in der Inventartabelle nicht, da anscheinend ein Fehler auftritt, wenn verschiedene Threads denselben Schlüssel schreiben können (entfernt das Partitionierungsschema diese Möglichkeit?). Ich bin gespannt, wie groß die Beschleunigung bei der ersten Einführung mehrerer Threads war, da sie irgendwann gut funktioniert haben muss.

Versuchen Sie, die Abfrage eher wie eine Massenoperation zu lesen und das "Wo nicht vorhanden" in ein "Anti-Join" umzuwandeln. (Letztendlich kann der Optimierer diese Anstrengung ignorieren). Wie oben erwähnt, würde ich den NOLOCK-Hinweis in der Zieltabelle entfernen, es sei denn, die Partitionierung hat möglicherweise keine Schlüsselkollisionen zwischen Threads garantiert.

 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

Wenn Sie das Timing als Basis ausführen, können Sie den Merge-Hinweis ("linker Join" -> "linker Merge-Join") als weitere Möglichkeit erneut ausführen. Sie sollten wahrscheinlich einen Index für die temporäre Tabelle (UpdItemWrk_Site_Key, UpdItemWrk_Item_Key, UpdItemWrk_Date) für den Zusammenführungshinweis haben.

Ich weiß nicht, ob die neueren Non-Express-Versionen von SQL Server 2008/2012 größere Zusammenführungen dieses Formulars automatisch parallelisieren können, sodass Sie die GUID-basierte Partitionierung entfernen können.

Um zu fördern, dass die Verknüpfung nur für die einzelnen Elemente und nicht für alle Elemente erfolgt, können die Klauseln "select different ... from ..." zuvor in "select * from (select different ... from ...)" konvertiert werden Fortsetzung des Joins. Dies macht möglicherweise nur dann einen spürbaren Unterschied, wenn der Unterschied viele Zeilen filtert. Auch hier kann der Optimierer diesen Aufwand ignorieren.

crokusek
quelle