面對一條看似簡單的查詢,如何通過索引設計引導優化器生成最優計劃?本文以 Sales.SalesOrderDetail 表為例,完整演示從「全掃描 + 排序溢寫 TempDB」到「覆蓋索引 + 有序輸出」的調優全過程,揭示優化器的決策邏輯與陷阱。
DECLARE @i INT = 999; SELECT SalesOrderID, SalesOrderDetailID, CarrierTrackingNumber, OrderQty, LineTotal FROM Sales.SalesOrderDetail WHERE ProductID < @i ORDER BY CarrierTrackingNumber;
初始計劃缺陷:
ProductID < @i。ProductIDCREATE NONCLUSTERED INDEX idx_Test ON Sales.SalesOrderDetail(ProductID);
結果:優化器拒絕使用。強制提示 WITH (INDEX(idx_Test)) 後,出現 並行計劃 + 書簽查找,邏輯讀暴增至 37 萬,性能反而更差。原因:查詢返回絕大多數行(超過臨界點),書簽查找開銷遠超全掃描。
CREATE NONCLUSTERED INDEX idx_Test ON Sales.SalesOrderDetail(ProductID) INCLUDE (CarrierTrackingNumber, OrderQty, UnitPrice, UnitPriceDiscount) WITH (DROP_EXISTING = ON);
結果:優化器自動選用,邏輯讀降至 757,但 Sort 運算符仍在,仍溢寫 TempDB(基數估算未變)。耗時仍 ~800ms。
CREATE NONCLUSTERED INDEX idx_Test ON Sales.SalesOrderDetail(CarrierTrackingNumber, ProductID) INCLUDE (OrderQty, UnitPrice, UnitPriceDiscount) WITH (DROP_EXISTING = ON);
結果:Sort 運算符消失,改為索引有序掃描 + Residual Predicate 過濾。邏輯讀 763,耗時降至 600ms(提升 25%)。無 TempDB 溢寫,基數估算誤差不再影響性能。
-- 優化器建議:ProductID 為鍵,INCLUDE 所有選列表列 CREATE NONCLUSTERED INDEX [...] ON Sales.SalesOrderDetail(ProductID) INCLUDE (SalesOrderID, SalesOrderDetailID, CarrierTrackingNumber, OrderQty, LineTotal);
驚人發現:創建後,優化器使用舊索引(步驟 3 的索引),而非新建議索引。新索引僅增加寫入開銷,佔用空間,從未被查詢使用。
WHERE 等值列優先,ORDER BY 列緊隨其後,INCLUDE 覆蓋選列表。OPTION (RECOMPILE) 或參數化處理。調優是與優化器博弈的過程:理解其成本模型,用索引結構表達業務意圖,再用事實數據校正預期。