一個查詢從 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 毫秒以內。

索引設計的三個實務原則

  1. 複合索引的欄位順序很重要: 等於條件(=)的欄位放前面,範圍條件(>, <, BETWEEN)或排序欄位放後面。 順序錯了,索引可能完全用不到。
  2. 不是每個查詢都值得建索引: 如果某個查詢一天只執行幾次,但對應的表寫入非常頻繁, 為了這個查詢加索引可能得不償失。
  3. 定期檢查未使用的索引: 可以用 sys.dm_db_index_usage_stats 這個 DMV 找出長期沒被查詢用到、卻持續拖慢寫入效能的索引,該砍就砍。

結語

索引優化不是一次性的工作,而是隨著資料量成長、查詢模式改變需要持續檢視的過程。 養成先看 Execution Plan、再動手調整的習慣,比盲目加索引更能長期維持系統效能。