MySQL DELETE无索引WHERE会全表扫描并加锁,导致锁升级为表锁;应确保WHERE条件走索引、分批删除时用ORDER BY锚定单调字段、每批后显式COMMIT。

WHERE条件没索引,删多少行都等于锁全表
MySQL 的 DELETE 不是“只锁要删的行”,而是对**所有扫描到的行(含间隙)加 X 锁 + GAP 锁**。如果 WHERE 字段没索引,InnoDB 只能全表扫描——几百万行挨个判断、逐行加锁、写 undo/redo,锁数量爆炸后直接升级为表锁。
验证方法:用 EXPLAIN SELECT * 替代 EXPLAIN DELETE(MySQL 不支持后者),确认 type 是 range 或 ref,不是 ALL。
- 别写
WHERE DATE(created_at) = '2024-01-01'——函数导致索引失效,改用created_at >= '2024-01-01' AND created_at - 别传字符串给 INT 字段,比如
WHERE user_id = '123',隐式转换会让索引失效 - 联合条件优先建复合索引,例如删
status = 'pending' AND id > 100000,索引应为(status, id),不是(id, status)
分批删不加 ORDER BY,大概率漏删或重复删
只写 DELETE ... LIMIT 1000 极其危险:InnoDB 不保证无 ORDER BY 时的物理顺序,优化器可能每次选不同行;并发写入下,同一批可能反复删同一组数据,跳过另一些——循环永远停不下来。
正确做法是锚定一个单调字段(如主键 id),用边界值控制扫描起点和方向:
DELETE FROM logs WHERE id > @last_id AND status = 'expired' ORDER BY id LIMIT 2000-
ORDER BY字段必须是索引列(最好是主键),否则触发filesort,更慢更耗资源 - 每批执行完查
ROW_COUNT(),返回 0 就停;别硬设循环次数
事务不显式提交,锁就一直挂着
锁释放时机取决于事务结束,不是语句执行完。BEGIN 后套个 WHILE 处理 10 万行,最后才 COMMIT,是最危险的反模式——锁、undo log、MVCC 版本链全卡住,其他查询全被堵死。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 每批
DELETE/UPDATE后必须显式COMMIT,或确保autocommit = 1 - 单次处理建议控制在 500–5000 行之间:太小事务开销占比高;太大仍可能触发锁升级或日志写满
- 把 HTTP 调用、JSON 解析、文件读写等移出事务块;日志记录、消息推送放到
COMMIT之后再做
最容易被忽略的隐形锁源:幽灵事务和其他会话
你加了索引、分了批、限了速,还是卡——大概率不是 SQL 写得不对,而是别的进程在“偷偷锁着表”。这类问题不报错,但会让所有优化失效。
排查要点:
-
autocommit = 0但忘记手动COMMIT:锁会一直挂着,直到会话断开 - 优化器因统计信息过期选了全表扫描:执行前先
EXPLAIN确认是否走了索引 - 从库上直接删:主从延迟导致同一行在主库已删、从库还没同步,触发唯一键冲突或主键缺失报错
- 用
SELECT ... FOR UPDATE加锁太早,然后做耗时操作——锁挂着几十秒不动
真正麻烦的不是怎么写 SQL,而是判断哪条路径适合你的表结构、索引情况和业务容忍度——比如 ORDER BY id LIMIT 要求 id 有索引,而 BETWEEN 方式要求 id 和时间字段强相关。










