ALTER TABLE 四件事:加列、改列、改列名、删列。
四件事的代价差得远,只有改名字是秒回。
加列可以走 INSTANT,改类型和删列要重写整张表。
「改一个字段类型」在千万行表上不是一条 DDL,是一次迁移。
6.1 建的表现在要改,这一篇讲怎么改、改的时候表会怎样。 它是第 5 章「类型选错了很难改」那句的兑现:难就难在这里。 这一篇的四种改法,在第 12 章讲索引维护时会再撞上一次。
一、加列:最轻的一种
ADD COLUMN 是四种改法里唯一「几乎不耗时」的,前提是加在末尾。
ALTER TABLE tb_stu ADD COLUMN sex CHAR(1) DEFAULT '未知';
ALTER TABLE tb_stu ADD COLUMN remark TEXT;
MySQL 8.0 起,加列默认走 INSTANT 算法:只改数据字典,不动数据文件。
所以百万行的表加一列也是毫秒级。
但有两个前提。
一是加在最后一列(写 AFTER 某列 就退回重建);二是不带「没法给老行补的值」。
没给默认值时,老行的新列就是 NULL——这不用改数据文件。
给了默认值,8.0 也只在字典里记一句「缺省是这个值」,同样不动数据。
但加 NOT NULL 又不给默认值,一定失败。
因为已有的行没法满足这个约束,数据库也不会替你想一个默认值。
报错会直接说 Cannot add a NOT NULL column with default value NULL。
二、改类型:MODIFY 与 CHANGE
两句只差一件事:MODIFY 不动列名,CHANGE 连名字一起改。
ALTER TABLE tb_stu MODIFY age TINYINT NOT NULL DEFAULT 0;
ALTER TABLE tb_stu CHANGE age stu_age TINYINT NOT NULL DEFAULT 0;
注意 CHANGE 要写两遍列名:旧名在前,新名在后,然后才是类型。
这个写法很容易写漏,漏了就变成「把列改名成类型名」,报错很难看懂。
代价要看「改的方向」。
| 改的方向 | 要不要重写数据 | 为什么 |
|---|---|---|
VARCHAR(20) → VARCHAR(50) |
要 | MySQL 8.0 之前一律重建 |
INT → BIGINT |
要 | 每行的字节数变了 |
INT → TINYINT |
要 | 还要检查有没有超出范围的值 |
加 NOT NULL |
要 | 要逐行确认没有 NULL |
改 DEFAULT |
不要 | 只动字典 |
第一行那个「8.0 之前一律重建」要补一句:8.0 起对「只加长 VARCHAR」有 INSTANT 优化, 但条件很窄(长度前缀字节数不变),实际很少能命中。 所以按「要重建」来估算时间比较安全。 第三行是最容易被忽略的:把类型改窄,数据库要逐行检查值放不放得下。 只要有一行放不下,整条语句失败,表回到原样——这一点倒是安全。
三、删列:DROP COLUMN
DROP COLUMN 看着比改类型简单,代价一样重。
ALTER TABLE tb_stu DROP COLUMN sex;
它不是「把那一列标记成删除」,是把每一行重写一遍、把那一列的数据扔掉。
所以大表上删列和改类型是一个量级的事。
删之前有两件事要确认。
一是这一列有没有被索引、视图、触发器引用——有的话可能直接报错,也可能删完才发现坏了。
二是这一列的数据要不要留一份。
DROP COLUMN 是 DDL,隐式提交,删完就没有撤销这回事了。
一个稳妥的做法是先改名再删。
CHANGE 列名 列名_old_20261030 类型,跑一周没人喊,再 DROP。
改名是秒回的,这一步几乎零成本,换来的是一周的后悔期。
四、改列名:唯一秒回的一种
8.0 起有 RENAME COLUMN,它只改字典里的字符串,不碰数据。
ALTER TABLE tb_stu RENAME COLUMN name TO stu_name;
所以再大的表也是秒回,这一点和「改表名」是同一个道理。
| 语句 | 改什么 | 要不要重写数据 |
|---|---|---|
RENAME COLUMN |
列名 | 不要 |
MODIFY |
类型和属性 | 通常要 |
CHANGE |
列名+类型 | 通常要 |
这张表值得记一下,因为很多人以为 CHANGE 是「改名顺便改类型」,改个名也得等半天。
其实只改名用 RENAME COLUMN 就够了。
8.0 之前没有这句话,改名只能靠 CHANGE,所以老资料里全是 CHANGE 的写法。
看到老教程写「改名用 CHANGE」,要知道那是版本限制,不是最佳实践。
五、四种改法的代价排行
按代价从轻到重排一遍,这张表决定了「什么时候做」。
| 改法 | 算法 | 大表上能不能在线做 |
|---|---|---|
改列名 RENAME COLUMN |
只改字典 | 能,秒回 |
加列(末尾、无 AFTER) |
INSTANT | 能,毫秒级 |
| 改默认值 | 只改字典 | 能 |
| 改类型、删列、加索引 | COPY 或 INPLACE | 要窗口,可能要停写 |
前三行可以随时做,第四行要排期。 这就是「设计阶段多花十分钟,运维阶段少加三个夜班」的具体含义。 最后一行里「加索引」也在,这一点常被低估。 加索引要扫全表建一棵树,百万行以上就不是几秒钟的事了。 MySQL 8.0 的在线加索引不锁写,但会占 IO,业务能感觉到慢。
六、动手 Demo:改表结构:四件事与三步重建
这一课在 1000 行的表上把四种改法走一遍。
沙箱里 ADD COLUMN、DROP COLUMN、RENAME COLUMN 都有,MODIFY 和 CHANGE 没有。
没有的那两个,正好可以按底层真实做法演一遍:建新表、拷数据、换名。
# 一、加列:老行填 NULL
c.execute("ALTER TABLE tb_stu ADD COLUMN sex TEXT")
# 二、加 NOT NULL 无默认值:一定失败
print(try_do(c, "ALTER TABLE tb_stu ADD COLUMN sid TEXT NOT NULL"))
# 三、改类型:MySQL 一句,这里三步
c.execute("CREATE TABLE tb_stu_new (id INTEGER PRIMARY KEY, name TEXT, "
"stu_age INTEGER NOT NULL DEFAULT 0 CHECK (stu_age BETWEEN 1 AND 150))")
c.execute("INSERT INTO tb_stu_new (id, name, stu_age) SELECT id, name, age FROM tb_stu")
c.execute("DROP TABLE tb_stu")
c.execute("ALTER TABLE tb_stu_new RENAME TO tb_stu")
# 四、只改名:一句就够,不碰数据
c.execute("ALTER TABLE tb_stu RENAME COLUMN name TO stu_name")
实跑输出:
--------------------------------------------------------------------
第二十七课:改表结构,四件事与三步重建
--------------------------------------------------------------------
**教材 6.3 讲 `ALTER TABLE` 的四件事:加列、改列、改列名、删列。**
**MySQL 里四件都是一句话,沙箱里有两件做不到,得重建整张表。**
**做不到的那两件正好能说明一件事:改结构在底层到底是什么动作。**
一、加一列:ADD COLUMN
--------------------------------------------------------------------
**加列是最轻的一种,MySQL 8.0 起对「加在最后一列」这种可以走 INSTANT 算法,几乎不耗时。**
mysql> ALTER TABLE tb_stu ADD COLUMN sex TEXT
**加完之后老行里那个新列是什么?我查了一行:**
id name age sex
--------------------
1 学生1 19 NULL
**是 NULL。**
**因为加列时没给默认值,已有的 1000 行统一按 NULL 处理。**
**如果给一个默认值,8.0 起也不会真去改 1000 行,它在元数据里记一句「这一列缺省是某值」。**
**这就是 INSTANT 算法能做到「秒回」的原因。**
**但有一种加列会失败,我试了:**
语句 结果
----------------------------------------------------------------------------------
ADD COLUMN sid TEXT NOT NULL Cannot add a NOT NULL column with default value NULL
**因为已有的 1000 行没法满足「不能为空」又「没有默认值」。**
**要用 `NOT NULL` 就必须同时给 `DEFAULT`。**
**真 MySQL 报的错误号是 1364 或 1138,意思一样。**
二、删一列:DROP COLUMN
--------------------------------------------------------------------
mysql> ALTER TABLE tb_stu DROP COLUMN sex
**删掉之后,表里剩这几列:id、name、age**
**这一句在 MySQL 里是「重建整张表」,不是「把那一列标记为删除」。**
**所以大表上删列要按重活排期:InnoDB 要把每一行重写一遍。**
三、改列的类型和名字:MySQL 一句,这里三步
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| ALTER TABLE tb_stu MODIFY age TINYINT NOT NULL DEFAULT 0;
| ALTER TABLE tb_stu CHANGE age stu_age TINYINT NOT NULL DEFAULT 0;
③ 教材原码,本沙箱不执行
**`MODIFY` 改类型不动名字,`CHANGE` 两个一起改——这是它们唯一的分工。**
**沙箱里没有这两句,只能按底层真实做法来:建新表、拷数据、换名。**
字段 类型 空 默认
---------------------------
id INTEGER YES NULL
name TEXT YES NULL
stu_age INTEGER NO 0
**1000 行、三步、耗了 0.024 秒。**
**三步的顺序不能换:先建新表,再拷数据,最后换名——换名是唯一一步「原子」的。**
**为什么不能先删旧表?因为拷的过程中还要读旧表。**
**为什么不能边拷边改?因为改到一半失败,两张表都是半截数据。**
**代价也在这三步里:拷的时候要有差不多两倍的磁盘,还要能停写。**
**所以「改一个字段类型」这件小事,在千万行表上就是一次迁移。**
四、只改名字:MySQL 也能不重建
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| ALTER TABLE tb_stu RENAME COLUMN name TO stu_name;
④ 教材原码,本沙箱不执行
**8.0 起有 `RENAME COLUMN`,它只改字典里的名字,不碰数据。**
**沙箱对应的是同名的 `ALTER TABLE ... RENAME COLUMN`:**
mysql> ALTER TABLE tb_stu RENAME COLUMN name TO stu_name
**现在的列是:id、stu_name、stu_age**
**区分清楚:`RENAME COLUMN` 是改列名,`MODIFY` 是改类型,`CHANGE` 是两者一起改。**
**只有第一种是「秒回」,后两种都要重写数据(除非只放宽长度)。**
第一段加完列,我立刻查了一行:新列是 NULL。
因为加列时没给默认值,已有的 1000 行统一按 NULL 处理。
第二段那句 ADD COLUMN sid TEXT NOT NULL 被拒了,
报的是Cannot add a NOT NULL column with default value NULL。
要用 NOT NULL 就必须同时给 DEFAULT,这是硬规矩。
第三段的三步耗了 0.024 秒,1000 行。
这个数每次跑会浮动,别当基准,只看量级。
要紧的是那三步的顺序:先建新表、再拷数据、最后换名。
为什么不能先删旧表?因为拷的时候还要读它。
为什么不能边拷边改?因为改到一半失败,两张表都是半截数据。
为什么最后一步才换名?因为换名是原子操作,前面的都不算数。
第四段那句 RENAME COLUMN 才是「只改名」的正确写法。
它和第三段的三步形成对照:改名字只动字典,改类型要重写数据。
30 秒实验
- 给
tb_stu加一列birth DATE,再跑一次DESCRIBE看它排在哪。 排在最后。ADD COLUMN只能往末尾加,想插在中间只能重建。 - 把
stu_age的CHECK (stu_age BETWEEN 1 AND 150)去掉再跑一遍三步重建,比较两次耗时。 差别很小,因为瓶颈在拷数据和落盘,不在约束检查。 - 想一想:三步重建的过程中,如果有别的连接在读这张表,会看到什么?
会看到「表突然不存在了」——
DROP TABLE和RENAME TO之间有一个空窗。 真 MySQL 里靠事务或RENAME TABLE一句搞定来避免。
常见误区
- 「加列不影响大表,随时能加。」
加在末尾可以,但只要写了
AFTER 某列,就退回重建。 很多人加列时顺手写AFTER,把秒回变成了几十分钟。 - 「删列很快,反正只是删一列。」 它要把每一行重写一遍。 和改类型是一个量级,大表上同样要排期。
- 「
CHANGE只是改名,很快。」CHANGE连类型一起改,通常要重写数据。 只改名要用 8.0 的RENAME COLUMN。 - 「改失败了表就坏了。」 DDL 失败会回滚,表回到原样。 真正危险的是「改到一半进程被杀」,那要靠 MySQL 8.0 的原子 DDL 来保证。
自测三问(点开对答案)
1. `MODIFY` 和 `CHANGE` 差在哪? —— **只差「要不要同时改列名」。** **`CHANGE` 要写两遍列名,旧名在前、新名在后。** 2. 为什么 `ADD COLUMN ... NOT NULL` 会失败? —— **已有的行没法满足「不能为空」,数据库也不会替你想默认值。** **要加 `NOT NULL` 就必须同时给 `DEFAULT`。** 3. 三步重建的顺序为什么不能换? —— **先建新表(有地方放)、再拷数据(旧表还在,能读)、最后换名(原子)。** **换任何一步都会出现「丢数据」或者「两张半截表」。**6.3 到这儿结束。下一篇 6.4 讲重命名表—— 改列名和改表名不是一回事,后者还能一次改多张、还能跨库。
- ☐ (本篇完,下一篇 6.4 重命名表)