淺析SQL SERVER執行計划中的各類怪相


在查看執行計划或調優過程中,執行計划里面有些現象總會讓人有些疑惑不解:

    1:為什么同一條SQL語句有時候會走索引查找,有時候SQL腳本又不走索引查找,反而走全表掃描?

    2:同一條SQL語句,查詢條件的取值不同,它的執行計划會一致嗎?

    3: 同一條SQL語句,其執行計划會變化,為什么

    4: 在查詢條件的某個或幾個字段上創建了索引,執行計划就一定會走該索引嗎?

    5:同時存在幾個索引,SQL語句會走那個索引?

     ............................................................

有時候如果要跟別人解釋清楚這些問題,如果不通過一些案例或例子來解說,很難闡述清楚,一方面是表達能力問題。另外一方面,再華麗的語言也難敵眼見為實,畢竟人接受信息大部分通過眼睛,小部分通過耳朵。眼見為實耳聽為虛嗎!

下面來看一個簡單的例子,為什么我在對應的查詢字段上建有索引,但是它不走索引反而走全表掃描。

DROP TABLE TEST 
   CREATE TABLE TEST (OBJECT_ID  INT, NAME VARCHAR(8));
 
   CREATE INDEX PK_TEST ON TEST(OBJECT_ID) 
   DECLARE @Index INT =0;
 
WHILE @Index < 20
BEGIN
    INSERT INTO TEST
    SELECT @Index, 'kerry';
    
    SET @Index = @Index +1;
END
 
 
UPDATE STATISTICS TEST WITH FULLSCAN
 
SELECT * FROM TEST WHERE OBJECT_ID=1

 

clip_image001

 

已經在查詢字段OBJECT_ID上建立了索引,為什么SQL優化器不走索引,而要走全表掃描呢?為了說明白,那么我們借助於查詢提示(Hints)強制優化器走索引查找來說明上述情況,對比走索引查找、全表掃描兩者的代價開銷,從下圖,我們可以看到當前情況下,走全表掃描的開銷要小於索引查找。因為當前情況下,走索引需要額外的IO開銷,反而不如全表掃描。所以優化器選擇了走全表掃描而非索引查找。很多開發人員有種根深蒂固的固執觀念“走索引查找一定要優於全表掃描”(我跟他們解釋的時候,很多人不相信,"慷慨激昂"的質疑我,以至於我的解釋都顯得蒼白無力),大多數情況下,走索引查找要優於全表掃描,但是在特定的場景、特定數據情況下,會出現全表掃描優於索引查找的情況。尤其是ORACLE里面,很多做開發的同事一看到SQL執行計划走全表掃描,立馬大呼小叫。其實完全是先入為主的觀念作怪。

SELECT * FROM TEST WHERE OBJECT_ID=1
 
 
SELECT * FROM TEST WITH(INDEX=PK_TEST) WHERE OBJECT_ID =1
 

 

clip_image002

 

兩者開銷不一致,其實在IO開銷這一塊,可以從下面看出邏輯讀取的差異。

DBCC FREEPROCCACHE;
DBCC DROPCLEANBUFFERS;
SET STATISTICS IO ON;
 
SELECT * FROM TEST WHERE OBJECT_ID=1

 

 

clip_image003

 

DBCC FREEPROCCACHE;
 
DBCC DROPCLEANBUFFERS;
 
SET STATISTICS IO ON;
 
SELECT * FROM TEST WITH(INDEX=PK_TEST) WHERE OBJECT_ID =1
 

 

 

clip_image004

 

 

那么接下來,我們將該表的數據從20條記錄增長到10000條記錄,你覺得執行計划會變化嗎?大家不妨先思考一下這個問題,再看下文。

TRUNCATE TABLE TEST;
DECLARE @Index INT =0;
 
WHILE @Index < 10000
BEGIN
    INSERT INTO TEST
    SELECT @Index, 'kerry';
    
    SET @Index = @Index +1;
