返回博客

本地跑得好好的备份 SQL,上了 RDS 直接 1786 报错

发版前备份旧表是标准流程:CREATE TABLE xxx_bak AS SELECT * FROM xxx,本地 MySQL 跑顺溜了几个月。某次切流窗口,同样一条 SQL 打到阿里云 RDS,立刻报错,整个迁移脚本卡死:

ERROR 1786 (HY000): CREATE TABLE ... SELECT is forbidden
when @@GLOBAL.ENFORCE_GTID_CONSISTENCY = 1.

窗口期有限,慌了。

现象

错误只在云 RDS(阿里云 RDS MySQL 5.7 / AWS RDS MySQL 5.7+)上复现。本地用同版本 MySQL 执行毫无问题。日志只有这一行 1786,没有其他上下文。

常见触发场景:

  • 发版前备份缓存/临时表:CREATE TABLE cache_bak AS SELECT * FROM cache
  • 数据修复脚本保留原表快照
  • 运维手工拷贝表做线上数据比对

根因

MySQL GTID 模式要求每个事务只产生一个 GTIDCREATE TABLE ... SELECT 看起来是一条语句,实际在 binlog 层面会写入两个独立事件:一个 DDL(CREATE)+ 一个 DML(INSERT SELECT)。两个事件 → 两个 GTID → 破坏复制协议的一致性保证。

云厂商 RDS 的多可用区、只读实例全部依赖 GTID 进行主从同步,所以默认强制开启 enforce_gtid_consistency=ON不允许用户修改

可以用下面的 SQL 确认实例状态:

SHOW VARIABLES LIKE 'gtid%';
-- enforce_gtid_consistency = ON   → 禁用 CREATE AS SELECT
-- gtid_mode = ON / ON_PERMISSIVE  → GTID 已启用
坑:本地没问题不等于 RDS 没问题

本地 MySQL 默认 gtid_mode=OFFCREATE TABLE ... SELECT 完全合法。开发者往往在本地、CI 都跑通了才发现云 RDS 禁用这条语法——而此时往往已经在发版窗口内,压力最大。另外,试图临时关掉 GTID 也走不通:云 RDS 禁止 SET GLOBAL enforce_gtid_consistency=OFF,权限直接被拒。

正解

把一条混合语句拆成两条独立事务:

-- Step 1:只建结构(单一 DDL 事务,获得一个 GTID)
CREATE TABLE orders_bak LIKE orders;
 
-- Step 2:拷数据(独立 DML 事务,获得另一个 GTID)
INSERT INTO orders_bak SELECT * FROM orders;
 
-- Step 3(可选):加时间戳后缀标注备份时点
RENAME TABLE orders_bak TO orders_bak_20260417;

两条语句各自获得独立 GTID,RDS 不再拒绝,复制链路完全正常。

任何"备份当前表 + 清空/迁移"的脚本,可以套这个模板:

-- 1. GTID-safe 备份
CREATE TABLE {target}_bak LIKE {target};
INSERT INTO {target}_bak SELECT * FROM {target};
 
-- 2. 清空或结构变更
TRUNCATE TABLE {target};
-- 或:ALTER TABLE {target} ADD COLUMN ...
正解:CREATE LIKE + INSERT SELECT,两步拆分

CREATE LIKE 只复制表结构(纯 DDL),INSERT SELECT 只复制数据(纯 DML),两条语句独立事务、独立 GTID,完全符合 RDS GTID 一致性要求。RENAME 同样是纯 DDL,可以安全附加。

排查流程

写迁移/备份脚本
  ↓
SELECT @@GLOBAL.enforce_gtid_consistency;
  ↓
结果 = ON? ──是──→ 必须用 CREATE LIKE + INSERT SELECT
  │
  否(本地开发环境)
  ↓
可用 CREATE AS SELECT(但仍建议拆分,生产兼容且可读性更好)

一句话外卖

在云 RDS 上,CREATE TABLE ... SELECT 是永久禁区;养成「先 CREATE LIKE,再 INSERT SELECT」的习惯,本地和生产行为一致,再也不怕 1786。