AutoMigrate不能用于大表结构变更,因其对DDL操作是原子的,MySQL/PostgreSQL在ALTER TABLE时易锁表,超千万行可能阻塞写入数分钟;它不支持分批、不控制ALGORITHM/LOCK、无超时机制,且静默失败。

大表迁移时直接 AutoMigrate 会锁表、卡住线上服务,必须分批次更新结构 —— 但 GORM 本身不提供“分批改表”能力,得靠手动拆解 + 原生 SQL 配合事务控制。
为什么 AutoMigrate 不能用于大表结构变更
AutoMigrate 对 PostgreSQL/MySQL 的 DDL 操作是原子的:新增字段、改类型、加索引等操作在底层会触发 ALTER TABLE。而 MySQL 5.7+ 虽支持 ALGORITHM=INPLACE,但像 TEXT → JSON、NOT NULL 加默认值这类操作仍需重建表;PostgreSQL 则多数 ALTER COLUMN TYPE 会锁全表。一旦表行数超千万,AutoMigrate 可能阻塞写入数分钟甚至更久。
常见错误现象:
- 执行
db.AutoMigrate(&User{})后 HTTP 接口大面积超时 - pg_stat_activity 显示大量
ALTER TABLE ... WAITING状态 - 日志里没报错,但表结构没变 —— 其实是被锁住,
AutoMigrate静默重试或跳过
分批次添加字段的可靠写法(以 MySQL 为例)
核心思路:先加可空字段(无锁),再用 FindInBatches 分批写入默认值,最后改 NOT NULL(此时数据已就位,只改元数据)。
- 第一步:原生 SQL 加字段(跳过 GORM,避免被 AutoMigrate 干扰)
db.Exec("ALTER TABLE users ADD COLUMN vip_level TINYINT NULL DEFAULT NULL AFTER age") - 第二步:分批填充默认值(每批 1000 行,带事务)
var ids []uint db.Model(&User{}).Where("vip_level IS NULL").Select("id").FindInBatches(&ids, 1000, func(tx *gorm.DB, batch int) error { return tx.Model(&User{}).Where("id IN ?", ids).Updates(map[string]interface{}{"vip_level": 0}).Error }) - 第三步:安全改非空(此时无 NULL 值,仅元数据变更)
db.Exec("ALTER TABLE users MODIFY COLUMN vip_level TINYINT NOT NULL DEFAULT 0")
分批次修改字段类型的注意事项
类型变更无法靠 FindInBatches 搞定,必须用在线 DDL 工具或数据库原生命令。GORM 不参与这一步,但你要控制节奏:
- MySQL:优先用
pt-online-schema-change,命令中指定--chunk-size=1000和--max-load="Threads_running=25" - PostgreSQL:用
CONCURRENTLY创建新索引,但ALTER COLUMN TYPE仍需锁表 —— 必须选低峰期,且提前在测试库验证耗时 - 千万别在 GORM 迁移里写
gorm:"type:jsonb"然后跑AutoMigrate:它不会分批,也不会告诉你正在锁表
索引和外键必须单独处理
索引创建同样会锁表(尤其 CREATE INDEX),而 GORM 的 AutoMigrate 默认不带 CONCURRENTLY 或 ALGORITHM=INPLACE 参数:
- PostgreSQL 添加唯一索引务必加
CONCURRENTLY:db.Exec("CREATE UNIQUE INDEX CONCURRENTLY idx_users_email ON users(email)") - 外键建议延迟启用:先建字段,等数据对齐后再加约束,避免迁移中途因脏数据失败
- 复合索引命名要统一,否则
AutoMigrate可能反复删建 —— 检查pg_indexes或SHOW INDEXES确认是否已存在
真正麻烦的从来不是“怎么写”,而是“什么时候停写、怎么校验中间状态、失败后如何回退”。分批改表没有银弹,只有把每一步的锁范围、耗时、回滚路径写进运维 checklist 里。











