不良 SQL 效能多源於三大根因:索引設計不當、連接條件不足、WHERE 子句無法利用索引。本文結合實測數據(Sybase 11.0.3,62 萬筆測試表),深度剖析三大優化方向與具體寫法。
一、索引設計:群集 vs 非群集 vs 組合索引
測試表 `record`(62 萬筆),查詢條件含 `date`(大量重複值)、`place`、`amount`。
| 索引類型 | 查詢 1:date 範圍 + amount | 查詢 2:date GROUP BY | 查詢 3:date 範圍 + place IN |
| date 非群集 | 25 秒 | 55 秒 | 27 秒 |
| date 群集 | 14 秒 | 28 秒 | 14 秒 |
| 組合 (place, date, amount) | 26 秒 | 27 秒 | <1 秒(索引覆蓋) |
| 組合 (date, place, amount) | <1 秒(覆蓋) | 11 秒 | <1 秒(覆蓋) |
核心結論
- 群集索引:適合大量重複值、頻繁範圍查詢(BETWEEN, >, <)、ORDER BY/GROUP BY 的欄位。資料物理順序與索引一致,範圍掃描極快。
- 組合索引:前導欄位必須為最高頻率查詢欄位;盡量讓關鍵查詢形成索引覆蓋(所有查詢欄位皆在索引中),避免回表。
- 預設建立的非群集索引未必最佳,需依查詢模式分析調整。
二、連接條件:充分條件才能誘導最佳執行計畫
測試表 `card`(7,896 筆,card_no 非群集)與 `account`(191,122 筆,account_no 非群集)。
| 連接條件 | 執行時間 | I/O 估算 | 最佳驅動表 |
| `a.card_no = b.card_no` | 20 秒 | 595,907 | account (外層) → card (內層) |
| `a.card_no = b.card_no AND a.account_no = b.account_no` | <1 秒 | 33,528 | card (外層) → account (內層) |
原則
- 連接條件需包含帶索引的欄位,且優先考慮行數較少的表做外層驅動表。
- I/O 成本公式:`外層表頁數 + (外層表行數 × 內層表單次查找頁數)`,乘積最小者為最佳計畫。
- 使用 `SET SHOWPLAN ON` 檢視執行計畫;`DBCC TRACEON(3604,310,302)` 取得詳細資訊(需 SA 權限)。
三、WHERE 子句:避免運算導致索引失效
1. 欄位運算移至等號右側
| 低效寫法(掃表) | 高效寫法(用索引) | 原理 |
| `SUBSTRING(card_no,1,4)='5378'` (13秒) | `card_no LIKE '5378%'` (<1秒) | 函數/運算在執行時逐列計算,無法用索引 |
| `amount/30 < 1000` (11秒) | `amount < 1000*30` (<1秒) | 同左 |
| `CONVERT(char(10),date,112)='19991201'` (10秒) | `date = '1999/12/01'` (<1秒) | 同左 |
2. IN / OR 易觸發工作表策略,導致索引失效
測試 `stuff` 表(20 萬筆,id_no 非群集):
- `WHERE id_no IN ('0','1')` → 23 秒(轉為 OR 策略,建立臨時工作表,未用索引)
- 拆分為兩條 `UNION ALL` 或分別查詢再相加 → 3 秒(各用索引)
- 或寫存儲過程分別查詢加總 → 同樣快
建議:不產生大量重複值時,拆分 IN/OR;需去重時用 `UNION`(非 `UNION ALL`)。
3. 善用存儲過程封裝邏輯,減少網路往返與編譯開銷
開發規範速查(節錄)
SQL 寫法
- 避免大事務、慎用 `HOLDLOCK`,提升並發。
- 避免重複存取大表,先放入暫存表再連接。
- 慎用游標,超過 1 萬行必改寫;游標中避免再做連接。
- WHERE 條件順序依索引順序、範圍大小排序(大→小)。
- **絕不**在 `=` 左側對欄位做函數/運算。
- 用 `EXISTS` 代替 `COUNT(1)` 判斷存在性;`COUNT(1)` 優於 `COUNT(*)`。
- 優先用 `>=` 而非 `>`。
- 注意 `OR` 與 `UNION` 互換場景。
- 連接欄位資料型別必須一致,避免隱含轉換。
- 單次 INSERT/UPDATE 超過 200 頁(400KB)會觸發鎖升級(頁鎖→表鎖)。
索引使用
- 大型 OLTP 表建議不超過 6 個索引。
- 優先用索引欄位查詢,必要時 `INDEX (index_name)` 強制指定。
- 避免大表 Table Scan,必要時新增索引。
- 聯合索引必須用到第一欄位才會被使用。
- 定期重建索引、重新編譯存儲過程。
TempDB 使用
- 減少 `DISTINCT/ORDER BY/GROUP BY/HAVING/JOIN/COMPUTE` 等加重 TempDB 負擔的操作。
- 避免頻繁建刪暫存表;大量資料用 `SELECT INTO` 避免 Log,小量用 `CREATE TABLE + INSERT`。
- 大暫存表需建索引時,將建表與建索引放入子存儲過程,確保優化器能用到索引。
- 結束時顯式 `TRUNCATE TABLE` 再 `DROP TABLE`,減少系統表鎖定時間。