我們知道資料庫中的統計信息的準確性是非常重要的。它會影響執行計劃。一直想寫一篇關於統計信息影響執行計劃的相關博客,但是都卡在如何構造一個合適的例子上,所以一直拖著沒有寫。巧合,最近在生產環境中遇到這麼一個案例,下麵對案例中的相關信息做了脫敏處理,有些中間步驟也省略了,只關註核心部分SQL。如下所示,... ...
我們知道資料庫中的統計信息的準確性是非常重要的。它會影響執行計劃。一直想寫一篇關於統計信息影響執行計劃的相關博客,但是都卡在如何構造一個合適的例子上,所以一直拖著沒有寫。巧合,最近在生產環境中遇到這麼一個案例,下麵對案例中的相關信息做了脫敏處理,有些中間步驟也省略了,只關註核心部分SQL。如下所示,同事反饋一個SQL語句執行很慢。
UPDATE b
SET b.[Status] = '已掃描,未簽收' ,
b.[Time] = pr.CreatedDate
FROM #Batch b
JOIN WDPM.PdaRecords pr WITH ( NOLOCK ) ON b.Batch_No = pr.OrderNo
AND pr.FunctionName = '[WDPM].[usp_SaveOutOrder]'
WHERE b.[Status] = '已打單,未掃描'
AND pr.CreatedDate > b.[Time];
如下截圖所示,這個SQL語句基本上耗時271秒。一個臨時表與一個表做嵌套迴圈連接(Nested Loops)。 因為表WDPM.PdaRecords只有一個聚集索引,所以執行計劃中,這個表走聚集索引掃描。
註意:這裡表WDPM.PdaRecords本身缺少合適的索引,只有一個聚集索引。後面展開講述這個問題.這裡先圍繞統計信息的準確性對執行計劃的影響來展開講述。
物理表WDPM.PdaRecords的數據量為2505369(當然這個是一直在變化的。這個數值僅僅是實驗前的檢測記錄,一直有會話對其進行DML操作,所以數據會變化,所以這裡沒有列出統計信息截圖)。
我們看到Table Scan部分,預估行數(Estimated Number of Rows)為1, 實際行數為150。 這個偏差已經比較大了。
對於物理表WDPM.PdaRecords而言,基數估計的預估行數(Estimated Number of Rows)為921771, 但是由於嵌套迴圈連接,所以累加起來的實際行數(Actual Number of Rows)為: 921771*150 =138265650 。
我們知道嵌套迴圈(Nested Loops)演算法的時間複雜度為N*M, N的預估值從1變成了150 ,這裡面的偏差就大了(因為每次聚集索引掃描的開銷也很大)。所以導致優化器在表的物理連接方式上選擇了嵌套迴圈(Nested Loops), 因為預估的代價是很小的。但是實際因為統計信息的誤差,導致這個代價放大了150倍。那麼如果我們更新臨時表的統計信息呢?然後執行這個SQL,會有什麼變化呢?
如下所示,我們在執行SQL語句前,更新一下臨時表的統計信息。發現優化器在獲取了準確的統計信息後,在表的物理連接上選擇了Hash Join方式。而且SQL語句耗時變成了1秒多。為什麼呢? 因為優化器發現選擇Nested Loops的代價遠遠高於 Hash Join。所以它在獲取了準確的信息後,作出了最優選擇。之前之所以生成了一個錯誤的執行計劃,就是因為它得到的“信息”不准確,導致它作出了錯誤的抉擇。這個就好比你獲取了錯誤的信息,作出了錯誤的選擇,購買了一隻錯誤的股票,而巴菲特由於掌握了準確的行業信息,作出了正確的選擇。 購買了幾隻購票都大漲了。
UPDATE STATISTICS #Batch WITH FULLSCAN;
UPDATE b
SET b.[Status] = '已掃描,未簽收' ,
b.[Time] = pr.CreatedDate
FROM #Batch b
JOIN WDPM.PdaRecords pr WITH ( NOLOCK ) ON b.Batch_No = pr.OrderNo
AND pr.FunctionName = '[WDPM].[usp_SaveOutOrder]'
WHERE b.[Status] = '已打單,未掃描'
AND pr.CreatedDate > b.[Time];
當然,瞭解到這裡,還遠遠沒有結束。我們發現表WDPM.PdaRecords 只有一個聚集索引,而且聚集索引位於Iden自增欄位上,從另外一個角度來看,這個表其實是缺少合適的索引的。那麼我們可以創建一個索引。
CREATE INDEX IX_PdaRecords_N1 ON wdpm.PdaRecords(OrderNo,FunctionName)
創建索引後,即使不更新臨時表#Batch的統計信息,我們發現執行計劃也會走嵌套迴圈(Nested Loops),而不會走Hash Join了。這個又是什麼原因呢?
此處截圖,是第二次執行SQL,臨時表的數據變化了(生成臨時表的數據的SQL有好幾個,每次執行獲取的數據都會有部分變化)
因為有了合適的索引,趨近準確的統計信息,以及謂詞下推(predicate push down),基數(Cardinality)的預估行數(Esitmted Row Size)為35.0545 與實際行數(Actual Number of Rows)為666, 這樣即使迴圈次數為140. 總的訪問記錄數為140*666=93240 , 這個是遠遠小於之前錯誤執行計劃的138265650 。所以即使臨時表的#Batch的統計信息有誤,但是優化器還是生成了一個不錯的執行計劃。這樣SQL的執行時間也就縮短到了1秒內.
這個案例僅僅是為了展示:統計信息的準確與否,會導致優化器生成的執行計劃選擇不同的表連接方式, 例如從嵌套迴圈(Nested Loops)變成Hash Join。 僅僅是為了說明統計信息準確的重要性。