SQL Server臨界點游戲——為什么非聚集索引被忽略!


當我們進行SQL Server問題處理的時候,有時候會發現一個很有意思的現象:SQL Server完全忽略現有定義好的非聚集索引,直接使用表掃描來獲取數據。我們來看看下面的表和索引定義:

 1 CREATE TABLE Customers
 2 (
 3    CustomerID INT NOT NULL,
 4    CustomerName CHAR(100) NOT NULL,
 5    CustomerAddress CHAR(100) NOT NULL,
 6    Comments CHAR(185) NOT NULL,
 7    Value INT NOT NULL
 8 )
 9 GO
10  
11 CREATE UNIQUE CLUSTERED INDEX idx_Customers ON Customers(CustomerID)
12 GO
13  
14 CREATE UNIQUE NONCLUSTERED INDEX idx_Test ON Customers(Value)
15 GO

我們往表里插入80000條記錄:

 1 DECLARE @i INT = 1
 2 WHILE (@i <= 80000)
 3 BEGIN
 4 INSERT INTO Customers VALUES
 5 (
 6    @i,
 7    'CustomerName' + CAST(@i AS CHAR),
 8    'CustomerAddress' + CAST(@i AS CHAR),
 9    'Comments' + CAST(@i AS CHAR),
10    @i
11 )
12 
13 SET @i += 1
14 END
15 GO

執行下列查詢,就會發現SQL Server完全忽略非聚集索引,而使用表掃描來獲取數據,點擊工具欄的顯示包含實際的執行計划:

1 SELECT * FROM Customers
2 WHERE Value < 1267
3 GO

 

而當我們把查詢條件修改為1266時,我們驚奇的發現,SQL Server又重新使用非聚集索引來獲取數據了:

1 SELECT * FROM Customers
2 WHERE Value < 1266
3 GO

很多人估計會很興奮,因為他們認為它們找到了SQL Server里的一個BUG,用指定索引來查詢就可以避免這個問題:

1 SELECT * FROM Customers
2 WITH (INDEX(idx_Test))
3 WHERE Value < 1267
4 GO

 

從執行計划里我們可以看到,SQL Server需要進行書簽查找,因為針對這個查詢,我們並沒有定義對應的覆蓋非聚集索引。當你進行全表聚集索引掃描時,SQL Server這里幫了你一個大忙:用書簽查找獲取每條記錄成本太高,因此SQL Server使用了全表掃描,這樣就只需要較少的IO和CPU占用,因為書簽查找都要通過內循環運算符完成。

在SQL Server里,這個行為被稱為臨界點(Tipping Point) 。我們再詳細解釋下這個概念。簡單來說,臨界點定義了SQL Server是使用書簽查找還是全表/索引掃描。這也意味着臨界點只與非覆蓋非聚集索引有關。一個對指定查詢扮演覆蓋非聚集索引的角色的話,不會有臨界點,也就不會有剛才介紹的問題。

在有書簽查找的查詢時,SQL Server使用書簽查找還是全表掃描取決於獲取的頁數。是的,你沒看錯!獲取的頁數決定了書簽查找是好的還是不好的!這與查詢返回的記錄條數完全無關,唯一有關就是頁數。臨界點出現在查詢需要讀取的24%-33%頁數之間。

在這范圍之前,查詢優化器會選擇書簽查找,在這范圍之后,查詢優化器會選擇全表掃描(在全表掃描運算符里會有謂語定義)。

這也意味着你記錄的大小決定了臨界點的位置。在查詢越過臨界點進行全表掃描時,小記錄,你就只能從表獲取小數量的記錄,大記錄,你就能夠獲得大量的記錄。下圖就是對臨界點的一個圖示。

在我們剛才的例子里,每條記錄是400 bytes長,因此8kb的頁面里可以保存20條記錄,當我們進行全表掃描時,SQL Server會產生4016個邏輯讀。

1 SET STATISTICS IO ON
2 SELECT * FROM Customers

 剛才的例子里,我們的表在聚集索引的葉子層有4000個數據頁,也就是說臨界點在1000與1333頁之間的某個地方。在優化器選擇進行全表掃描前,你只能讀取0.25%-0.67%(1000* 20/80000,1333*20/80000)的表數據。

下面這個查詢會用到書簽查找:

1 SET STATISTICS IO ON
2 SELECT * FROM Customers
3 WHERE Value < 1266
4 GO

