一個查詢從 3 秒降到 50 毫秒,中間到底發生了什麼事?多數時候答案不是「換更快的伺服器」, 而是一個被忽略已久的索引問題。這篇整理實務上處理 SQL Server 慢查詢時的思路與具體作法。
為什麼「加索引」不是萬靈丹
很多人對索引的認知停留在「查詢慢就加索引」,但索引不是免費的午餐。每多一個索引, 寫入(INSERT / UPDATE / DELETE)就要多付出一次索引維護的成本。當一張表的索引超過 5~6 個,寫入效能往往會開始明顯下降。所以優化的第一步從來不是「加索引」, 而是「先搞懂查詢在做什麼」。
用 Execution Plan 找出真正的瓶頸
SQL Server Management Studio 的「顯示估計執行計畫」(Ctrl+L)是診斷的起點。 實務上最常見的三個警訊:
- Table Scan / Clustered Index Scan: 代表查詢掃描了整張表,沒有用到任何索引。表越大,代價越高。
- Key Lookup: 查詢用了非叢集索引(Non-clustered Index)找到資料列,但還需要額外查回主表取得其他欄位。 如果 Key Lookup 出現的次數很多,通常代表索引該加「包含欄位」(INCLUDE)。
- 高估計成本的 Sort / Hash Match: 常見於 ORDER BY 或 JOIN 沒有對應索引時,SQL Server 被迫在記憶體(甚至磁碟)中做排序或雜湊比對。
實務案例:從 3 秒到 50 毫秒
某次處理一個訂單查詢頁,條件是「依客戶編號查詢近三個月的訂單,並依建立時間排序」。 原本的查詢在資料量成長到約 80 萬筆後開始明顯變慢,Execution Plan 顯示為 Clustered Index Scan,代表完全沒用到索引。
解法是建立一個複合索引(Composite Index),把 customer_id 放在最前面
(因為查詢一定會用等於條件過濾),再加上 created_at 作為排序欄位,
並用 INCLUDE 帶入查詢會用到但不需要排序的欄位,避免 Key Lookup:
CREATE NONCLUSTERED INDEX IX_Orders_CustomerId_CreatedAt
ON Orders (customer_id, created_at DESC)
INCLUDE (order_no, total_amount, status);
這個索引設計的關鍵在於:等於條件的欄位放前面、範圍或排序條件放後面, 查詢會用到但不需要用來過濾或排序的欄位放進 INCLUDE。調整後, 同樣的查詢從 3 秒左右降到 50 毫秒以內。
索引設計的三個實務原則
- 複合索引的欄位順序很重要: 等於條件(=)的欄位放前面,範圍條件(>, <, BETWEEN)或排序欄位放後面。 順序錯了,索引可能完全用不到。
- 不是每個查詢都值得建索引: 如果某個查詢一天只執行幾次,但對應的表寫入非常頻繁, 為了這個查詢加索引可能得不償失。
-
定期檢查未使用的索引:
可以用
sys.dm_db_index_usage_stats這個 DMV 找出長期沒被查詢用到、卻持續拖慢寫入效能的索引,該砍就砍。
結語
索引優化不是一次性的工作,而是隨著資料量成長、查詢模式改變需要持續檢視的過程。 養成先看 Execution Plan、再動手調整的習慣,比盲目加索引更能長期維持系統效能。