将几行插入大表时性能缓慢

Pau*_*ams 9 performance sql-server query-performance

我们有一个从商店获取数据并更新公司范围内的库存表的流程。该表按日期和按项目为每个商店提供行。对于拥有许多商店的客户,该表可能会变得非常大——大约有 5 亿行。

当商店输入数据时,此库存更新过程通常每天运行多次。这些运行仅从少数商店更新数据。但是,客户也可以运行此程序来更新过去 30 天内的所有商店。在这种情况下,该过程启动 10 个线程并在单独的线程中更新每个商店的库存。

客户抱怨这个过程需要很长时间。我分析了这个过程,发现插入到这个表中的一个查询消耗的时间比我预期的要多得多。此 INSERT 有时会在 30 秒内完成。

当我对这个表(由 BEGIN TRAN 和 ROLLBACK 限定)运行 ad-hoc SQL INSERT 命令时,ad-hoc 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)
Run Code Online (Sandbox Code Playgroud)

库存表有 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 次拆分,但我没有记录这些发生的时间间隔。一个半小时后,我测量了聚集索引上的 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                                                                                       
Run Code Online (Sandbox Code Playgroud)

查看查询与各种 dmv 的对比,我看到查询在此 Inventory 表中的页面上等待 PAGEIOLATCH_EX 的持续时间为 0。 我没有看到任何等待或锁定锁定。

这台机器有大约 32 GB 的内存。它运行的是 SQL Server 2005 标准版,不过它们很​​快就会升级到 2008 R2 企业版。我没有关于库存表在磁盘使用方面有多大的数字,但如果有必要,我可以得到。它是该系统中最大的表之一。

我对 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
Run Code Online (Sandbox Code Playgroud)

小智 4

看起来聚集索引页拆分将会很痛苦,因为聚集索引保存实际数据,这将需要分配新页面并将数据移动到这些页面。这可能会导致页面锁定并从而阻塞。

另请记住,聚集索引键为 21 字节,需要将其作为书签存储在所有二级索引中。

您是否考虑过将主键标识列设置为聚集索引,这不仅会减少其他索引的大小,还意味着您将减少聚集索引中的页面拆分数量。如果您能忍受重建索引,那么值得一试。