當我們進行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里是非常,非常,非常,非常有選擇性的用例!下次當你碰到這個情況的時候,想下你要怎么處理這個問題!