WHERE条件对索引列使用函数(如YEAR(created_at)=2024)会破坏索引有序性,导致优化器放弃索引,引发临键锁或全表扫描加锁,应改写为范围查询。

为什么 WHERE 条件用错函数会让索引失效、锁全表
MySQL 在执行 UPDATE/DELETE 时,如果优化器没走索引,或者走了索引但无法精确定位行,就会退化成临键锁(next-key lock)甚至全表扫描加锁。最典型的情况是:对索引列用了函数或表达式,比如 WHERE YEAR(created_at) = 2024 或 WHERE phone LIKE '%123'——这时索引的有序性被破坏,优化器只能放弃使用该索引。
后果不是“查得慢”,而是“锁得多”:本该只锁几行,结果锁住整个索引范围,甚至阻塞其他事务对无关数据的修改。
- 避免在索引列上使用函数:改写为
WHERE created_at >= '2024-01-01' AND created_at - 模糊查询慎用前导通配符:
LIKE '%abc'必定索引失效;LIKE 'abc%'可走索引 - 隐式类型转换也会失效:比如
user_id是INT,却传字符串WHERE user_id = '123',MySQL 可能放弃索引
FORCE INDEX 能强制走索引,但不等于锁就安全
加 FORCE INDEX 确实能让优化器选中你指定的索引,但它只解决“走哪个索引”的问题,不解决“锁多少行”。如果 WHERE 条件本身范围过大(比如 WHERE status IN (0,1,2,3,4)),即使走了索引,也可能锁住大量索引记录及其间隙。
更危险的是:当强制的索引不是最优覆盖索引时,MySQL 还得回表,而回表过程可能引入额外锁竞争。
- 优先用覆盖索引:确保
SELECT或UPDATE涉及的字段都在索引里,避免回表 -
FORCE INDEX后务必用EXPLAIN FORMAT=TRADITIONAL看key_len和rows,确认实际扫描行数可控 - 不要在高并发写场景下盲目加
FORCE INDEX,它可能把锁争用从“随机分散”变成“集中热点”
UPDATE 语句中 SET 字段影响锁行为吗
不影响锁的范围,但影响锁的类型和持续时间。MySQL 行锁是加在聚簇索引(主键)上的,只要 WHERE 能准确定位到主键值,锁的就是那几行。但如果你的 SET 修改了二级索引列,MySQL 会同时更新对应二级索引项——这意味着:除了主键锁,还可能对二级索引加 X 锁(尤其是唯一索引冲突检查时)。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
常见陷阱是:以为只改非索引字段就没事,结果因为二级索引字段也被更新(比如 UPDATE t SET name='x', updated_at=NOW() WHERE id=123,而 updated_at 在某个联合索引里),触发额外索引维护锁。
- 检查所有被
SET的字段是否出现在任何二级索引中 - 避免在高频更新语句里修改包含在大范围查询索引中的字段(如
status、type) - 如果必须改,考虑拆成两步:先改主键字段,再异步更新索引字段(需业务层配合)
如何验证某条 UPDATE 是否真的只锁了预期的行
不能只看 EXPLAIN,它只告诉你“怎么查”,不告诉你“锁什么”。真实锁行为得靠 INFORMATION_SCHEMA.INNODB_TRX + INNODB_LOCKS(MySQL 5.7)或 performance_schema.data_locks(8.0+)来观察。
最实用的现场验证方式是:在事务里执行 UPDATE 后,立刻开另一个会话查锁状态,而不是等超时或死锁才发觉。
- MySQL 8.0:执行
SELECT * FROM performance_schema.data_locks WHERE OBJECT_SCHEMA = 'db_name' AND OBJECT_NAME = 'table_name'; - 重点关注
LOCK_DATA字段,它显示具体锁住的主键值或索引值(如123或'pending', 456) - 如果看到
LOCK_MODE是X,REC_NOT_GAP,说明是精准行锁;如果是X,GAP或X,INSERT_INTENTION,说明有间隙锁介入,需检查 WHERE 是否存在范围条件
真正难的不是加对索引,而是理解“索引选对了”和“锁住的行数合理”之间还隔着执行计划、隔离级别、索引结构三层逻辑。稍一疏忽,FORCE INDEX 就成了锁放大器。










