經過前面四篇文章,能夠知道 Execution Plan 的四大核心:閱讀方向(粗細箭頭)、存取方式(Seek 與 Scan)、關聯策略(三大 Join),以及警告標誌(四大陷阱)。

然而,當正式面對一張包含幾十個、甚至上百個節點的巨大執行計畫時,很容易再度感到不知所措;這時候,需要的是一套系統化的除錯 SOP;效能調校的心法「一次只動一個地方,並且永遠以數據驗證差異。」以下是從一團亂麻中找出解方的步驟:


步驟一:鎖定「最貴」的目標 (Find the Most Expensive)

面對龐大的執行計畫,不要試圖由右至左把每個節點都看懂,需要「擒賊先擒王」。

  • 尋找 Cost % 最高的節點:每個節點下方都會標示該步驟佔整句查詢成本的百分比;直接掃視全圖,把目光鎖定在那些標示 40%、60% 甚至 90% 的高成本節點上。
  • 追蹤「最粗的箭頭」:尋找哪兩個節點之間傳遞了異常龐大的資料量,特別是「漏斗效應」;如果一個節點右邊進來很粗的箭頭,左邊出去卻變得很細,這代表它浪費了大量資源在過濾不必要的資料。

限縮範圍,挑出 1 到 2 個效能最差的局部節點作為第一階段的開刀對象,不要急著對整句 SQL 做全域(Global)的改寫。


步驟二:辨識症狀與警告 (Identify Symptoms)

鎖定可疑節點後,把滑鼠懸停(Hover)在該節點上,開始進行「健康檢查」:

  • 有沒有黃色驚嘆號:如果是 CONVERT_IMPLICIT,就去檢查程式端參數型別是不是和資料庫不符(尤其是字串型別);如果是 Spill to TempDB,先去檢查統計資料是否過期。
  • 它是怎麼存取資料的:如果是 Table Scan 或 Clustered Index Scan,檢查 WHERE 條件欄位是不是漏建了索引,或者條件寫法讓索引失效了(例如在欄位上套用函數);如果是 Index Seek,點開屬性檢查有沒有發生了「殘餘篩選條件(Residual Predicate)」,導致讀取了一堆資料卻被拋棄。
  • 有沒有跟著 Key Lookup:如果高成本節點是 Key Lookup,去看看 SELECT 了哪些多餘的欄位,或者把它們加入現有索引的 INCLUDE 清單中。

步驟三:戳破 Optimizer 的幻覺 (Check Row Estimates)

很多時候,SQL Server 選了極差的執行計畫(例如:不該用 Hash Match 卻用了),是因為它「猜錯了資料量」:

  • 比對預估與實際:在節點的詳細屬性中,仔細比對 Estimated Number of Rows(預估資料列數)與 Actual Number of Rows(實際資料列數)。
  • 落差代表什麼:如果 Optimizer 預估只有 10 筆,實際卻跑出 100 萬筆,它就會錯配過少的記憶體(導致 Spill)或選錯 Join 方式(導致 Nested Loops 癱瘓)。
  • 如何修正幻覺:這種落差通常是因為「統計資料(Statistics)」太久沒更新,不要急著改語法,先執行 UPDATE STATISTICS 資料表名稱,有極高的機率,更新完後 Optimizer 就會自動切換成完美的執行計畫。

步驟四:對症下藥與驗證 (Prescribe and Verify)

找出了病因,最後一步就是採取行動,請依循以下流程:

  • 開啟驗證數據:在執行任何修改前,請先在 SSMS 執行 SET STATISTICS IO ONSET STATISTICS TIME ON,記錄下原本的 Logical reads(邏輯讀取次數)與耗時。
  • 單一變數法則:一次只做一個改變,要嘛新建一個索引,要嘛改寫一段 WHERE,要嘛更新統計資料;不要同時做兩件事,否則會不知道是哪個動作起了作用。
  • 比對前後差異:再次執行查詢,查看新的 Execution Plan,原本的 Scan 變成 Seek 了嗎?黃色警告消失了嗎?更重要的是,Logical reads 是否有顯著下降?在資料庫的世界裡,降低 Logical reads 才是降低伺服器負載的指標。

透過這系列文章,從觀念建立到實戰除錯,明白了 SQL Server 的 Execution Plan 不是死板的系統報告,而是 Query Optimizer 在訴說「我從這裡拿資料,我用這種方式比對,我在這裡遇到了記憶體不足的困難……」

效能調校,是一場開發者與資料庫的對話,下次遇到跑出 Timeout 的 SQL 查詢時,大膽地點開 Execution Plan,看懂它的求救信號,人人都有機會成為精準治癒效能痼疾的資料庫神醫!