把欄位包進函數,WHERE 就用不到那個欄位的索引

索引存的是欄位原本的值。一旦你在 WHERE 裡把欄位丟進函數,資料庫要比對的就不再是索引裡那個值,只能每一列都先算一次函數再比——索引直接出局。

WHERE COALESCE(bonus, 0) < 1000      -- ❌ bonus 的索引用不到
WHERE bonus < 1000 OR bonus IS NULL  -- ✅ 用得到
 
WHERE DATE(created_at) = '2026-09-20'                          -- ❌
WHERE created_at >= '2026-09-20' AND created_at < '2026-09-21' -- ✅
 
WHERE UPPER(email) = 'A@B.COM'       -- ❌
WHERE email = 'a@b.com'              -- ✅(改用 citext 或函數索引)

判準很簡單:欄位必須光禿禿地站在比較運算子的一邊。 這個性質有個名字叫 sargable(Search ARGument ABLE)。

更精確的講法是看哪一邊被包:

轉型發生在「欄位」上會出事,發生在「常數」上沒關係。

WHERE height::text = '1.56'    -- ❌ 欄位被轉型 → numeric 索引失效
WHERE height = '1.56'::numeric -- ✅ 轉的是常數 → 索引照用

常數那一側你怎麼包都行——它只算一次,而且算完就是一個值,索引照樣定位得到。

為什麼這條常被忽略

因為包函數的版本通常更好讀——COALESCE(bonus, 0) < 1000 一句話講完意圖,比 bonus < 1000 OR bonus IS NULL 乾淨。可讀性和 sargability 在這裡是對立的,而可讀性的收益是常數,索引的收益隨資料量放大。

這正是 WHERE 決定回傳幾列,索引才決定要看過幾列 的延伸:WHERE 寫得再精準,決定掃描量的仍然是索引有沒有派上用場。包了函數 = 索引沒派上用場 = 回到 full scan,不管條件多窄。

真的需要的時候

如果那個表達式本身就是你的查詢模式(例如永遠用小寫 email 查),大多數資料庫可以建函數索引 / 表達式索引:

CREATE INDEX idx_email_lower ON users (LOWER(email));

建了之後 WHERE LOWER(email) = ... 就變回 sargable。重點不是「不准用函數」,而是索引必須跟 WHERE 裡的表達式長得一樣。