系統上線初期數據量小,SQL 寫法優劣難見真章;隨數據增長,劣質 SQL 與優質 SQL 速度可相差百倍。本文深度剖析 WHERE 子句六大常見反模式,揭示導致優化器放棄索引、退化為全表掃描的根本原因,並給出改寫模板。
原理:B+ 樹索引默認不存儲 NULL 值(Oracle 明確不存,MySQL InnoDB 單列索引不存,聯合索引全 NULL 不存)。任何包含 NULL 的列,該行不在索引中。
後果:WHERE col IS NULL 或 IS NOT NULL 必須全表掃描。
改寫策略:
NULL,用默認值(空字符串、0、'1900-01-01')替代,並加 NOT NULL 約束。NULL,建立函數索引(Oracle: CREATE INDEX idx_col_nn ON t(NVL(col,'NULL'));,PostgreSQL: CREATE INDEX ON t((col IS NULL))),查詢改用函數表達式。案例:員工表 First_Name、Last_Name 分列存儲,查詢「Bill Clinton」。
-- ❌ 錯誤:連接列,索引失效
SELECT * FROM Employee WHERE First_Name || ' ' || Last_Name = 'Bill Clinton';
-- ✅ 正確:拆分條件,各列獨立使用索引
SELECT * FROM Employee WHERE First_Name = 'Bill' AND Last_Name = 'Clinton';
變量場景:應用層拿到完整姓名變量 @FullName,不可在 SQL 中拆分(函數作用於列會失效),應在應用層拆分後傳兩個參數,或用覆蓋索引包含兩列讓優化器選擇 Index Skip Scan(Oracle 12c+)。
-- ❌ 通配符開頭,無法用索引
SELECT * FROM Employee WHERE Last_Name LIKE '%Clinton%';
-- ✅ 通配符結尾,可用索引範圍掃描
SELECT * FROM Employee WHERE Last_Name LIKE 'Clinton%';
原理:B+ 樹索引按字符順序存儲,前綴固定才能定位範圍;前置通配符破壞前綴有序性,只能掃描全索引/全表。
替代方案:全文檢索(CONTAINS/MATCH AGAINST)、倒排索引、Elasticsearch 異構同步。
-- ❌ 排序列無索引或含表達式
SELECT * FROM Orders ORDER BY YEAR(OrderDate) DESC, Amount * 1.1;
-- ✅ 方案一:建立組合索引匹配排序
CREATE INDEX idx_od_ymd_amt ON Orders (YEAR(OrderDate), Amount); -- 需函數索引支持
-- ✅ 方案二:應用層排序(小結果集)
SELECT * FROM Orders WHERE ... ORDER BY OrderDate DESC LIMIT 100; -- 利用 OrderDate 索引
原則:ORDER BY 列應與索引順序一致(或反向),避免臨時表排序(Using filesort)。
-- ❌ 否定條件,優化器傾向全表掃描
SELECT * FROM Employee WHERE Salary <> 3000;
-- ✅ 改寫為正向範圍 OR,可用索引範圍掃描
SELECT * FROM Employee WHERE Salary < 3000 OR Salary > 3000;
原因:否定條件選擇率極高(接近 100%),優化器判斷全表掃描成本更低。改寫為兩個範圍 OR 可各自利用索引 RANGE SCAN 再 UNION。
-- ❌ IN 子查詢:先執行子查詢物化臨時表,再驅動主查詢
SELECT * FROM Orders WHERE CustomerID IN (SELECT CustomerID FROM Customers WHERE Country='USA');
-- ✅ EXISTS 相關子查詢:主查詢驅動,逐行探測子查詢,命中即停止
SELECT * FROM Orders o WHERE EXISTS (SELECT 1 FROM Customers c WHERE c.CustomerID=o.CustomerID AND c.Country='USA');
| 維度 | IN 子查詢 | EXISTS 相關子查詢 |
|---|---|---|
| 執行順序 | 子查詢先執行 → 物化 → 主查詢 | 主查詢驅動 → 逐行探測子查詢 |
| 索引利用 | 子查詢結果無索引(臨時表) | 子查詢可用外部引用列索引 |
| 早期終止 | 不支持 | 支持(找到 1 行即返回 TRUE) |
| NULL 語義 | 子查詢含 NULL 導致 UNKNOWN | 不受 NULL 影響,邏輯更直觀 |
| 適用場景 | 子查詢結果集小、靜態、可物化 | 外部表大、內部表有索引、高選擇性 |