SAVEPOINT用于局部回滚,仅在支持的数据库(如PostgreSQL、MySQL InnoDB、SQLite、SQL Server)中有效;设点、回滚、删点须按序显式操作,嵌套使用需防重名与隐式清除,ORM混合场景应统一管理。
SAVEPOINT 用在哪?不是所有数据库都支持
局部回滚只在需要绕过部分失败操作、又不想丢弃整个事务时才有意义。比如批量导入数据,中间某条记录违反唯一约束,你不想让前面成功的全白干——这时候 savepoint 才是解法。但得先确认你的数据库撑不撑得住:postgresql 和 mysql(innodb) 支持;sqlite 支持;sql server 支持;而 oracle 虽然语法允许,但实际行为和文档有偏差;mysql(myisam) 直接不认这个命令,执行就报错 error 1193 (hy000): unknown system variable 'savepoint'。
怎么设、怎么回、怎么删:三步不能乱序
SAVEPOINT 不是自动触发的,必须显式命名并声明;回滚也只作用于该点之后的操作;释放(RELEASE SAVEPOINT)不是必须,但不释放可能干扰后续同名定义或占用内部资源。
- 设点:
SAVEPOINT sp_user_insert;—— 名字别用数字开头、别含空格或特殊符号,否则某些驱动会解析失败 - 回滚:
ROLLBACK TO SAVEPOINT sp_user_insert;—— 注意不是ROLLBACK TO sp_user_insert(漏掉SAVEPOINT关键字在 PostgreSQL 里会报错,在 MySQL 里反而能蒙混过去,但属非标准写法) - 删点:
RELEASE SAVEPOINT sp_user_insert;—— 如果之后还要用同名点,必须先RELEASE,否则 MySQL 报ERROR 1305 (42000): SAVEPOINT sp_user_insert does not exist(其实是已存在,但它拒绝覆盖)
嵌套 SAVEPOINT 容易踩的坑:名字冲突和隐式释放
多个 SAVEPOINT 可以嵌套,但名字重复时行为因库而异。MySQL 会静默覆盖前一个同名点,PostgreSQL 则直接报错;更隐蔽的是:执行 COMMIT 后,所有 SAVEPOINT 都被自动清除,但不会报错——如果你在 COMMIT 后还试图 ROLLBACK TO,就会收到 ERROR: no such savepoint 这类提示,而不是“事务已结束”的明确信号。
- 避免重名:哪怕逻辑上分属不同模块,也建议加前缀,比如
sp_batch_001、sp_batch_002 - 别依赖自动清理:显式
RELEASE掉确定不用的点,尤其在长事务或循环插入中 - 不要在
COMMIT或ROLLBACK后继续操作SAVEPOINT—— 此时事务上下文已不存在,任何ROLLBACK TO都无效
应用层怎么配合?ORM 通常不帮你管 SAVEPOINT
像 Django 的 transaction.atomic()、SQLAlchemy 的 session.begin_nested() 确实封装了 SAVEPOINT,但它们只在“嵌套事务”语义下生效,且底层仍依赖数据库支持。一旦你在同一连接里混用原生 SQL 和 ORM 操作,就容易出现点位错位:比如 ORM 创建了 sp_1,你手写 SQL 又建了个 sp_2,然后回滚到 sp_1,结果 ORM 认为它自己的状态还是干净的,接着提交——实际可能把本该丢弃的数据落库了。
- 纯 ORM 场景:优先用
begin_nested()或atomic(using='default', savepoint=True),别自己拼SAVEPOINT字符串 - 混合场景:所有
SAVEPOINT操作尽量收口到同一层,比如统一由 DAO 层管理,应用层只调接口 - 调试时查当前点:MySQL 没有系统表可查,PostgreSQL 可查
pg_prepared_statements(不直接显示 savepoint),最靠谱的方式还是日志里打点记录SAVEPOINT和ROLLBACK TO的调用栈
SAVEPOINT 不是保险丝,它是手动开关——按下去之前得清楚哪段逻辑真能“撤回”,哪段其实已经发往下游服务、写进日志、触发了 webhook。稍不留神,回滚的只是数据库,不是现实。

















