直接改字段类型常锁表或失败,因多数场景触发全表重建;数据截断风险隐蔽;INSTANT 算法不支持类型变更;大表耗时久,应采用建新表、导数据、原子切换的三步法。

ALTER COLUMN 直接改类型为什么常导致锁表或失败
MySQL 5.7+ 和 PostgreSQL 支持 ALTER COLUMN …… TYPE,但实际执行时,多数场景会触发全表重建——尤其是从 VARCHAR(255) 缩到 VARCHAR(50),或 TEXT 改成 VARCHAR。这时表会被加写锁,业务写入阻塞,超时失败很常见。
更隐蔽的问题是数据截断:没校验存量数据是否真能适配新类型,上线后 INSERT 或 UPDATE 突然报 data too long for column,但错误日志里根本看不出是字段收缩惹的祸。
- MySQL 的
ALGORITHM=INSTANT仅支持极少数变更(如加字段、改默认值),不支持类型变更 - PostgreSQL 的
USING表达式能转换数据,但若表达式有误(比如USING CAST(content AS VARCHAR(100))遇到超长值),整个ALTER直接回滚,且不提示哪行出问题 - 线上大表(千万级以上)执行一次带拷贝的
ALTER,可能耗时数小时,DBA 往往直接拒绝
用中间表迁移的关键三步:建、导、切
核心思路不是“改字段”,而是“换表”:新建兼容结构的表,把老数据按新规则清洗后导入,再原子切换表名。全程老表可读可写,业务无感。
以 MySQL 为例,把 users.name 从 VARCHAR(255) 安全缩为 VARCHAR(64):
CREATE TABLE users_new (id BIGINT PRIMARY KEY, name VARCHAR(64) NOT NULL DEFAULT '', created_at DATETIME );
注意:新表必须包含原表所有字段(含索引、约束),否则切换后查询 / 写入会出错;DEFAULT 值要显式声明,避免隐式空值引发应用异常。
- 导数据用
INSERT INTO users_new SELECT id, LEFT(name, 64), created_at FROM users;——LEFT()是安全截断,比直接CAST更可控 - 校验环节不能省:
SELECT COUNT(*) FROM users WHERE LENGTH(name) > 64;必须为 0,否则得先清理或告警 - 切换用
RENAME TABLE users TO users_old, users_new TO users;,原子操作,毫秒级完成
应用层怎么配合才不丢数据
切换瞬间,新旧表之间存在微小时间窗口(通常
上线前一周就要灰度开启双写逻辑:对 users 表的所有 INSERT/UPDATE,同步写一份到 users_new(或影子表)。不是简单复制 SQL,而是复用 ORM 的 save 方法,确保业务逻辑(如触发器、审计字段)一致生效。
- 双写失败要降级:写
users_new失败时,记录日志并继续写老表,** 不能抛异常中断主流程 ** - 切换后保留双写至少 24 小时,用脚本比对
users和users_old的增量差异,确认无遗漏 - 别忘了清理:确认无误后删
users_old,但索引、外键依赖需提前解绑,否则DROP TABLE会报错
PostgreSQL 的 COPY + 连续归档怎么绕过 WAL 压力
PG 大表迁移用 COPY 比 INSERT 快 5–10 倍,但默认模式下每行都记 WAL,磁盘和网络压力巨大。生产环境必须关掉部分日志。
在 users_new 上执行:SET LOCAL synchronous_commit = 'off'; + COPY users_new FROM …… WITH (FORMAT csv);。这能让 WAL 异步刷盘,提速明显,且不影响主库一致性(因为目标表尚未上线)。
- 切表前务必
VACUUM ANALYZE users_new;,否则首次查询可能走错执行计划 - 如果原表有
GENERATED列或BEFORE UPDATE触发器,中间表必须手动实现等效逻辑,COPY不会触发它们 - pg_dump 的
--inserts模式生成的是普通INSERT,不适合大数据量,别拿来当迁移脚本
最麻烦的从来不是语法,是存量数据里的脏值、应用缓存里的旧结构、以及 DBA 看到 RENAME TABLE 时那句“你确定?”——得拿校验报告和回滚方案去换他点个头。