数行を巨大なテーブルに挿入するとパフォーマンスが低下する


9

店舗からデータを取得し、会社全体の在庫表を更新するプロセスがあります。このテーブルには、日付別およびアイテム別のすべてのストアの行があります。多くの店舗を持つ顧客では、このテーブルは非常に大きくなる可能性があり、5億行程度になります。

この在庫更新プロセスは、通常、ストアがデータを入力するときに1日に何度も実行されます。これらの実行は、ほんの数店舗のデータを更新します。ただし、これを実行して、たとえば過去30日間のすべての店舗を更新することもできます。この場合、プロセスは10のスレッドを起動し、各ストアの在庫を別のスレッドで更新します。

お客様から、プロセスに時間がかかっているとの不満が寄せられています。プロセスのプロファイルを作成したところ、このテーブルにINSERTを実行する1つのクエリが予想以上に多くの時間を消費していることがわかりました。このINSERTは、30秒で完了する場合があります。

このテーブルに対してBEGIN TRANとROLLBACKで区切られたアドホックSQL INSERTコマンドを実行すると、アドホックSQLはミリ秒のオーダーで完了します。

パフォーマンスの遅いクエリは次のとおりです。アイデアは、そこにないレコードを挿入し、後でデータのさまざまなビットを計算するときにそれらを更新することです。プロセスの前のステップでは、更新する必要のあるアイテムを特定し、いくつかの計算を行い、結果をtempdbテーブルUpdate_Item_Workに詰め込みました。このプロセスは10個の個別のスレッドで実行されており、各スレッドはUpdate_Item_Workに独自のGUIDを持っています。

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)

Inventoryテーブルには42列があり、そのほとんどがさまざまな在庫調整の数量とカウントを追跡します。sys.dm_db_index_physical_statsは、各行が約242バイトであるため、約33行が単一の8 KBページに収まると予想しています。

テーブルは一意制約(Inv_Site_Key、Inv_Item_Key、Inv_Date)でクラスター化されます。すべてのキーはDECIMAL(15,0)であり、日付はSMALLDATETIMEです。IDENTITY主キー(非クラスター化)と4つの他のインデックスがあります。すべてのインデックスとクラスター化制約は明示的に定義されています(FILLFACTOR = 90、PAD_INDEX = ON)。

ログファイルを調べて、ページ分割をカウントしました。クラスタ化インデックスで約1,027分割、別のインデックスで1,724分割を測定しましたが、それらが発生した間隔を記録していません。1時間半後、クラスター化インデックスで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 GBです。SQL Server 2005 Standard Editionを実行していますが、間もなく2008 R2 Enterprise Editionにアップグレードされます。ディスク使用量に関してインベントリテーブルがどれほど大きいかはわかりませんが、必要に応じて取得できます。これは、このシステムで最大のテーブルの1つです。

私はsys.dm_io_virtual_file_statsに対してクエリを実行し、tempdbに対する平均書き込み待機が1.1 以上であることを確認しました。このテーブルが格納されているデータベースの平均書き込み待機時間は約350ミリ秒です。ただし、サーバーは6か月ごとに再起動するだけなので、この情報が適切かどうかはわかりません。tempdbは4つの異なるファイルに分散されています。それらには、Inventoryテーブルを保持するデータベース用に3つの異なるファイルがあります。

単一のINSERTが非常に高速であるときに、多くの異なるスレッドで実行すると、このクエリが数行をINSERTするのにとても時間がかかるのはなぜですか?

-更新-

以下は、読み取られたバイト数を含むドライブごとのレイテンシ数です。ご覧のとおり、tempdbのパフォーマンスには疑問があります。Inventoryテーブルは、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バイトであり、これをブックマークとしてすべてのセカンダリインデックスに保存する必要があることにも注意してください。

主キーID列をクラスター化インデックスにすることを検討したことがあります。これにより、他のインデックスのサイズが削減されるだけでなく、クラスター化インデックスのページ分割数が削減されることになります。インデックスの再構築に時間がかかる場合は、試してみる価値があります。


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

ベースとして実行されるタイミングは、別の可能性として、マージヒント(「左結合」->「左結合」)で再実行できます。おそらく、マージヒントの一時テーブル(UpdItemWrk_Site_Key、UpdItemWrk_Item_Key、UpdItemWrk_Date)にインデックスが必要です。

SQL Server 2008/2012の新しい非Expressバージョンがこのフォームのより大きなマージを自動的に並列化して、GUIDベースのパーティションを削除できるかどうかはわかりません。

すべてのアイテムではなく、個別のアイテムでのみ結合が行われるようにするには、前に "select different ... from ..."句を "select * from(select different ... from ...)"に変換できます。結合を続行します。これにより、Distinctが多くの行をフィルタリングする場合にのみ、顕著な違いが生じる可能性があります。この場合も、オプティマイザはこの作業を無視することがあります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.