是,因ddl会隐式提交事务,导致此前dml无法回滚。mysql规定所有ddl语句(如alter table)执行时强制提交当前事务,无论autocommit设置如何,且savepoint失效,故事务中混用ddl会使rollback对之前操作无效。

事务中执行 ALTER TABLE 为什么会丢数据
因为 MySQL 遇到 ALTER TABLE、DROP TABLE、CREATE INDEX 等 DDL 语句时,会**立即隐式提交当前事务**,之后开启新事务。你前面的 INSERT 或 UPDATE 已经落地,ROLLBACK 对它们完全无效。
常见错误现象:
- 事务块里先
INSERT INTO log VALUES (...),再ALTER TABLE user ADD COLUMN phone VARCHAR(20),最后ROLLBACK—— 日志表里的记录还在 - 用 Navicat 或 DBeaver 执行多条语句时勾选了“自动提交”,但没注意某条是 DDL,结果事务被悄悄切开
- 存储过程中动态拼接并执行
SET @sql = CONCAT('ALTER TABLE ...'); PREPARE stmt FROM @sql; EXECUTE stmt;,同样触发隐式提交
如何检测代码里是否混入了隐式提交操作
不能只靠肉眼扫 SQL,要从运行时状态确认事务是否还“活着”。关键指标是 SELECT @@autocommit, @@in_transaction;,但更实用的是在关键位置加检查:
- 执行 DDL 前:查
SELECT TRX_ID, TRX_STATE FROM information_schema.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID();,如果返回一行且TRX_STATE = 'RUNNING',说明当前有活跃事务 - 执行 DDL 后:立刻再查一遍
@@in_transaction,值会变成0(MySQL 8.0+)或SELECT @@tx_isolation不再能反映事务上下文 - 应用层日志中,在
conn.commit()前打点记录conn.get_autocommit()和conn.in_transaction()(PyMySQL / mysql-connector-python 支持)
DDL 必须进事务怎么办:替代方案与取舍
没有真正的“DDL 在事务中”,只有绕过或模拟。核心原则是:把 DDL 和业务逻辑解耦,接受它不可回滚的事实,再设计补偿路径。
- 用
pt-online-schema-change:它不锁主表,通过影子表 + 触发器同步数据,失败时自动清理临时对象,但原表上的 DML 不受影响——适合线上大表变更 - 拆成两阶段:先
ALTER TABLE(确保成功),再执行业务 DML;若 DML 失败,需人工或脚本补偿(如ADD COLUMN可逆,DROP COLUMN几乎无法回退) - 改用
CREATE TABLE AS SELECT+RENAME TABLE:新建结构表,导入数据,原子切换表名;但期间需停写或用触发器双写,复杂度高 - 开发期规避:在 CI 流程中用
mysqld --sql-mode=STRICT_TRANS_TABLES+ 静态 SQL 检查工具拦截 DDL 出现在事务块内
为什么 SET autocommit = 0 也救不了 DDL 事务
SET autocommit = 0 只影响后续 DML 的默认行为,对 DDL 完全无效。MySQL 内核层面规定 DDL 是“自包含事务单元”,无论会话处于什么模式,只要解析到 DDL 语法,就强制提交当前事务、执行 DDL、再重置事务状态。
容易被忽略的细节:
-
START TRANSACTION和BEGIN效果相同,但都不能包裹 DDL -
SAVEPOINT在 DDL 前设也没用——DDL 执行后,所有 savepoint 全部失效 - MyISAM 表上执行
ALTER TABLE虽不报错,但依然会隐式提交(哪怕引擎本身不支持事务) - MySQL 8.0 引入的原子 DDL(如
ALTER TABLE ... ALGORITHM=INSTANT)仍是原子操作,不是可回滚事务
真正要警惕的,不是“怎么让 DDL 进事务”,而是“有没有在不知不觉中把 DDL 当成了普通语句来编排逻辑”。











