效能調優6:Spool 假離線調優 SQLSERVER中的假離線
SQL Server的Spool(假離線)操作符,用於把前一個操作符處理的資料(又稱作中間結果集)儲存到一個隱藏的臨時結構中,以便在執行過程中重用這些資料。這個臨時結構都建立在tempdb中,通常的結構是工作表(worktable)和工作檔案(workfile)。假離線運算子會取出表或索引結構中的一部分的資料集,將他們存放在tempdb資料庫的臨時結構裡,我推測:如果這個臨時結構用於儲存表資料,稱作worktable;如果這個臨時結構儲存的是Hash表,稱作workfile。
SQL Server使用Spool操作符的優點是:
- 資料或中間結果集會被多次重用
- 使假離線資料與源資料保持隔離
一,Spool操作的分類
在執行計劃中,Eager Spool和Lazy Spool是邏輯操作符,這兩個邏輯操作符之間的區別是:
- Eager Spool:一次性把所有資料儲存到臨時結構中,它是一個阻塞性的操作符,這意味著它需要讀取輸入中的所有資料,然後處理完所有的資料行之後,才向下一個操作符返回結果,也就是說,除非Eager Spool把所有的資料行都處理完成,否則無法訪問到這些資料。
- Lazy Spool:逐行把資料儲存到臨時結構中,它是一個非阻塞性的操作符,這意味著它可以邊讀取資料,邊向下一個操作符輸出資料,也就是說,在Lazy Spool讀取完所有的資料之前,可以訪問這些資料。
Spool相關的物理操作符有Spool, Table Spool, Index Spool, Window Spool 和 Row Count Spool,這些物理操作符的作用是:
- Spool運算子用於把查詢的中間結果集儲存到tempdb資料庫中
- Row Count Spool運算子掃描輸入,計算現有的行數n,返回行數n,用於描述輸入的總行數。
- Index Spool 是把非聚集索引的資料存放到tempdb中的臨時結構中,該運算子掃描輸入的索引結構,把每行的副本放置在隱藏的Spool檔案中(儲存在tempdb資料庫中的worktable,且只在查詢的生命週期記憶體在),併為這些行建立非聚集索引,這樣可以使用索引的seek功能來僅輸出那些滿足SEEK()謂詞的行。
- Table Spool 運算子是把表資料存放到tempdb中的臨時結構中,該操作符掃描輸入的資料表,把每行的副本放置在隱藏的Spool表中,此表叫做worktable,儲存在tempdb資料庫中,且只在查詢的生命週期記憶體在
- Window Spool 操作符和OVER() 視窗函式息息相關,因為只有OVER()函式才會使用到Window Spool 操作符。
二,Lazy Spool調優
在查詢計劃中出現Spool操作符,意味著查詢語句需要儲存臨時資料集,以便在執行過程中重用這些資料。在查詢語句執行的生命週期內,SQL Server為了儲存資料,會在tempdb中建立臨時表,然後把臨時資料集儲存到臨時表中,這個操作會給硬碟帶來額外的IO開銷。tempdb的使用最終會使查詢語句的開銷增加,並且常常導致查詢效能不佳。
Lazy Spool之所以被成為懶假離線,這是因為它僅在收到請求時才會把資料載入到臨時結構中,並且在載入資料時不會停止資料流。雖然Lazy Spool是一個非阻塞的操作符,但是當有大量的資料需要處理時,它的開銷會非常大。
當查詢計劃中出現多個Lazy Spool操作符時,這種情況可能會導致非常嚴重的效能問題,例如:
SET STATISTICS IO ON GO SELECT [InvoiceID], [OrderID] FROM [Sales].[Invoices] o WHERE [TotalDryItems] = (SELECT AVG([TotalDryItems]) FROM [Sales].[Invoices] o1 WHERE o.[CustomerID] = o1.[CustomerID] GROUP BY [CustomerID]) GO
當你執行上述查詢之後,從SSMS中你可以看到如下的執行計劃圖形:
如果你仔細檢視這個執行計劃,你會發現三個不同的Lazy Spool操作符,每一個操作符的開銷都是0%,但是,當你切換到Message Tab,你會看到IO的統計資訊,Lazy Spool操作符把大量把大量的資料儲存到Worktable中,也就是說,消耗了14.4萬的邏輯讀操作,把資料寫入到tempdb中。
(17214 rows affected)
Table 'Invoices'. Scan count 9, logical reads 11994, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 24, logical reads 143680, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
要對這個查詢進行效能調優,必須保證執行計劃不會把大量的資料載入到tempdb中,通常修復Lazy Spool效能低下的做法是建立新的索引。
在上述的示例查詢中,有兩個where子句,一個where子句是關於CustomerID的,另一個是關於TotalDryItems,因此,我們在表[Sales].[Invoices]上建立一個關於這兩列的索引,首先檢視這兩列的選擇性(即唯一值的數量),把高選擇性的列作為索引的第一列:
SELECT COUNT(DISTINCT [TotalDryItems]) AS [CountTotalDryItems] ,COUNT (DISTINCT [CustomerID]) As [CountCustomerID] FROM [Sales].[Invoices] GO
列CustomerID唯一值的數量是663,列TotalDryItems唯一值的數量是6,由於CustomerID列的選擇性高,因此,把CustomerID作為索引的第一列:
CREATE NONCLUSTERED INDEX [IX_FirstTry] ON [Sales].[Invoices] ([CustomerID] ASC, [TotalDryItems] ASC) INCLUDE ([InvoiceID], [OrderID]) GO
重新執行示例查詢語句,得到一下的執行計劃圖示:
從執行計劃圖示中可以清楚地看出,對錶[Sales].[Invoices]執行Index Scan操作,由於索引IX_FirstTry比聚集索引掃面要窄的多,因此這是一個比Clustered Index Scan快很多的操作。最重要的是,執行計劃中沒有Lazy Spool操作符,不需要把資料寫入tempdb,然後再從tempdb中讀取,這大大提升了查詢語句的執行效能。
檢視Message Tab,檢查IO統計:
Table 'Invoices'. Scan count 2, logical reads 396, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
從統計資訊中,可以看出,沒有worktable,因此查詢不需要把資料寫入tempdb,再從tempdb中讀取資料。另外,由於使用了索引 IX_FirstTry,對Invoices執行的邏輯讀的次數,從11994減少到396,因此,查詢語句的IO效能成倍提升。
三,Eager Spool 應用場景
在更新語句中,如果執行計劃使用聚集索引來查詢資料行,那麼執行計劃不會使用Eager Spool操作符;如果執行計劃使用非聚集索引來查詢資料,那麼執行計劃就會使用Eager Spool操作符來與資料來源隔離。
例如,下面的語句使用非聚集索引(IX_Price)來查詢和讀取資料:
UPDATE Inventory SET Price = Price * 1.1 FROM Inventory WITH (INDEX = IX_Price) WHERE Price < 100.00
從上面的執行計劃中,可以看到,從非聚集索引IX_Price讀取資料之後,SQL Server使用Table Spool(Eager Spool)阻塞性操作符,它讀取Index Seek操作符輸入的所有資料之後,把資料寫入到tempdb資料庫中。這樣,Update語句不會從非聚集索引IX_Price讀取任何資料,取而代之,Update語句使用Eager Spool操作符來執行資料的讀取操作。
From above execution plans, we see that after reading the data from non-clustered in IX_Price SQL Server uses Table Spool(Eager Spool) blocking operator. It reads all data and then moves to next operator. In our example, Eager Spool will read all data from IX_Price then move to tempdb and hence later on UPDATE doesn’t read non-clustered index IX_Price anymore and instead all reads are performed using Eager Spool operator.
如果SQL Server不適用Eager Spool操作符,SQL Server 需要直接從非聚集索引IX_Price中讀取資料,定位到目標資料行,然後逐行更新資料。問題是在這種情景下,非聚集索引中行的位置可能被重置,導致資料被多次更新,使用Eager Spool可以避免這個問題。
參考文件:
Performance Tuning Made Easy – Optimizing Lazy Spool
SQL Server Eager Spool operator – Part2