🛢️ 第6章 数据表操作
📄 一句话概括

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 秒实验

  1. 给 tb_stu 加一列 birth DATE,再跑一次 DESCRIBE 看它排在哪。 排在最后。ADD COLUMN 只能往末尾加,想插在中间只能重建。
  2. 把 stu_age 的 CHECK (stu_age BETWEEN 1 AND 150) 去掉再跑一遍三步重建,比较两次耗时。 差别很小,因为瓶颈在拷数据和落盘,不在约束检查。
  3. 想一想:三步重建的过程中,如果有别的连接在读这张表,会看到什么? 会看到「表突然不存在了」——DROP TABLE 和 RENAME TO 之间有一个空窗。 真 MySQL 里靠事务或 RENAME TABLE 一句搞定来避免。

常见误区

  1. 「加列不影响大表,随时能加。」 加在末尾可以,但只要写了 AFTER 某列,就退回重建。 很多人加列时顺手写 AFTER,把秒回变成了几十分钟。
  2. 「删列很快,反正只是删一列。」 它要把每一行重写一遍。 和改类型是一个量级,大表上同样要排期。
  3. 「CHANGE 只是改名,很快。」 CHANGE 连类型一起改,通常要重写数据。 只改名要用 8.0 的 RENAME COLUMN。
  4. 「改失败了表就坏了。」 DDL 失败会回滚,表回到原样。 真正危险的是「改到一半进程被杀」,那要靠 MySQL 8.0 的原子 DDL 来保证。
自测三问(点开对答案) 1. `MODIFY` 和 `CHANGE` 差在哪? —— **只差「要不要同时改列名」。** **`CHANGE` 要写两遍列名,旧名在前、新名在后。** 2. 为什么 `ADD COLUMN ... NOT NULL` 会失败? —— **已有的行没法满足「不能为空」,数据库也不会替你想默认值。** **要加 `NOT NULL` 就必须同时给 `DEFAULT`。** 3. 三步重建的顺序为什么不能换? —— **先建新表(有地方放)、再拷数据(旧表还在,能读)、最后换名(原子)。** **换任何一步都会出现「丢数据」或者「两张半截表」。**

6.3 到这儿结束。下一篇 6.4 讲重命名表—— 改列名和改表名不是一回事,后者还能一次改多张、还能跨库。

相关笔记