在查看執行計劃或調優過程當中,執行計劃裏面有些現象總會讓人有些迷惑不解: html
1:爲何同一條SQL語句有時候會走索引查找,有時候SQL腳本又不走索引查找,反而走全表掃描? 數據庫
2:同一條SQL語句,查詢條件的取值不一樣,它的執行計劃會一致嗎? app
3: 同一條SQL語句,其執行計劃會變化,爲何 性能
4: 在查詢條件的某個或幾個字段上建立了索引,執行計劃就必定會走該索引嗎? 測試
5:同時存在幾個索引,SQL語句會走那個索引? 優化
............................................................ spa
有時候若是要跟別人解釋清楚這些問題,若是不經過一些案例或例子來解說,很難闡述清楚,一方面是表達能力問題。另一方面,再華麗的語言也難敵眼見爲實,畢竟人接受信息大部分經過眼睛,小部分經過耳朵。眼見爲實耳聽爲虛嗎! code
下面來看一個簡單的例子,爲何我在對應的查詢字段上建有索引,可是它不走索引反而走全表掃描。htm
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
已經在查詢字段OBJECT_ID上創建了索引,爲何SQL優化器不走索引,而要走全表掃描呢?爲了說明白,那麼咱們藉助於查詢提示(Hints)強制優化器走索引查找來講明上述狀況,對比走索引查找、全表掃描二者的代價開銷,從下圖,咱們能夠看到當前狀況下,走全表掃描的開銷要小於索引查找。由於當前狀況下,走索引須要額外的IO開銷,反而不如全表掃描。因此優化器選擇了走全表掃描而非索引查找。不少開發人員有種根深蒂固的執拗觀念「走索引查找必定要優於全表掃描」(我跟他們解釋的時候,不少人不相信,"慷慨激昂"的質疑我,以致於個人解釋都顯得蒼白無力),大多數狀況下,走索引查找要優於全表掃描,可是在特定的場景、特定數據狀況下,會出現全表掃描優於索引查找的狀況。尤爲是ORACLE裏面,不少作開發的同事一看到SQL執行計劃走全表掃描,立馬大呼小叫。其實徹底是先入爲主的觀念做怪。blog
SELECT * FROM TEST WHERE OBJECT_ID=1
SELECT * FROM TEST WITH(INDEX=PK_TEST) WHERE OBJECT_ID =1
二者開銷不一致,其實在IO開銷這一塊,能夠從下面看出邏輯讀取的差別。
DBCC FREEPROCCACHE;
DBCC DROPCLEANBUFFERS;
SET STATISTICS IO ON;
SELECT * FROM TEST WHERE OBJECT_ID=1
DBCC FREEPROCCACHE;
DBCC DROPCLEANBUFFERS;
SET STATISTICS IO ON;
SELECT * FROM TEST WITH(INDEX=PK_TEST) WHERE OBJECT_ID =1
那麼接下來,咱們將該表的數據從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
以下所示,當數據變化時,優化器認爲走索引查找要優於全表掃描,因此選擇了索引查找,說到底優化器是基於成本的優化器,在衆多的執行計劃中,它會選擇代價開銷最小的一個執行計劃。
此時,強制優化器走全表掃描,對比開銷結果,你會發現結果徹底跟上面結果相反。
我若是更新該表數據,使其分佈徹底傾斜,那麼你能夠看到對於同一個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
可見同一條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;
以下所示,這種狀況下走彙集索引查找與彙集索引掃描的開銷幾乎接近。
若果我將數據增加到10000條記錄後,狀況又不一樣。這是一個顯而易見的結果,僅僅爲了說明數據對執行計劃的影響。
下面咱們刪除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兩個字段
對比二者的執行計劃
這裏涉及索引覆蓋所,想深刻理解能夠參考宋沄劍這篇博客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'
某個開發人員在測試、優化過程當中,發現執行計劃走彙集索引掃描,因而想若是給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
咱們又要使用查詢提示強制其走索引查找,來對比其開銷代價
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
優化器發現走彙集索引的開銷小於走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
另外還附上我測試過程當中,查詢條件取值不一樣,執行計劃不一樣的案例(否則有些人也會以爲迷惑),仍是那句話,數據會影響執行計劃的選擇。
後記:
生產環境的案例每每比我上面幾個簡單例子複雜得多,分析優化起來更加麻煩。咱們優化時要透過現象看本質,多思考,多對比才能撥開迷霧見真相!