嵌套查询本身不生成Undo,但会因长事务延长ReadView生命周期,锁死旧版本Undo记录,导致purge滞后和Undo膨胀;需通过INNODB_TRX与PROCESSLIST交叉定位trx_state='RUNNING'且trx_query IS NULL的空闲长事务。

嵌套查询本身不生成Undo,但大事务会让它变成“放大器”
嵌套查询(比如 SELECT * FROM t1 WHERE id IN (SELECT id FROM t2))执行时不会直接写Undo Log;真正触发Undo膨胀的,是它所处的**外层事务生命周期**。只要整个语句被包在一个未提交的事务里,InnoDB 就必须为该事务开始时刻起所有被修改/读取过的行保留旧版本——不管这些行是不是由嵌套子查询触达的。
为什么嵌套结构在长事务里特别危险?
嵌套查询容易掩盖事务实际持有时间,尤其当它和外部I/O、业务逻辑耦合时:
- 子查询返回结果后,应用没立刻提交,而是继续做 RPC 调用、JSON 解析、循环处理——此时事务已空闲,但Undo无法清理
- ORM 自动预加载(如 MyBatis 的
collection或 Django 的prefetch_related)在事务内展开多层 JOIN,把本可分步执行的读操作全塞进一个写事务 - 嵌套中含
ORDER BY或DISTINCT,导致优化器物化中间结果到磁盘临时表(Using temporary),而该临时表的创建/更新过程又产生新Undo记录
怎么确认是嵌套+长事务共同惹的祸?
别只看执行计划里的 Using temporary,要交叉验证事务状态:
- 运行
SELECT trx_id, trx_started, trx_state, trx_query FROM information_schema.INNODB_TRX ORDER BY trx_started LIMIT 5,找trx_state = 'RUNNING'且trx_query IS NULL的记录 - 查对应线程:
SELECT ID, USER, HOST, COMMAND, TIME, INFO FROM information_schema.PROCESSLIST WHERE ID = ?,如果INFO显示的是一个带子查询的SELECT,但TIME已持续数百秒,基本就是它 - 再看
SHOW ENGINE INNODB STATUS\G中的History list length:若 > 5000 且缓慢上涨,说明 purge 被卡住,而根源几乎一定是那个最早启动却没提交的嵌套查询事务
最易被忽略的细节:只读嵌套也阻塞Undo清理
很多人以为只有 UPDATE 或 DELETE 才写Undo,其实不然。哪怕整个嵌套查询全是 SELECT,只要它处于一个未提交的事务中,且该事务之前执行过写操作,InnoDB 就必须维持其 ReadView 不变——这就锁死了所有早于该 ReadView 的旧版本Undo记录,purge 线程动不了它们。这种“只读但不提交”的事务,在连接池配置不当或异常中断后残留最多,也最难被监控告警捕获。

















