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 字节,需要将其作为书签存储在所有二级索引中。
您是否考虑过将主键标识列设置为聚集索引,这不仅会减少其他索引的大小,还意味着您将减少聚集索引中的页面拆分数量。如果您能忍受重建索引,那么值得一试。