END
 
 
UPDATE STATISTICS TEST WITH FULLSCAN
SELECT * FROM TEST WHERE OBJECT_ID=1

 

如下所示,當數據變化時,優化器認為走索引查找要優於全表掃描,所以選擇了索引查找,說到底優化器是基於成本的優化器,在眾多的執行計划中,它會選擇代價開銷最小的一個執行計划。

 

clip_image005

 

此時,強制優化器走全表掃描,對比開銷結果,你會發現結果完全跟上面結果相反。

 

clip_image006

 

我如果更新該表數據,使其分布完全傾斜,那么你可以看到對於同一個SQL,不同的取值,它的執行計划也會完全不同。

UPDATE TEST SET OBJECT_ID =1 WHERE OBJECT_ID<9999
UPDATE STATISTICS TEST WITH FULLSCAN
 
SELECT OBJECT_ID,COUNT(1) SUM_COUNT FROM TEST GROUP BY OBJECT_ID
OBJECT_ID    SUM_COUNT
----------- -----------
1             9999
9999           1
 
SELECT * FROM TEST WHERE OBJECT_ID=1
SELECT * FROM TEST WHERE OBJECT_ID=9999

 

clip_image007

 

可見同一條SQL語句,查詢條件的取值不同,它的執行計划可能會不一樣。

這幾個例子,其實我想說的是執行計划往往會受數據變化的、數據分布(直方圖)的影響,在統計信息正確的情況下,優化器會根據代價來判斷選取最優的執行計划。前提是統計信息准確。在調優過程中,有時候遇到統計信息不正確導致執行計划很差的情況。我沒有想到一個好的例子來讓大家形象觀察統計信息的不正確性導致執行計划的不同。在此不做詳細討論。

也許細心的朋友已經發現了我上面測試用例使用的是非聚集索引,也就是說該表是一個堆表。如果我創建的索引是聚集索引,情況會怎么樣?如下所示,聚集索引下的執行計划跟非聚集索引情況又不一樣。

DROP TABLE TEST;
CREATE TABLE TEST (OBJECT_ID  INT, NAME VARCHAR(8));
 
CREATE CLUSTERED INDEX PK_TEST ON TEST(OBJECT_ID) 
DECLARE @Index INT =0;
 
WHILE @Index < 20
BEGIN
    INSERT INTO TEST
    SELECT @Index, 'kerry';
    
    SET @Index = @Index +1;
END
UPDATE STATISTICS TEST WITH FULLSCAN;

 

clip_image008

 

如下所示,這種情況下走聚集索引查找與聚集索引掃描的開銷幾乎接近。

clip_image009

若果我將數據增長到10000條記錄后,情況又不同。這是一個顯而易見的結果,僅僅為了說明數據對執行計划的影響。

clip_image010

下面我們刪除TEST表, 新建另外一個TEST表, 如下所示

 

DROP TABLE TEST;
SELECT * INTO TEST FROM sys.objects
 
(2014 行受影響)
 
CREATE INDEX IDX_TEST_N1 ON TEST(CREATE_DATE, TYPE);
 
UPDATE STATISTICS TEST WITH FULLSCAN;
 
SELECT CREATE_DATE, TYPE FROM TEST 
WHERE CREATE_DATE >='2013-07-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'
  
SELECT * FROM TEST 
WHERE CREATE_DATE >='2013-07-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'

下面看看這兩個SQL的執行計划的差異,這兩個SQL略有差異,查詢字段不同,一個是查詢所有字段,一個是查詢CREATE_DATE, TYPE兩個字段

clip_image011

對比兩者的執行計划

clip_image012

這里涉及索引覆蓋所,想深入理解可以參考宋沄劍這篇博客T-SQL查詢高級--理解SQL SERVER中非聚集索引的覆蓋,連接,交叉和過濾.

