WHERE 条件中对索引列使用函数或运算会导致索引基本不走,因破坏 B + 树有序性;正确做法是保持左侧为裸列名,将计算移至右侧或改用范围查询。

WHERE 里对索引列做计算,索引直接不走
MySQL、PostgreSQL、SQL Server 都一样:只要在 WHERE 条件里对已建索引的列用了函数或运算,优化器大概率放弃使用该索引。不是“可能失效”,是“基本不走”。比如 WHERE YEAR(create_time) = 2024,哪怕 create_time 上有索引,也大概率全表扫描。
- 常见错误现象:
EXPLAIN显示type=ALL或key=NULL,执行变慢,CPU/IO 暴涨 - 根本原因:索引是按原始值有序存储的,而
YEAR()、UPPER()、col + 1等操作破坏了有序性,无法用 B + 树快速定位 - 不是所有函数都绝对禁止——像 MySQL 8.0+ 对
CAST(col AS DATE)在某些场景下能用索引,但别赌,优先避开
把计算提到右边,让左边保持纯列名
最稳妥的写法,是把计算逻辑从索引列上“剥离开”,挪到等号右侧或子查询里。核心原则:WHERE 左侧必须是裸列名(或列名加简单前缀如 table.col)。
- 错的:
WHERE DATE(create_time) = '2024-01-01'→DATE()作用于索引列 - 对的:
WHERE create_time >= '2024-01-01' AND create_time → 左侧仍是 <code>create_time - 字符串场景同理:
WHERE UPPER(name) = 'ABC'改成WHERE name = 'abc'(配合大小写敏感索引或COLLATE utf8mb4_0900_as_cs) - 如果业务真需要模糊前缀匹配,用
LIKE 'prefix%'而非SUBSTR(col, 1, 5) = 'prefix'
更新语句里的 WHERE 同样适用这条铁律
UPDATE 不是特例——它底层仍依赖 WHERE 的执行计划。一旦 WHERE 失效,不仅慢,还可能锁全表(尤其在 RR 隔离级别下),引发阻塞。
- 危险写法:
UPDATE users SET status = 1 WHERE DATE(last_login) = CURDATE()→ 可能锁住几百万行 - 安全写法:
UPDATE users SET status = 1 WHERE last_login >= CURDATE() AND last_login - 注意
CURDATE()是函数,但它没作用在列上,不影响索引;真正关键的是last_login左侧是否“干净” - 批量更新时,务必先
EXPLAIN对应的SELECT语句,确认 key 和 rows 量级合理
特殊情况:函数索引不是万能解药
MySQL 8.0+、PostgreSQL 支持函数索引(如 CREATE INDEX idx_year ON t (YEAR(create_time))),但它只对 ** 完全匹配该函数表达式 ** 的查询生效。
- 你建了
YEAR(create_time)索引,那只有WHERE YEAR(create_time) = 2024才能用;换成YEAR(create_time) > 2023或WHERE create_time BETWEEN ……依然无效 - 函数索引会增加写开销和存储,且无法覆盖范围查询,实用性有限
- 更隐蔽的坑:JSON 字段里取值如
json_col->>'$.id',即使建了函数索引,MySQL 对它的优化仍不稳定,生产环境慎用
真正难的不是知道“别在 WHERE 里算”,而是每次写条件前下意识检查左侧是不是赤裸裸的列名——尤其是拼接动态 SQL 或用 ORM 自动生成时,很容易漏看一层包装。