MySQL联合索引必须遵循最左前缀原则,WHERE条件若跳过最左列(如索引为(user_id, status, created_at)却查status = 'paid'),索引完全失效,触发全表扫描和大量行锁,显著增加死锁风险。

为什么WHERE条件顺序和索引字段顺序必须严格对齐
MySQL 的 InnoDB 只能按联合索引的**最左前缀**匹配 WHERE 条件。如果索引是 (user_id, status, created_at),但查询写成 WHERE status = 'paid' AND created_at > '2024-01-01',那这个索引根本不会被选中——执行计划里 key 列会是 NULL,最终触发全表扫描,每行都加 X 锁,死锁面直接拉满。
- 高频等值查询字段(如
user_id、pool_id)必须放在联合索引最左侧 - 范围查询字段(
created_at > ?、id BETWEEN ? AND ?)只能放末尾,否则后续字段失效 -
ORDER BY和GROUP BY字段也得纳入索引设计,否则排序阶段可能触发临时表 + 文件排序,延长锁持有时间
EXPLAIN看到type=ALL或key=NULL就是死锁高危信号
别只看 SQL 写得“看起来合理”。在 Hyperf 里跑一条 Db::table('orders')->where('status', 'pending')->update(['status' => 'processing']),如果 status 没单独索引,或者只是复合索引里的非前缀字段,EXPLAIN FORMAT=TRADITIONAL UPDATE ... 就会显示 type=ALL。这意味着 InnoDB 正在逐行扫描并加锁,而不是精准定位到几行。
- 用
EXPLAIN检查所有带WHERE的UPDATE/DELETE/SELECT FOR UPDATE - 特别注意
possible_keys和key是否一致,rows是否远超预期(比如查 1 个用户却扫了 5000 行) - 函数操作会让索引完全失效:
WHERE ABS(score) > 80→ 改成score > 80 OR score < -80
联合索引里NULL字段会破坏前缀匹配,引发间隙锁扩散
比如建了索引 (pool_id, identifier, status),但 identifier 允许为 NULL,而业务查询却是 WHERE pool_id = 123 AND status = 'used'。InnoDB 无法跳过中间的 identifier 直接用 status,结果只用了 pool_id 前缀,导致范围扩大,间隙锁覆盖大量无关记录——多个事务一碰就死锁。
- 把可空字段挪到联合索引末尾,或干脆设为
NOT NULL(需业务允许) - 若必须保留可空字段且常用于等值查询,单独建索引比塞进联合索引更稳妥
- 用
SHOW INDEX FROM table_name确认Null列是否为YES,再结合实际查询条件反推索引有效性
Hyperf 中 ORM 预加载和 chunk() 也会悄悄触发无索引扫描
with(['orderItems']) 或 Db::table('orders')->chunk(100, ...) 看似安全,但如果底层 ORDER BY 字段没索引(比如按 created_at 分页),MySQL 仍会生成 WHERE created_at > ? 查询——而该字段若无索引,就又回到全表扫描加锁的老路。
-
chunk()默认按主键分页,但显式指定orderBy('created_at')后,必须确保created_at有索引 -
with()触发的 JOIN 查询,ON字段和WHERE字段要共同设计联合索引,单字段索引无效 - 在 Hyperf 控制器里开启
DB::enableQueryLog()+DB::getQueryLog(),抓出真实执行的 SQL 再 EXPLAIN
WHERE 条件漏掉索引、一处 ORDER BY 没覆盖、一处 NULL 值干扰前缀匹配,InnoDB 就会默默把行锁升级成间隙锁甚至表锁——而协程并发一上来,这些“默默”的锁就会立刻撞成死锁。


















