Schema 变更的类型:新增列、删除列、修改列类型、重命名列、新增表、删除表
说实话,刚入行那会儿,我觉得改个表结构不就是几条 ALTER TABLE 的事吗?直到我在线上把一个 VARCHAR(50) 直接改成 VARCHAR(200),结果锁表锁了 15 分钟,业务直接报警。嗯,从那以后,我对 Schema 变更就多了一份敬畏。
这一章,咱们就把六种最常见的 Schema 变更类型掰开揉碎讲清楚。每种变更的风险在哪、怎么安全操作、有什么坑,我都会结合自己的实战经历来说。
核心观点:Schema 变更没有「小操作」。哪怕只是加一列,在高并发场景下都可能引发连锁反应。理解每种变更的本质,是保证数据一致性的第一步。
新增列:看似简单,实则暗藏玄机
加一列,听起来是最无害的操作了吧?其实不然。我见过太多人在生产环境直接 ALTER TABLE ADD COLUMN,结果导致整个表被锁住,读请求全部排队。
关键要看你的数据库版本和加列的方式。MySQL 5.6 之前,加列会重建整张表,锁表时间跟数据量成正比。5.6 之后有了 Instant ADD COLUMN,但只支持加在最后。
我的建议:如果表数据量超过 100 万行,优先用 ALTER TABLE ... ALGORITHM=INSTANT(MySQL 8.0+)或借助 pt-online-schema-change 这类工具。别图省事直接跑默认的 ALTER,你想想看,万一锁个半小时,老板不找你才怪。
-- 安全的加列方式(MySQL 8.0+)
ALTER TABLE users
ADD COLUMN last_login_ip VARCHAR(45)
AFTER email,
ALGORITHM=INSTANT,
LOCK=NONE;
-- 或者用 pt-osc 工具
pt-online-schema-change h=localhost,D=test,t=users \
--alter "ADD COLUMN last_login_ip VARCHAR(45) AFTER email" \
--execute
删除列:最容易被低估的风险操作
删列,说白了就是「开弓没有回头箭」。数据一旦被物理删除,恢复成本极高。我曾经有个同事,在测试环境跑顺了手,一条 DROP COLUMN 直接怼到生产库上,把用户表的 phone 字段给删了。还好有备份,但恢复也花了两个小时。
删列的风险点有三个:
- 数据永久丢失:除非有备份,否则无法恢复
- 依赖断裂:存储过程、视图、ORM 映射可能全部报错
- 锁表时间长:InnoDB 删列需要重建表空间
避坑指南:删列之前,先检查所有依赖。我习惯用 SELECT * FROM information_schema.columns WHERE table_name='xxx' 确认字段存在,再用 SHOW CREATE VIEW 检查视图依赖。删之前,先标记为「废弃」观察一周,确认没有程序再写入,再动手删除。
修改列类型:牵一发而动全身
改类型,比如把 INT 改成 BIGINT,或者把 VARCHAR(50) 扩到 VARCHAR(200)。你可能会想:「扩一下长度而已,能有什么问题?」
问题大了去了。MySQL 里,VARCHAR(50) 改成 VARCHAR(200) 虽然可以 in-place 完成,但如果是 VARCHAR 改成 TEXT,或者 INT 改成 DECIMAL,那就得重建整张表。数据量大的时候,磁盘 IO 直接打满。
| 变更类型 | 是否重建表 | 锁表风险 | 推荐工具 |
|---|---|---|---|
| VARCHAR(n) → VARCHAR(m) 且 m > n | 否(in-place) | 低 | 直接 ALTER |
| INT → BIGINT | 是 | 中 | pt-osc / gh-ost |
| VARCHAR → TEXT | 是 | 高 | gh-ost |
| FLOAT → DECIMAL | 是 | 高 | gh-ost + 数据校验 |
我个人习惯是:只要涉及重建表的类型变更,一律用 gh-ost 做在线变更。它通过 Binlog 同步数据,对主库几乎无锁影响。
重命名列:小心 ORM 的「幽灵引用」
重命名列,语法很简单:ALTER TABLE t RENAME COLUMN old_name TO new_name。但真正的坑不在 SQL 本身,而在代码里。
你想想看,你的 Java 代码里可能写着 resultSet.getString("old_name"),你的 Python ORM 模型里可能还挂着 old_name = Column(String(50))。一旦重命名,这些地方全部报错。
我的做法:先加一个新列,写双写逻辑(同时写旧列和新列),跑一段时间确认无误后,再删掉旧列。这个过程叫「渐进式重命名」,虽然慢,但安全。线上出过事故的人,都懂这个道理。
-- 渐进式重命名步骤
-- 1. 新增列
ALTER TABLE users ADD COLUMN nickname VARCHAR(100);
-- 2. 应用层双写:同时写入 name 和 nickname
-- 3. 数据回填
UPDATE users SET nickname = name WHERE nickname IS NULL;
-- 4. 切换应用层读取 nickname
-- 5. 确认无误后删除旧列
ALTER TABLE users DROP COLUMN name;
新增表:最安全的变更,但也有讲究
新增表,说实话,是六种变更里最安全的。没有存量数据,不影响现有业务。但安全不代表可以随便搞。
我见过最离谱的事:有人在生产库新建了一张表,没加索引就直接上线了。结果业务方一接入,全表扫描,把 CPU 打到了 100%。
- 命名规范:统一用小写 + 下划线,比如
user_login_log,别搞大小写混用 - 字符集:统一用
utf8mb4,别问为什么,问就是 emoji 兼容 - 主键:必须显式定义主键,最好是自增 BIGINT 或 UUID 有序存储
- 索引:根据查询模式提前建好索引,别等上线后再补
-- 一张规范的新表应该长这样
CREATE TABLE user_login_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
login_ip VARCHAR(45) NOT NULL COMMENT '支持IPv6',
login_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_login_time (login_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
删除表:最危险的操作,没有之一
删表,DROP TABLE 三个单词,打出来只需要两秒钟。但这两秒钟,可能让你丢掉几个 TB 的数据。
我曾经在凌晨三点被电话叫醒,原因是有人把 DROP TABLE orders 写到了定时任务里,结果跑错了环境。嗯,那哥们后来被调去写文档了。
血的教训:
- 删表之前,先
RENAME TABLE t TO t_bak_20250101,保留一周 - 生产环境禁止直接执行
DROP TABLE,必须走工单审批 - 删表前检查外键依赖,否则会报
Cannot delete or update a parent row - 如果表很大,用
TRUNCATE清数据比DROP重建更快,但无法回滚
-- 安全的删表流程
-- 第一步:重命名备份
RENAME TABLE orders TO orders_bak_20250101;
-- 第二步:观察一周,确认无报错
-- 第三步:确认备份存在后,再删除
DROP TABLE orders_bak_20250101;
小结一下
六种变更类型,风险等级完全不同。我按自己的经验排了个序:
| 风险等级 | 变更类型 | 核心关注点 |
|---|---|---|
| 🟢 低 | 新增表 | 命名规范、索引设计 |
| 🟢 低 | 新增列(末尾) | 锁表时间、默认值 |
| 🟡 中 | 重命名列 | ORM 映射、代码引用 |
| 🟡 中 | 修改列类型(in-place) | 数据截断、精度丢失 |
| 🔴 高 | 修改列类型(重建表) | 锁表、磁盘 IO、工具选择 |
| 🔴 高 | 删除列 | 数据恢复、依赖检查 |
| 💀 极高 | 删除表 | 备份、审批、外键 |