可以看到,這個查詢需要3887個IO操作,而全表掃描只需要4016個IO,這里的書簽查找成本(IO和CPU消耗)越來越昂貴了。超過了這個點,SQL Server就決定不使用書簽查找,改用全表掃描了。

1 SET STATISTICS IO ON
2 SELECT * FROM Customers
3 WHERE Value < 1267
4 GO

我們一起執行看下:

1 SELECT * FROM Customers
2 WHERE Value < 1266
3 GO
4 SELECT * FROM Customers
5 WHERE Value < 1267
6 GO

2個近乎一樣的查詢,卻有完全不同的執行計划,這在性能調優的時候是個大問題,因為你的執行計划失去了穩定性。

針對輸入參數的不同,卻有完全不同的計划!這也是書簽查找的重大缺陷!用了書簽查找,你就不能獲得穩定的執行計划。如果這個執行計划被緩存(或你的統計信息過期了),你用它獲取大量數據的時候就會有性能上的問題,因為低效的書簽查找被SQL Server盲目重用了!這會造成原先只要幾秒的查詢,要花好幾分鍾才能完成!

我們說過,臨界點取決於查詢的讀取頁數。我們對剛才的表做下一點改動,每條記錄40 bytes長,8k的頁里能存儲200條的記錄,同樣我們也插入80000條記錄(記得關掉IO統計:SET STATISTICS IO OFF和執行計划顯示,否則電腦蝸牛了-_-)。

 1 CREATE TABLE Customers3
 2 (
 3    CustomerID INT NOT NULL,
 4    CustomerName CHAR(10) NOT NULL,
 5    CustomerAddress CHAR(10) NOT NULL,
 6    Comments CHAR(5) NOT NULL,
 7    Value INT NOT NULL
 8 )
 9 GO
10 
11 CREATE UNIQUE CLUSTERED INDEX idx_Customers ON Customers3(CustomerID)
12 GO
13  
14 CREATE UNIQUE NONCLUSTERED INDEX idx_Test ON Customers3(Value)
15 GO
16 
17 
18 DECLARE @i INT = 1
19 WHILE (@i <= 80000)
20 BEGIN
21 INSERT INTO Customers3 VALUES
22 (
23    @i,
24    'C2',
25    'C3',    
26    'C4',
27    @i
28 )
29 
30 SET @i += 1
31 END
32 GO

這樣的話,我們需要400頁來存儲這些數據。我們來看下臨界點位置:臨界點在100-133頁讀取的位置,也就是說通過非聚集索引,你只能讀取0.125%-0.167%的數據,對於80000條數據的表來說,這幾乎就是沒數據你的非聚集索引毫無用處!

我們來看下臨界點的2個不同查詢,這里我們可以打開執行計划顯示。

 1 SET STATISTICS IO ON
 2 -- 書簽查找會產生332個邏輯讀。
 3 SELECT * FROM Customers3
 4 WHERE Value < 157
 5 GO
 6 
 7 -- 聚集索引掃描會產生419個邏輯讀。
 8 -- The query produces 419 I/Os.
 9 SELECT * FROM Customers3
10 WHERE Value < 158
11 GO

我們來看第2個查詢,我們只選擇80000條記錄的157條,我們只選擇了很少的數據,但是SQL Server在這里就非常聰明,完全忽略你的的非聚集索引,使用表掃描來獲取數據。但對於整個查詢來說,這個非聚集索引設計並不完美,因為不是覆蓋的非聚集索引,如果有人用指定索引來查找數據,就會非常恐怖:

1 SELECT * FROM Customers3 WITH(INDEX(idx_Test))
2 WHERE Value < 80001
3 GO

這個查詢產生了165120個邏輯讀,把聚集索引全表掃描需要的IO數直接秒殺!從這個例子我們可以看出,臨界點是SQL Server里的性能保障,它阻止着使用書簽查找,造成占用昂貴資源的查詢發生。但這些和記錄數完全無關。這2個例子里的表記錄數都是80000。我們只修改了表記錄的大小,因此我們就改變了表的大小,最后臨界點也跟着改變,SQL Server就會忽略我們的非聚集索引。

寓意:非聚集索引,不是覆蓋非聚集索引的話,在SQL Server里是非常,非常,非常,非常有選擇性的用例!下次當你碰到這個情況的時候,想下你要怎么處理這個問題! 

參考文章:

https://www.sqlpassion.at/archive/2013/06/12/sql-server-tipping-games-why-non-clustered-indexes-are-just-ignored/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM