文章:12 SQL Server Optimizations That Slashed My Query Times by 98% (12 個將我的查詢時間削減 98% 的 SQL Server 優化技巧)
這篇文章透過作者的親身經歷,分享了 12 個實用且高效的 SQL Server 查詢優化技巧。
前言與動機
段落重點: 作者分享了一個幾年前的經歷:一份財務團隊的月度銷售報表需要運行 15 分鐘,每次運行都讓資料庫伺服器不堪重負。直到有一天,財務長問他:「為什麼我的報表載入時間比我週末的 Netflix 待看清單還長?」這句話成為他的警鐘。在接下來的一個月裡,他嘗試了數十種優化方法,最終將報表時間從 15 分鐘縮短到了 2 秒。這篇文章將分享他學到的 12 個關鍵優化技巧。
總結: 此段落以一個生動的真實故事開場,強調了 SQL 查詢效能問題的嚴重性以及優化後帶來的巨大效益,成功地激發了讀者的興趣,並點明了文章的主旨——分享具體的優化實踐。
優化技巧 1-3
段落重點:
-
用明確的欄位取代
SELECT *:- 問題:
SELECT *會獲取所有欄位,掃描不必要的資料。 - 範例:將
SELECT * FROM Sales ...改為SELECT OrderID, OrderDate, TotalAmount FROM Sales ...。 - 原因:SQL Server 檢索的欄位更少,減少了 I/O 和記憶體使用。結果執行時間從 3.2 秒降至 1.1 秒。
- 問題:
-
使用適當的索引:
- 問題:頻繁的查詢正在進行全表掃描。
- 解決方案:建立非叢集索引,例如
CREATE NONCLUSTERED INDEX IX_Sales_OrderDate ON Sales(OrderDate);。 - 原因:索引讓 SQL Server 更快地定位資料行,避免全表掃描。結果邏輯讀取從 1,200,000 次降至 4,300 次。
-
更新過時的統計資訊:
- 問題:查詢優化器做出了錯誤的執行計畫猜測。
- 解決方案:運行
UPDATE STATISTICS Sales;。 - 原因:最新的統計資訊意味著更好的執行計畫。結果一個慢查詢從 9 秒縮短到 1 秒以下。
總結: 這三個基礎技巧是效能調校的基石。明確指定欄位可以減少資料傳輸量;建立索引是避免全表掃描、提升查詢速度最直接有效的方法;而保持統計資訊的更新則能確保查詢優化器基於準確的資料分佈來制定最高效的執行計畫。
優化技巧 4-6
段落重點:
4. 用 IN 或 UNION ALL 改寫 OR:
* 問題:OR 條件可能阻止索引的使用。
* 範例:將 WHERE Status = 'Shipped' OR Status = 'Delivered' 改為 WHERE Status IN ('Shipped', 'Delivered')。
* 原因:IN 讓優化器能夠使用索引搜尋(Index Seek)而非掃描(Scan)。
-
避免在
WHERE子句的索引欄位上使用函數:- 問題:在索引欄位上套用函數會使索引失效。
- 範例:將
WHERE YEAR(OrderDate) = 2023改為WHERE OrderDate >= '2023-01-01' AND OrderDate < '2024-01-01'。 - 原因:這樣能保持欄位的「可搜尋性」(SARGable)。
-
將關聯式子查詢轉換為
JOIN:- 問題:巢狀迴圈(Nested loops)嚴重影響效能。
- 範例:將
SELECT OrderID, (SELECT COUNT(*) FROM OrderDetails od WHERE od.OrderID = o.OrderID) ...改為SELECT o.OrderID, COUNT(od.ProductID) ... FROM Orders o JOIN OrderDetails od ON o.OrderID = od.OrderID GROUP BY o.OrderID;。 - 原因:
JOIN的執行效率通常遠高於逐行執行的關聯式子查詢。結果從 20 秒縮短到 0.4 秒。
總結:
這組技巧專注於改寫查詢以更好地利用索引和查詢優化器。避免使用 OR 和欄位函數能讓索引發揮作用,而將關聯式子查詢改寫為 JOIN 則是一種更高效的資料關聯方式,能大幅減少查詢的執行時間。
優化技巧 7-9
段落重點:
7. 儘早過濾資料:
* 問題:過晚過濾資料導致大量不必要的資料處理。
* 解決方案:在 CTE 或衍生表內部,於進行 JOIN 之前就應用 WHERE 過濾器。
* 重點:你越早過濾,SQL Server 需要搬運的垃圾資料就越少。
-
使用索引檢視表 (Indexed Views):
- 問題:重複的重量級彙總計算拖慢了報表速度。
- 解決方案:建立一個帶有唯一叢集索引的檢視表來預先計算彙總結果。
CREATE VIEW vMonthlySales WITH SCHEMABINDING AS SELECT OrderDate, SUM(TotalAmount) AS Total FROM dbo.Sales GROUP BY OrderDate; CREATE UNIQUE CLUSTERED INDEX IX_vMonthlySales ON vMonthlySales(OrderDate); - 重點:預先煮好你的彙總資料。結果報表生成時間從 6 秒降至 1 秒以下。
-
將複雜查詢分解為暫存表 (Temp Tables):
- 問題:查詢優化器難以處理龐大而複雜的查詢。
- 解決方案:將中間結果存入暫存表,再對暫存表進行後續操作。
SELECT * INTO #FilteredOrders FROM Orders WHERE OrderDate >= '2023-01-01'; SELECT f.OrderID, SUM(od.Total) FROM #FilteredOrders f JOIN OrderDetails od ON f.OrderID = od.OrderID GROUP BY f.OrderID; - 重點:有時候,一小口一小口地吃會更容易咀嚼。
總結: 這三個技巧提供了更進階的結構性優化策略。及早過濾是減少處理資料量的基本原則;索引檢視表能將複雜的彙總結果物化,將查詢成本轉移到寫入時;而使用暫存表分解複雜查詢,則可以簡化執行計畫,幫助優化器做出更好的決策。
優化技巧 10-12
段落重點:
10. 用 GROUP BY 取代 DISTINCT:
* 問題:DISTINCT 常常被用來掩蓋由不當 JOIN 產生的重複資料。
* 解決方案:當你需要彙總而不是簡單地去重時,使用 GROUP BY。
* 重點:別讓 DISTINCT 成為你的OK繃。
-
在
JOIN中匹配資料類型:- 問題:不匹配的資料類型會導致隱含轉換和全表掃描。
- 解決方案:確保
JOIN兩邊的欄位資料類型完全相同,例如ALTER TABLE Orders ALTER COLUMN CustomerID INT;。 - 重點:資料類型不匹配是沉默的效能殺手。
-
移除未使用的索引:
- 問題:過多的索引會拖慢寫入操作(INSERT/UPDATE/DELETE)並浪費儲存空間。
- 解決方案:使用
sys.dm_db_index_usage_stats動態管理檢視來找出未被使用的索引,然後將它們刪除。 - 重點:每個索引都有其維護成本。
總結:
最後這組技巧關注的是查詢的精確性和系統的整體健康度。使用 GROUP BY 而非 DISTINCT 能更清晰地表達意圖;匹配 JOIN 欄位的資料類型可以避免隱式轉換帶來的效能懲罰;而定期清理未使用的索引則是在讀寫效能之間取得平衡的關鍵維護工作。
整篇文章結論
這篇文章透過一個從 15 分鐘優化到 2 秒的真實案例,系統性地介紹了 12 個極具價值的 SQL Server 效能優化技巧。這些技巧從基礎到進階,涵蓋了查詢撰寫、索引策略、資料庫結構以及日常維護等多個層面。
核心觀念是,高效的 SQL 不僅僅是寫出能得到正確結果的程式碼,更是要理解資料庫引擎如何工作,並撰寫出「對優化器友好」的查詢。這包括:
- 精準取用:只查詢需要的欄位 (
SELECTspecific columns)。 - 善用捷徑:建立並使用正確的索引 (
INDEX),避免在索引欄位上使用函數。 - 聰明連接:用
JOIN取代低效的子查詢,並確保JOIN欄位資料類型一致。 - 化繁為簡:將大查詢分解為使用暫存表的幾個小步驟。
- 預先計算:對於頻繁的彙總操作,使用索引檢視表 (
Indexed View)。 - 定期維護:更新統計資訊 (
STATISTICS) 並移除無用索引。
總結來說,這是一份非常實用的行動指南。對於任何希望提升資料庫效能的開發者或 DBA 而言,遵循這些原則不僅能解決眼前的效能瓶頸,更能培養出一種思考 SQL 效能的良好習慣,從而建構出更穩健、更快速的系統。