1. 修改字段长度:ALTER TABLE MODIFY 的正确姿势
1.1 先说语法:MODIFY 和 CHANGE 到底怎么选
在 MySQL 里修改字段,最常用的两条命令就是ALTER TABLE ... MODIFY COLUMN和ALTER TABLE ... CHANGE COLUMN。很多新手写表结构变更的时候,喜欢随手敲MODIFY,但一问区别就含糊了。其实两者的核心差异就一句话:MODIFY只能改字段的定义(类型、长度、默认值、注释等),不能改字段名;而CHANGE既能改字段名,也能改字段的定义,相当于MODIFY的完全版。
实际开发中我一般遵循这样一个原则:只要不改字段名,一律用MODIFY,因为语法更简单、可读性更好;只有需要重命名字段(比如user_name要改成username)或者做比较复杂的结构迁移时才用CHANGE。举个典型例子,假如要把user_profile表里的nickname字段从VARCHAR(50)改成VARCHAR(100),直接写:
ALTER TABLE user_profile MODIFY COLUMN nickname VARCHAR(100) NOT NULL DEFAULT '' COMMENT '用户昵称';注意,MySQL 的MODIFY COLUMN是整段替换字段定义,不是“只修改你列出来的属性”。也就是说,你写MODIFY COLUMN nickname VARCHAR(100),如果你没有带上NOT NULL、DEFAULT、COMMENT,那么这些属性会被重置成默认值。这是一个非常容易踩的坑,尤其是老表里字段可能带着一堆约束和注释,一改完才发现注释没了、默认值丢了。
1.2 字段长度扩展实操:VARCHAR、INT、BIGINT 实例
业务发展过程中最常见的需求就是“这个字段不够长了”。典型场景是手机号从 11 位变成带区号、订单号规则升级、或者是业务方要求支持更长的备注文本。我这里分三种常见类型说下实操。
第一种,扩展 VARCHAR 的长度。比如goods_sku表的sku_code原来定义为VARCHAR(32),现在要支持最长 64 位的 SKU 编码:
ALTER TABLE goods_sku MODIFY COLUMN sku_code VARCHAR(64) NOT NULL DEFAULT '' COMMENT '商品SKU编码';执行成功后可以用SHOW CREATE TABLE goods_sku;确认一下变更结果。这个操作在数据量不大的表上几乎是毫秒级的,但要注意:扩展 VARCHAR 长度时,如果新长度使得行记录的最大可能大小超过 65535 字节,就会直接报错,因为 MySQL 的 InnoDB 行大小上限是 65535 字节(注意是所有变长字段累加)。比如你有一张表里有 10 个VARCHAR(1000)字段,还想再扩展一个到 2000,基本就撞上限了,得改设计。
第二种,扩展整数类型的范围。比如user_id原来用INT(11),现在注册量级大了或者要做数据仓库的 ID 统一规范,需要升级成BIGINT:
ALTER TABLE user_account MODIFY COLUMN user_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID';这里有个细节:BIGINT(20)里的20不是存储长度,而是显示宽度。MySQL 8.0 里整型的显示宽度已经废弃了,所以你写成BIGINT完全没问题。在 MySQL 5.7 及以下,BIGINT(20)这种写法还能见到,但不影响实际存储范围(BIGINT的范围就是-2^63到2^63-1)。实际操作中,如果这个字段是主键或者有外键引用,要改类型就得先处理关联关系,这个我会在后面单独讲。
第三种,TEXT 家族的调整。MySQL 的 TEXT 字段分了TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT四个档位,分别对应 255 字节、64KB、16MB、4GB。把TEXT升级为MEDIUMTEXT或者反过来,语法也是用MODIFY:
ALTER TABLE article_content MODIFY COLUMN content MEDIUMTEXT COMMENT '文章正文';不过字段长度扩展在大表上的成本不能忽略。ALTER TABLE 在很多情况下会触发表的重建,即使你只改一个 VARCHAR 的长度,InnoDB 也可能把整张表的行数据重新拷贝一遍,这个过程会锁表、占用大量 IO 和磁盘空间。我在 4.2 节会专门讲大表操作怎么规避风险。
1.3 收缩字段长度的风险窗口
如果说扩展长度是“顺手就做了”,那收缩字段长度就真的需要谨慎再谨慎。还是拿nickname举例,现在要把VARCHAR(100)改回VARCHAR(50):
ALTER TABLE user_profile MODIFY COLUMN nickname VARCHAR(50) NOT NULL DEFAULT '' COMMENT '用户昵称';如果表里已经存在长度为 80 的昵称,这条语句会直接失败,报Data too long for column 'nickname'。这算是比较友好的情况,MySQL 会前置检查并拒绝执行。但还有两种更隐蔽的风险:
- 多字节字符集下的长度理解偏差。
VARCHAR(50)在 utf8mb4 字符集下表示的是 50 个字符,不是 50 个字节,但因为一个中文字符最多占 4 个字节,一行里如果有多个中文字段,收缩后可能触发行大小超限。 - 收缩后写入报错,历史数据读不出来。MySQL 在
ALTER TABLE的时候会做全表扫描检查,但在某些特殊情况下(比如 SQL_MODE 没开启 STRICT,或者用了在线 DDL 的特定算法),可能不会严格校验已存在的数据。收缩后应用程序写入超长数据时开始报错,排查起来很麻烦。
所以我的习惯是,任何收缩字段长度的操作前,先跑一条 SQL 查一下当前字段的最大长度:
SELECT MAX(CHAR_LENGTH(nickname)) AS max_len FROM user_profile;CHAR_LENGTH是按字符计算长度,而不是LENGTH(后者按字节计算)。确认最大长度小于目标长度后,再决定是否执行。如果确定业务上可以截断超长数据,那就先备份、再 UPDATE 清洗、最后 ALTER,分三步走。
2. 修改字段类型的核心细节与转换陷阱
2.1 类型转换:什么时候安全,什么时候会翻车
修改字段类型比修改长度要复杂得多,因为类型决定了数据的存储格式和解释方式。比如把INT改成VARCHAR,MySQL 需要把每个数值转成字符串;把VARCHAR改成INT,MySQL 会尝试把每个字符串解析成数值。这个“解析”的过程就是最容易出问题的地方。
我先给一个安全等级的分类,方便你快速判断当前的操作风险:
| 转换方向 | 安全等级 | 风险说明 |
|---|---|---|
INT→BIGINT | 安全 | 存储范围扩大,数值不会溢出,推荐直接 ALTER |
VARCHAR→TEXT | 安全 | 存储方式有变化,但值本身不变 |
VARCHAR长度调大 | 基本安全 | 风险主要来自行大小上限,注意累加 |
VARCHAR→INT | 高风险 | 非纯数字字符串会导致转换失败或截断为 0 |
DECIMAL→FLOAT | 高风险 | 精度丢失,小数位四舍五入可能不一致 |
TEXT→VARCHAR | 高风险 | 超长内容会被截断或报错,且 TEXT 到 VARCHAR 通常涉及全表复制 |
INT→VARCHAR | 中风险 | 本身安全,但要考虑后续查询是否还有隐式转换问题 |
DATETIME→TIMESTAMP | 中风险 | 范围不同,超出 2038 年的数据直接报错 |
这里重点讲一下VARCHAR转INT的坑。MySQL 在做字符串转数值时,遵循“尽量解析”的原则:如果字符串以数字开头,它会把前面的数字截出来转成数值,比如'123abc'转成123;如果字符串不以数字开头,转出来就是0。所以当你把code字段从VARCHAR(20)改成INT,如果历史数据里有'A001'这样的值,ALTER 之后它就变成了0,这个数据就废了。
我之前接手过一个订单系统,业务方反馈历史订单的“来源渠道”全部变成了 0,一查就是某次迁移把渠道编码字段从VARCHAR改成了INT,所有非纯数字的渠道码全被转成了 0。这种问题 SQL 层面很难在 ALTER 之前发现,因为你不会刻意去跑一条SELECT code + 0 FROM table来看转换结果。所以我的建议是,在做任何可能改变数据语义的类型转换之前,先把目标类型映射后的结果 SELECT 出来看一遍,确认无误后再改表结构。
2.2 常用类型转换的实测对比表
为了让大家对转换成本有个直观感受,我整理了一份常见转换操作在 100 万行数据表上的实测参考(数据会因机器配置、索引数量、存储引擎而有差异,仅供参考):
| 原类型 | 目标类型 | 耗时参考 | 磁盘占用变化 | 注意事项 |
|---|---|---|---|---|
VARCHAR(32) | VARCHAR(64) | 1~3 秒 | 略微增加 | 小表安全,大表可能触发 COPY |
INT | BIGINT | 2~5 秒 | 行大小增加 | 会重建表 |
VARCHAR(20) | INT | 3~8 秒 | 减少 | 非数字字符串会被转成 0 |
DATETIME | TIMESTAMP | 3~6 秒 | 不变 | 注意 2038 年问题 |
TEXT | MEDIUMTEXT | 5~15 秒 | 增加 | 需要 COPY 全表 |
VARCHAR(100) | TEXT | 5~10 秒 | 略增 | 可能需要更多磁盘空间完成重建 |
CHAR(10) | VARCHAR(10) | 1~3 秒 | 减少 | 去除尾部空格可能影响业务 |
注意,上面这个表只是一个“感觉参考”,不要把它当成性能基准。真正决定 ALTER 耗时的因素很多:表的总行数、每行平均大小、是否有二级索引、缓冲池大小、磁盘类型(机械盘还是 SSD)、是否开启了innodb_online_alter_log_max_size等等。在实际项目中,如果一张表有 5000 万行,且带了好几个二级索引,就算只是改一个INT到BIGINT,也可能要跑上几分钟甚至更久。
2.3 字符串和数字互转的注意事项
字符串和数字互转是日常开发里很常见的需求,但有一个隐性问题特别容易被忽略:隐式类型转换对索引的影响。
这句话什么意思呢?假设有一张表user_order,order_no字段是VARCHAR(64),并且建了普通索引idx_order_no。现在你要把order_no改成BIGINT类型,但如果业务代码里还在用字符串方式查询:
SELECT * FROM user_order WHERE order_no = '202401011234567890';在order_no是VARCHAR的情况下,这个查询是可以正常走索引的;改成BIGINT之后,MySQL 会把字符串'202401011234567890'隐式转换成数字再比较,这里其实也不会导致索引失效。真正危险的是反过来:字段是VARCHAR,查询条件是数字,比如WHERE order_no = 202401011234567890,MySQL 会把字段值先转成数字再比较,导致索引失效,全表扫描就来了。
所以我的建议是:类型转换之前先梳理所有关联查询 SQL,确认代码层面的数据类型一致再动手。尤其是 Java 的 MyBatis、Python 的 ORM 这些框架,如果映射类型没跟着改,很容易出现隐式转换问题。别问我为什么知道,我在生产环境踩过不止一次了。
另外,字符串转数字时还要特别注意精度损失。比如VARCHAR里存了一个'12345678901234567890'(20 位数字),这个值已经超过了BIGINT的最大值9223372036854775807(19 位),如果直接转成BIGINT,MySQL 会报错或者变成最大值溢出。这种超长数字的场景,通常应该用DECIMAL或者保持VARCHAR不动,而不是硬转BIGINT。
3. 在线 DDL 与锁表:大表上改字段的真实成本
3.1 MySQL 8.0 的 INSTANT / INPLACE / COPY 机制
聊到 ALTER TABLE,就绕不开 MySQL 的 DDL 执行机制。很多从 Oracle 转过来的同学习惯性地以为 ALTER 是一个“快操作”,但在 InnoDB 引擎下,不同的 DDL 会有完全不同的成本和锁行为。MySQL 8.0 的官方文档把 ALTER TABLE 的算法分成三类:INSTANT、INPLACE、COPY。
- INSTANT:只修改数据字典中的元数据,不涉及数据行变动,速度极快。MySQL 8.0.12 之后,增加列的操作(在特定条件下)可以做到 INSTANT,但修改字段长度或类型大多不支持 INSTANT。
- INPLACE:不需要复制整表数据,但可能需要重建表或重建索引。比如
ALTER TABLE ... MODIFY COLUMN ... VARCHAR(50)到VARCHAR(100),在 utf8mb4 字符集下,如果新长度没有导致行存储格式变化,可能走 INPLACE;但很多情况下,VARCHAR 长度跨越了 255 字节边界,或者从VARCHAR改成TEXT,就会触发 COPY。 - COPY:MySQL 会创建一张临时表,按新结构逐行拷贝数据,然后重建索引,最后用临时表替换原表。这个过程会占用额外的磁盘空间(约等于原表大小 + 索引大小),且在拷贝期间大部分操作会被锁住,只允许部分并发读写(取决于
LOCK参数)。
实际上在真实环境里,我比较少手动指定ALGORITHM和LOCK,因为 MySQL 会自动选择最优方案。但有一个参数非常值得关注:lock_wait_timeout。在 DDL 执行期间,如果有长事务持有了元数据锁(MDL),你的 ALTER 语句会卡在Waiting for table metadata lock状态,而且这个等待是没有超时上限的(受lock_wait_timeout控制,默认 31536000 秒,也就是一年)。这个问题非常常见,尤其是在业务高峰期执行 ALTER,很容易被一个慢查询卡住几个小时。
3.2 大表操作时的实践方案:避开高峰 + 分批处理
如果你要修改的是一张千万级甚至亿级的大表,我的建议是阶梯式走方案:
- 首先,评估是否真的需要直接改表。如果只是某个字段的长度不够,而业务上又没有特别强烈的即时需求,可以考虑先改应用侧逻辑,用新字段代替旧字段,再通过数据订正逐步切换。这种“代码先行、表结构后改”的模式在大厂很常见,不过对中小团队来说有点过度设计了。
- 其次,选择低峰期操作。凌晨 1 点到 5 点通常是流量的最低点,在这个窗口执行 ALTER 能把对业务的影响降到最低。操作前提前发变更通知,操作过程中盯紧监控。
- 再次,善用在线变更工具。MySQL 官方生态里最常用的两个工具是
pt-online-schema-change(Percona Toolkit)和gh-ost(GitHub 开源)。它们的原理都是创建一张影子表,然后通过触发器或 binlog 同步增量数据,最后原子切换表名,实现在线变更。用这类工具可以在业务基本无感知的情况下完成字段类型修改。
gh-ost的大致用法是:
gh-ost \ --host=127.0.0.1 \ --user='dba' \ --password='your_password' \ --database='app_db' \ --table='user_order' \ --alter='MODIFY COLUMN order_no BIGINT NOT NULL COMMENT "订单号"' \ --execute需要注意的是,gh-ost和pt-osc也不是万能的。表里如果没有主键或者唯一键,这两个工具会拒绝执行;表上有外键或触发器也可能导致同步异常。所以在使用工具之前,先花点时间扫一下表结构里的“历史包袱”,比直接跑命令要稳得多。
4. 实操过程与核心环节实现:完整流程拆解
4.1 一次标准的字段修改完整流程
说了这么多理论,我拿一个真实的场景把整个流程串起来。假设现在有一个电商项目,订单表trade_order如下:
CREATE TABLE `trade_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号', `buyer_note` varchar(100) DEFAULT NULL COMMENT '买家备注', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0待支付 1已支付 2已取消', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';业务方提了两个需求:第一,order_no以后要支持最大 64 位,而且历史数据里有几个已经超过 32 位了;第二,buyer_note的 100 字不够用,需要提升到 500 字。
正常情况下,可以直接执行两条 ALTER:
ALTER TABLE trade_order MODIFY COLUMN order_no VARCHAR(64) NOT NULL COMMENT '业务订单号'; ALTER TABLE trade_order MODIFY COLUMN buyer_note VARCHAR(500) DEFAULT NULL COMMENT '买家备注';但按照我前面的经验,我不会直接跑。我自己的标准流程分四步:
第一步,先核对字符集。表是 utf8mb4,那VARCHAR(500)最大可能占 2000 字节。我需要确认这一行所有变长字段加起来没有超过 65535 字节的限制,否则执行会失败。可以用 information_schema 查一下:
SELECT TABLE_NAME, CHARACTER_SET_NAME, COALESCE(SUM(CHARACTER_MAXIMUM_LENGTH * 4), 0) AS max_bytes_estimate FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'app_db' AND TABLE_NAME = 'trade_order' GROUP BY TABLE_NAME, CHARACTER_SET_NAME;第二步,检查历史数据长度。确认order_no现有最大长度、buyer_note现有最大长度,防止 ALTER 后报Data too long:
SELECT MAX(CHAR_LENGTH(order_no)) AS max_order_no_len, MAX(CHAR_LENGTH(buyer_note)) AS max_note_len FROM trade_order;第三步,预估影响。看一下表有多少行、数据量多大:
SELECT COUNT(*) FROM trade_order;如果行数很小(比如几十万以内),我直接执行 ALTER 就行;如果到了千万级以上,我会走pt-osc或gh-ost。这里我补充一个信息:VARCHAR(32)到VARCHAR(64)在 utf8mb4 下,每行最大变化是增加了 128 字节,InnoDB 通常会在原地更新记录头字段,不一定会触发整表 COPY,但实际行为取决于版本和参数,不能想当然。
第四步,执行变更并验证。执行后马上查看表结构和数据完整性:
SHOW CREATE TABLE trade_order; SELECT COUNT(*) AS total, COUNT(DISTINCT order_no) AS uniq_order_no FROM trade_order;字段长度和类型修改后,最怕的就是数据不一致(尤其是唯一索引字段),所以查一下总数和去重数是否和变更前一致,能快速确认有没有数据丢失或重复。
4.2 修改主键字段类型的特殊处理
主键字段的类型修改比普通字段要麻烦得多。因为主键通常带有AUTO_INCREMENT,而且可能有其他表通过外键引用它。直接把INT改成BIGINT,如果用MODIFY COLUMN强制覆盖 AUTO_INCREMENT 属性,步骤顺序不对会报错。
正确姿势是分两步走。先改类型和属性,再恢复自增:
ALTER TABLE trade_order MODIFY COLUMN id BIGINT NOT NULL COMMENT '主键ID'; ALTER TABLE trade_order MODIFY COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID';如果表上有外键约束,那么外键引用的列类型也必须一致,否则 MySQL 会报Cannot change column 'id': used in a foreign key constraint。这时候必须先删除外键约束,改完主表和子表字段后,再加回外键。这个流程里,如果外键关联的表还不在你的掌控范围内,必须提前协调好负责人,不留沟通死角。
主键修改还有一个更隐蔽的坑:如果主键被用作聚簇索引,修改主键的值范围可能导致 B+ 树重新组织。虽然对 InnoDB 来说主键类型从 INT 改到 BIGINT 只是记录大小变大,但如果你改的是主键的实际值(比如把 ID 从数值型改成字符串型),那就要重建整个聚簇索引,这个成本是非常大的,甚至比全表 COPY 还高。所以改主键类型前,要评估索引重建的代价。
4.3 修改带索引字段时的重建逻辑
字段类型或长度变化会直接影响索引。普通索引和唯一索引的条目都保存了字段值,所以字段类型一变,索引必须重建。这个重建过程在数据量大时很耗时,而且会占用大量磁盘空间。
有个经验值得分享:对带有二级索引的表做VARCHAR(50)到VARCHAR(100)这样的长度扩展时,MySQL 很多时候会直接走 COPY 算法,把整张表和所有索引都重建一遍。所以在一条 ALTER 语句里尽量合并多个字段的变更,比一条一条地执行高效得多。比如:
ALTER TABLE trade_order MODIFY COLUMN order_no VARCHAR(64) NOT NULL COMMENT '业务订单号', MODIFY COLUMN buyer_note VARCHAR(500) DEFAULT NULL COMMENT '买家备注', MODIFY COLUMN status TINYINT NOT NULL DEFAULT '0' COMMENT '状态';一次 ALTER 重建一次表,三条 ALTER 就要重建三次。省下来的不是一条命令的时间,而是整表复制和索引重建的成倍开销。这也是为什么我建议把所有表结构变更都攒成一次性提交,而不是“想到哪改到哪”。
5. 常见问题与排查技巧实录
5.1 高频报错速查表
我在各种项目里总结了一份 MySQL 修改字段长度和类型时最常遇到的报错清单,直接贴在这里,大家遇到问题可以对号入座:
| 报错信息 | 原因分析 | 解决方案 |
|---|---|---|
Data too long for column 'xxx' | 已有数据超出新长度限制 | 先用 SELECT 查最大长度,先清洗数据再改结构 |
Duplicate entry 'xxx' for key 'uk_xxx' | 修改类型/长度后,字段内容发生转换导致唯一索引冲突 | 检查类型转换带来的值变化,必要时先处理重复数据 |
Cannot change column 'xxx': used in a foreign key constraint | 字段被外键引用 | 删除外键 → 修改字段 → 重建外键 |
You have an error in your SQL syntax | 语法错误,常见于 MODIFY/CHANGE 写错 | 检查语法,MODIFY COLUMN和CHANGE COLUMN的参数顺序不同 |
Waiting for table metadata lock | DDL 被长事务持有 MDL 锁阻塞 | 找到阻塞事务并 kill,或低峰期执行 |
Row size too large | 所有变长字段累加超过 65535 字节 | 改大 TEXT 类型,或减少字段数量 |
Invalid use of NULL value | 字段从允许 NULL 改为 NOT NULL,但存在 NULL 值 | 先将 NULL 更新为默认值,再执行 ALTER |
Alter table change column is not supported for generated columns | 试图修改生成列的类型 | 生成列类型由表达式决定,需先删除生成列再重建 |
5.2 修改字段时数据被截断怎么办
最让人头疼的“改完才发现数据出问题”的情况。比如把VARCHAR(500)收缩成VARCHAR(100),结果字段里有 300 字的内容,MySQL 在ALTER时直接报错,或者在某些非严格模式下静默截断。如果你已经执行了,发现数据被截断了,怎么办?
首先,看看有没有备份。如果有最近一次的逻辑备份或者 binlog,可以直接从备份里恢复这部分数据。这其实提醒了一个通用原则:任何结构变更前,最好先做一次逻辑备份。不要嫌麻烦,一条mysqldump命令的成本比出问题后的恢复成本低得多:
mysqldump -h127.0.0.1 -udba -p app_db trade_order > trade_order_backup.sql其次,如果备份时间点太久,可以考虑从 binlog 中找回变更前那一刻的数据。具体做法是用mysqlbinlog工具解析 binlog,定位到 ALTER 语句之前的位置,然后提取对应的事务记录。这个操作有一定的门槛,但对于核心表来说,值得花时间去搞。
最后,如果以上都没有,那就只能接受截断现实,然后用业务侧数据补救。比如备注字段被截断了,可以联系用户重新提交,或者从日志系统里恢复。这里也能看出一个设计上的最佳实践:像备注这种业务价值高、长度不可控的字段,一开始就用TEXT或者MEDIUMTEXT,不要精打细算地设一个很小的 VARCHAR,后期改结构的风险远大于当时多存那几十个字节的成本。
5.3 几个我踩过的坑
最后分享几个真实的踩坑记录,不是教科书内容,是实操中总结出来的。
第一个坑,ALTER TABLE在 MySQL 5.7 和 8.0 的默认行为差别很大。5.7 的 ALTER TABLE 默认会做 COPY,8.0 在很多场景做了优化,比如支持 INSTANT ADD COLUMN、支持原地修改某些 VARCHAR 长度。如果你在 5.7 上习惯了“加列秒开”,升级到 8.0 之后并没有变得更快,因为 8.0 对 MODIFY COLUMN 类型的变更仍然可能走 COPY。别拿着 5.7 的经验拍脑袋评估变更时间。
第二个坑,修改字段长度时默认值的坑。前面提过MODIFY COLUMN是整段替换字段定义,但还有一个更隐蔽的:如果字段定义了DEFAULT CURRENT_TIMESTAMP,你用MODIFY COLUMN created_at DATETIME把默认值丢了,那么以后 INSERT 不指定这个字段时就会报错或者写入 NULL。我在一次上线中就是因为这个原因,导致新订单全部缺少创建时间,花了大半个小时排查才发现是 ALTER 的时候没带上默认值。改动字段前,用 SHOW CREATE TABLE 把完整定义复制出来,在完整定义的基础上改,不要手打一部分属性。
第三个坑,ALTER TABLE 执行中不能 Ctrl+C 直接中断。很多人感觉语句卡住了就 Ctrl+C 想取消,但实际上 DDL 执行到一半被中断,MySQL 会回滚整个操作,但回滚也可能需要很长时间。而且中断期间表上的锁不会立刻释放,后面的请求还是会堆积。更聪明的做法是:先确认有没有会话持有 MDL 锁,没有的话就让 ALTER 跑完,同时准备好回滚方案。
第四个坑,线上环境改了字段类型,但缓存和 ORM 映射没同步更新。比如把status从TINYINT改成了VARCHAR(10),业务代码里还在用 Integer 接收。Java 的 MyBatis 可能能自动转换,但某些严格类型的框架会直接报错。结构变更不光是数据库的事,应用代码、接口文档、日志解析、数据同步管道都要同步考虑。我一般会在变更清单里加上“下游依赖确认”这一项,谁改动谁负责通知到位。
6. 类型变更后的验证与收尾习惯
6.1 变更后的完整性检查清单
结构改完了,不代表万事大吉。每次 ALTER TABLE 之后,我习惯性地跑一遍验证清单,这个习惯帮我避过很多雷:
- 查询表结构是否和预期一致:
SHOW CREATE TABLE table_name; - 检查自增值是否合理:
SELECT AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA='app_db' AND TABLE_NAME='trade_order'; - 对比变更前后行数:变更前记录 count,变更后再查 count,确认数据没有丢失。
- 抽查几条边界数据:比如最大长度的字符串、NULL 值、默认值数据,用
SELECT查出来人工确认。 - 确认索引还在:尤其是唯一的索引,字段长度变化后索引名可能会变,要确保应用程序没有依赖旧的索引名。
- 确认权限没丢:MySQL 的 ALTER TABLE 不会改变表级权限,但如果你改了表名,相关授权里的表名也要跟着改。
6.2 聚合多个变更的最佳实践
在实际项目中,表结构变更往往是成批出现的。比如版本迭代里需求文档列了“订单表增加渠道字段、修改备注长度、调整状态注释”,很多人顺手就开了三个工单、写了三条 ALTER 去执行。但前面提过,分开执行就意味着表被重建了三次,每次重建都是一次全量 COPY 加上索引重建。合并成一条 ALTER 能显著减少总体耗时和锁表窗口。
合并还有一个额外的好处:一条 ALTER 是一个原子操作,要么全部成功,要么全部失败。分开执行的话,第一条成功、第二条失败,数据库就处于一个中间状态,回滚起来非常痛苦。所以变更前先花几分钟把 SQL 整理到一条语句里,是稳赚不赔的习惯。
当然,凡事有例外。如果其中某个字段变更涉及的数据量特别大(比如要把 TEXT 改成 MEDIUMTEXT),而其他变更只是加注释,那合并执行也没问题——因为 InnoDB 处理 ALTER TABLE 时会选择一个最高成本的算法,合并不会让操作变快,但也绝不会比分开跑更慢多少。所以实践中我的原则是:默认合并,特殊拆分,但永远不要在没有把握的情况下一次性堆太多变更。
6.3 一些关于开发和 DBA 协作的建议
字段长度和类型修改这种看似基础的操作,其实很考验一个团队的协作规范。我自己的经验是,任何涉及线上表结构的变更,都需要过一遍 DBA 评审。尤其是大表、核心表、有主从延迟要求的表,哪怕只是改一个注释,也要先看清楚变更语句的执行计划评估。DBA 最怕的不是你提了多复杂的变更,而是你什么都不说直接在生产机上敲了个 ALTER,然后大家的业务请求全部排队卡死。
另外一个好习惯是:在测试环境复制一份线上表结构和数据,先在测试库上执行一遍 ALTER,记录耗时和锁表现象。这个复现结果能帮你确认线上操作时间窗口是否可行。特别是对于ALTER TABLE ... MODIFY COLUMN这类可能触发 COPY 的操作,在测试环境跑一下,就能心里有数:到底会锁表多久、会不会把数据文件撑爆、需不需要提前扩容磁盘。
我在实际工作中,每一条生产 DDL 都会写成变更单,包含回滚方案。回滚方案不是一句“改回去”,而是具体到:如果变更失败,是继续等,还是 kill 掉重来;如果成功但业务异常,用什么样的备份和 binlog 恢复到哪个时间点。这个可执行的回滚路径,比任何 SQL 技巧都重要。
提示:无论你是新手还是老手,我都建议你平时多关注 MySQL 的
information_schema表和官方文档中的 ALTER TABLE 部分。环境千差万别,但原理是通用的,理解了原理之后,很多问题你一眼就能看穿。