在這個簡單例子中,我們可以用查詢必須字段代替*,用索引覆蓋避免其走RID查找,但是實際環境中往往比較復雜,有時候同一個表上的查詢SQL,可能非常多,索引覆蓋也往往不可能全部涉及。所以在寫SQL代碼中,我們要養成查詢必要字段的習慣,不要生成SELECT *的習慣,因為它有下面一些弊端:

1:如果你只需要表中幾個字段,SELECT * 會產生額外的IO,消耗額外的帶寬資源。當數據庫有大量這類SQL,就會產生量變到質變。慢慢影響整個數據庫的性能。

2:習慣成必然(很多時候大部分人都是從SELECT * FROM開始的),養成了這樣寫SQL的習慣。

3:造成額外的書簽查找或是由查找變為掃描

4: 產生潛在的BUG 例如 INSERT INTO T (COLUMN1,…… )SELECT * FROM M . 如果M表字段增加、或修改字段類型等都會導致錯誤。

上面僅僅是題外話,這里要說明的是你的SQL寫法也有可能影響執行計划。

下面來看一個例子,突然某天有這么樣一個需求(當然實際情況遠比這個復雜),

DROP TABLE TEST;
SELECT * INTO TEST FROM sys.objects
 
CREATE CLUSTERED INDEX PK_TEST ON TEST(OBJECT_ID)
 
UPDATE STATISTICS TEST WITH FULLSCAN
 
 
SELECT * FROM TEST 
WHERE CREATE_DATE >='2013-04-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'

 

clip_image013

某個開發人員在測試、優化過程中,發現執行計划走聚集索引掃描,於是想如果給CREATE_DATE和TYPE字段建立一個索引,那么它會不會快一點?結果他發現他添加了索引,可是優化器根本不走他建立的索引,為什么呢?

CREATE  INDEX IDX_TEST_N1 ON TEST(CREATE_DATE, TYPE)
UPDATE STATISTICS TEST WITH FULLSCAN
 
 
 
SET SHOWPLAN_ALL  ON
GO
SELECT * FROM TEST 
WHERE CREATE_DATE >='2013-04-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'
GO

clip_image014

 

我們又要使用查詢提示強制其走索引查找,來對比其開銷代價

SET SHOWPLAN_ALL  ON
GO
SELECT * FROM TEST 
WHERE CREATE_DATE >='2013-04-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'
GO
SET SHOWPLAN_ALL  OFF;
GO
 
 
SET SHOWPLAN_ALL  ON
GO
SELECT * FROM TEST WITH( INDEX=IDX_TEST_N1)
WHERE CREATE_DATE >='2013-04-09 00:00' 
  AND CREATE_DATE <='2014-04-30 00:00' 
  AND TYPE='S'
GO
SET SHOWPLAN_ALL  OFF;
GO

 

clip_image015

 

clip_image016

 

優化器發現走聚集索引的開銷小於走IDX_TEST_N1索引查找,所以即使你在查詢條件上建有索引,執行計划還是不會走這個索引。如果我創建索引時,覆蓋這些字段,那么它就會走索引查找而不會是聚集索引。

DROP INDEX IDX_TEST_N1 ON TEST
 
CREATE NONCLUSTERED INDEX IDX_TEST_N1
ON [dbo].[TEST] ([type],[create_date])
INCLUDE ([name],[object_id],[principal_id],[schema_id],[parent_object_id],[type_desc],[modify_date],[is_ms_shipped],[is_published],[is_schema_published])
GO

clip_image017

 

另外還附上我測試過程中,查詢條件取值不同,執行計划不同的案例(不然有些人也會覺得迷惑),還是那句話,數據會影響執行計划的選擇。

clip_image018

clip_image019

 

后記:

   生產環境的案例往往比我上面幾個簡單例子復雜得多,分析優化起來更加麻煩。我們優化時要透過現象看本質,多思考,多對比才能撥開迷霧見真相!


免責聲明!

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



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