索引上的高 IO,有沒有更高性能的解決方案?
我有這個查詢,過去需要幾分鐘才能執行,現在需要 6 秒,但它每天執行數千次,所以我想讓它更快。
https://www.brentozar.com/pastetheplan/?id=SJMLjJOWm
似乎此查詢期間超過 99% 的 I/O 發生在一次聚集索引掃描上。
這是正常的嗎?這個查詢足以證明為它添加任何額外的索引是合理的,所以我想知道我是否在這裡遺漏了一些明顯的東西。
dbo.GROUP_CONCAT 函式來自這個 github 組裝項目 https://github.com/orlando-colamatteo/ms-sql-server-group-concat-sqlclr
SpecsProd 表定義:
CREATE TABLE [dbo].[SpecsProd]( [ID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL, [specsID_1] [int] NULL, [specsID_2] [int] NULL, [specID] [int] NOT NULL, [productID] [int] NOT NULL, [SpecValue_1] [varchar](1000) NULL, [SpecValue_2] [varchar](1000) NULL, [Flock] [bit] NULL, [SpecValue_1a] [varchar](2000) NULL, [SpecValue_2a] [varchar](2000) NULL, CONSTRAINT [PK_SpecsProd] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[SpecsProd] ADD CONSTRAINT [DF_SpecsProd_Flock] DEFAULT ((0)) FOR [Flock] GO ALTER TABLE [dbo].[SpecsProd] WITH NOCHECK ADD CONSTRAINT [FK_SpecsProd_Products] FOREIGN KEY([productID]) REFERENCES [dbo].[Products] ([Id_product]) NOT FOR REPLICATION GO ALTER TABLE [dbo].[SpecsProd] CHECK CONSTRAINT [FK_SpecsProd_Products] GO
使用 99% I/O 的聚集索引是
$$ PK_SpecsProd $$(第一個)。其他指標也在那裡。
ALTER TABLE [dbo].[SpecsProd] ADD CONSTRAINT [PK_SpecsProd] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY] GO CREATE NONCLUSTERED INDEX [IX_ProdID_SpecID] ON [dbo].[SpecsProd] ( [productID] ASC, [specID] ASC ) INCLUDE ( [ID], [SpecValue_1], [SpecValue_2]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY] GO CREATE NONCLUSTERED INDEX [IX_SpecID_ProductID] ON [dbo].[SpecsProd] ( [specID] ASC, [productID] ASC ) INCLUDE ( [SpecValue_1], [SpecValue_2]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY] GO CREATE NONCLUSTERED INDEX [product] ON [dbo].[SpecsProd] ( [productID] ASC ) INCLUDE ( [specID], [SpecValue_1], [SpecValue_2]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY] GO CREATE NONCLUSTERED INDEX [specId_inc_prodID_specvalue1] ON [dbo].[SpecsProd] ( [specID] ASC ) INCLUDE ( [productID], [SpecValue_1]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY] GO CREATE NONCLUSTERED INDEX [specsID] ON [dbo].[SpecsProd] ( [specsID_1] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY] GO
列最多的表(規格表)的吉尼斯記錄競爭者之一
CREATE TABLE [dbo].[Specs]( [ID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL, [Id_spec] [int] NOT NULL, [CatId] [int] NOT NULL, [sect] [varchar](50) NULL, [spec] [varchar](75) NOT NULL, [format] [varchar](50) NULL, [unit] [varchar](20) NULL, [definition] [ntext] NULL, [ordre] [int] NOT NULL, [Id_langue] [int] NOT NULL, [FormField] [varchar](50) NULL, [List] [varchar](max) NULL, [filterField] [bit] NOT NULL, [isFilter] [bit] NOT NULL, [isCollectionFilter] [bit] NOT NULL, [quickViewSubcats] [varchar](250) NULL, [width] [bit] NOT NULL, [height] [bit] NOT NULL, [depth] [bit] NOT NULL, [weight] [bit] NOT NULL, [DateCreation] [datetime] NOT NULL, [DateModification] [datetime] NOT NULL, [quickView] [bit] NOT NULL, [Visible] [bit] NULL, [compare] [bit] NOT NULL, [priceTag] [bit] NOT NULL, [ConvertionRate] [varchar](20) NULL, [ConvertionUnit] [varchar](20) NULL, [searchableLabel] [bit] NULL, [searchableValue] [bit] NULL, CONSTRAINT [PK_Specs] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY] ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_filter] DEFAULT ((0)) FOR [filterField] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_isFilter] DEFAULT ((0)) FOR [isFilter] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_isCollectionFilter] DEFAULT ((0)) FOR [isCollectionFilter] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_width] DEFAULT ((0)) FOR [width] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_height] DEFAULT ((0)) FOR [height] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_depth] DEFAULT ((0)) FOR [depth] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_weight] DEFAULT ((0)) FOR [weight] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_DateCreation] DEFAULT (getdate()) FOR [DateCreation] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_DateModification] DEFAULT (getdate()) FOR [DateModification] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_quickView] DEFAULT ((0)) FOR [quickView] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_compare] DEFAULT ((0)) FOR [compare] GO ALTER TABLE [dbo].[Specs] ADD CONSTRAINT [DF_Specs_priceTag] DEFAULT ((0)) FOR [priceTag] GO
- 等待統計表明
ASYNC_NETWORK_IO
是瓶頸。在您的實際計劃中,大約 87% 的已用時間用於將結果發送給客戶。您可能需要更改應用程式碼或減少發回的數據量。
<WaitStats> <Wait WaitType="CMEMTHREAD" WaitTimeMs="33" WaitCount="86" /> <Wait WaitType="SESSION_WAIT_STATS_CHILDREN" WaitTimeMs="1029" WaitCount="55" /> <Wait WaitType="LATCH_EX" WaitTimeMs="2796" WaitCount="308" /> <Wait WaitType="ASYNC_NETWORK_IO" WaitTimeMs="3083" WaitCount="144" /> </WaitStats> <QueryTimeStats ElapsedTime="3542" CpuTime="7247" />
MAXDOP 22``SELECT
對於查詢來說是相當高的。您是否使用不同的
MAXDOP
值測試了此查詢以驗證 22 確實是最佳選擇?如果我不得不猜測我會說你有一個兩個插槽伺服器,每個插槽有 12 個核心。出於某種原因,可能MAXDOP
在實例級別設置為 22。我基於計劃中的執行緒資訊:<ThreadStat Branches="4" UsedThreads="88"> <ThreadReservation NodeId="0" ReservedThreads="24" /> <ThreadReservation NodeId="1" ReservedThreads="24" /> <ThreadReservation NodeId="2" ReservedThreads="16" /> <ThreadReservation NodeId="3" ReservedThreads="24" /> </ThreadStat>
NUMA 節點 2 的調度程序比其他節點少,伺服器有 2 個插槽,每個插槽有 12 個調度程序,伺服器有 4 個插槽,每個插槽有 6 個調度程序,或者設置了手動軟 NUMA。MAXDOP 的一個常見建議是使用少於每個硬 NUMA 節點的物理核心數的東西。
綜上所述,您無需了解所有技術細節即可進行測試。嘗試使用不同的 MAXDOP 進行測試(確保多次執行測試),看看是否有幫助。
- 如果您願意,您可以減少計劃中所有三個表的 IO。
請注意,我只是查看了查詢計劃,並沒有查看您已經擁有的索引定義。
SpecsProd
看起來是你最大的桌子。您可以僅在所需的列上定義覆蓋索引。您無法對其進行搜尋,但您將執行更少的 IO,因為僅包含查詢中使用的三列的非聚集索引將小於聚集索引。該
Products
表的 IO 成本次之。您已經按聚集索引排序,這很好,但聚集索引的大小是表中所有數據的大小。您可以僅在聚集鍵列上創建非聚集索引以減少 IO。您甚至可以創建它DESC
以使掃描符合併行性,但我不知道這是否會由於TOP
表達式而在實踐中產生影響。您已經有一個覆蓋索引 on
Specs
,但過濾器是在謂詞而不是搜尋謂詞中評估的。如果您更改索引或創建具有正確順序的鍵列的新索引,您應該能夠進行查找而不是掃描。
只看程式碼,我推薦以下兩件事
第一:創建一個臨時表,其中包含以下子查詢
WHERE sp.productID IN ( SELECT TOP 25000 id_product FROM products ORDER BY 1 DESC )
即我會做
create table #t (id_product int primary key); insert into #t (id_product) SELECT TOP 25000 id_product FROM products ORDER BY 1 DESC;
然後將原來的 WHERE 子句更改為
WHERE sp.productID IN ( SELECT id_product FROM #t )
第二:我會刪除 where 子句中的 lower() 函式,如果您的數據庫排序規則不區分大小寫 (CS),則不需要 lower() 函式。如果你的數據庫是 CS,那麼你可以重寫條件。
例如,
lower(sp.SpecValue_1) = ('y')
可以重寫為
sp.SpecValue_1 in ('y', 'Y')
您可以從這種方法開始,看看這是否會提高您的性能。不要忘記使用全掃描更新您的統計資訊。
如果它仍然不符合您的要求,請考慮其他方法,例如 @Joe Obbish 提到的。