MySQL索引下推(ICP)失效的主因包括:对索引列使用YEAR()/DATE()等函数、子查询中聚合后过滤、隐式类型转换、字符集不匹配及不确定函数(如now())。应改用范围查询、内层过滤、显式类型、统一collation和预计算值。

WHERE里对索引列用YEAR()、DATE()就失效
MySQL 的索引下推(ICP)只认“原样”的索引列值,一旦你写 WHERE YEAR(created_at) = 2024,优化器就无法把该条件下推到 InnoDB 存储引擎层——因为 B+ 树里存的是完整时间戳,不是年份整数。
实际表现是:执行计划中 Extra 字段看不到 Using index condition,只有 Using where;rows 值偏大,说明全量读取索引页后才在 Server 层过滤。
- 换成
WHERE created_at >= '2024-01-01' AND created_at ,ICP 立刻生效 -
UPPER(name)、COALESCE(status, 'active')同理,都会阻断 ICP - 哪怕只是加个
+ 0或IS NULL判断,只要让列值被“包装”,下推就大概率失败
子查询里用了GROUP BY,外层WHERE过滤不了
嵌套查询如 (SELECT user_id, MAX(created_at) AS last_login FROM users GROUP BY user_id) t,t.last_login 是聚合结果,不是基表字段,数据库不会给它建索引,外层写 WHERE t.last_login > '2024-01-01' 只能等整个子查询跑完再过滤。
这不是语法错误,而是语义断层:聚合后的别名字段无法参与谓词下推,优化器没路可走。
- 正确做法是把日期过滤塞进子查询内部:
WHERE created_at > '2024-01-01'放在GROUP BY前 - 如果业务逻辑强制依赖聚合后结果(比如“最近一次登录超7天未活跃”),就得考虑物化:MySQL 8.0+ 可用
WITH t AS (...) /*+ MATERIALIZE */ SELECT ... - 避免在子查询中写
SELECT *,只取user_id和MAX(created_at),减少中间结果体积
隐式类型转换让索引和下推同时掉线
当索引列是 INT,但你写 WHERE user_id = '123',MySQL 会把每行的 user_id 转成字符串再比——这既破坏了索引的有序性,也让 ICP 无从下手。
执行计划里可能仍显示 key 用了索引,但 rows 高得离谱,且 Extra 没有 Using index condition,就是典型信号。
- 统一用数字字面量:
WHERE user_id = 123 - 检查列定义和传入参数的类型是否严格一致,尤其注意 ORM 自动生成 SQL 时容易带单引号
- 字符集不匹配(比如
name = '张三' COLLATE utf8mb4_0900_as_cs而索引用默认 collation)也会触发同样问题
不确定函数让整个WHERE条件失去下推资格
像 unix_timestamp()、rand()、now() 这类函数每次执行返回值可能不同,优化器无法在编译阶段确定其值,于是放弃对整条 WHERE 的谓词下推——哪怕其中混着确定性条件,比如 WHERE ds = '2024-01-01' AND create_time > unix_timestamp(),前半部分也会被拖下水。
这不是 bug,是代价模型下的保守策略:宁可多扫几行,也不冒语义错乱风险。
- 把动态条件拆出去,用应用层生成具体时间点再拼 SQL
- 用变量缓存一次结果(但注意:用户变量本身也禁用 ICP,慎用)
- 对实时性要求不高的场景,改用分区裁剪(比如按
ds分区)替代函数过滤
谓词下推不是开关一开就自动生效的魔法,它高度依赖你写的每个字符是否“干净”:有没有函数包裹、类型是否对齐、有没有不确定表达式、聚合是否隔断了原始列路径。真正卡住性能的,往往不是数据量,而是那一个括号、一个单引号、一个没注意的隐式转换。

















