InnoDB从不主动升级行锁为表锁,所谓“退化”实为WHERE条件未走索引→全表扫描→逐行加X锁+间隙锁→等效锁全表;根本原因是行锁对象是索引项,无索引则无法精确定位,只能遍历聚簇索引并锁定所有扫描记录及间隙。

MySQL InnoDB 从不主动“升级”行锁为表锁,所谓“退化为表锁”本质是:WHERE 条件没走索引 → 全表扫描 → 给每条聚簇索引记录加 X 锁 + Gap Lock → 效果等同于锁整张表。
为什么没走索引就会锁全表
InnoDB 的行锁不是加在数据行上,而是加在索引记录上。当 UPDATE、DELETE 或 SELECT ... FOR UPDATE 的 WHERE 条件无法命中任何索引时,优化器只能选择全表扫描(type=ALL),于是逐行遍历聚簇索引,并对每一条都加 X 锁。在默认的 REPEATABLE READ 隔离级别下,还会额外加 Gap Lock 和 Next-Key Lock,把所有扫描到的间隙也锁住。
- 常见触发写法:
UPDATE users SET status = 1 WHERE phone = 138(phone是VARCHAR,但传了数字,触发隐式转换丢索引) - 函数导致失效:
WHERE SUBSTRING(name, 1, 2) = 'Li'、WHERE DATE(created_at) = '2026-04-01' - OR 导致索引失效:
WHERE a = 1 OR b = 2(a、b各有单列索引,但优化器可能放弃全部) - 字符集不匹配:
utf8mb4列与utf8字符串比较,索引失效
如何快速定位正在退化的 SQL
别依赖 SHOW ENGINE INNODB STATUS\G —— 它只显示最近死锁或活跃事务的锁信息,漏掉静默持有者。真正可靠的是 performance_schema.data_locks(MySQL 8.0+ 唯一权威来源)。
- 查慢日志 + 索引未使用日志:
SET GLOBAL slow_query_log = ON; SET GLOBAL log_queries_not_using_indexes = ON; - 找高影响写语句:
SELECT * FROM performance_schema.events_statements_history WHERE sql_text LIKE '%UPDATE%' AND rows_affected > 1000; - 验证执行计划:
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE created_at > '2026-04-01' FOR UPDATE;,重点看key是否为NULL、rows是否远超实际匹配数、filtered是否极低(如
加索引就能解决?小心这些副作用
补索引是最直接解法,但不是万能膏药。加错索引反而让问题更隐蔽,甚至引入新瓶颈。
- 单列索引对
WHERE a = 1 AND b = 2效果有限,应建联合索引(a, b),顺序按字段选择性从高到低排 - 写多读少的表(如日志表),高频
INSERT/UPDATE下,过多索引会拖慢性能,锁竞争可能从表级转为索引树 latch 竞争 -
LIKE '%abc'这种前导通配无法走 B+ 树索引,加索引无效;得换FULLTEXT或倒排结构 - 允许
NULL的普通索引字段,在RR下会把所有NULL所在间隙纳入 Gap Lock 范围——这部分常被忽略,却是并发冲突高发区
DDL 操作和元数据锁(MDL)也会导致表级阻塞
这不是“锁升级”,而是另一类独立机制:元数据锁(MDL)。它和行锁无关,但效果类似——阻塞所有后续 DML。
-
ALTER TABLE、DROP TABLE、TRUNCATE TABLE会自动加 MDL 写锁,阻塞所有对该表的读写 - 长事务中一个慢
SELECT持有 MDL 读锁未结束,此时ALTER TABLE会被卡住,而后续所有试图访问该表的事务也会排队等待 MDL,形成锁链式阻塞 - DDL 不走
ALGORITHM=INPLACE(如旧版本 MySQL 添加列未指定算法)时,会强制获取全表 X 锁
真正麻烦的从来不是“要不要加索引”,而是“加在哪、怎么验证它真起作用了”。哪怕你加了索引,也要跑 EXPLAIN 看 key 和 rows,再用 performance_schema.data_locks 查实际加锁范围——否则你以为锁住了 1 行,其实锁了 10 万行。











