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

RENAME TABLE 能一次改多张、能跨库,而且多张之间是原子的。 ALTER TABLE ... RENAME TO 只能改一张、只能在同一个库里。 改名只动字典,所以再大的表也是秒回;但视图里的表名文本不会跟着改。

📝 本章和前面几章的关系

6.3 改的是列名,这一篇改的是表名——后者干净得多,因为它不碰数据。 它是「在线切换」这个手法的技术基础:新表建好、换名、旧表下线。 这一篇的两个写法,在第 6.6 节「建新表再换名」里还会用到。

一、两种写法,能力不一样

教材 6.4 讲的是 RENAME TABLE。它和第 6.3 节末尾那个 ALTER TABLE ... RENAME TO 是两回事。

RENAME TABLE tb_a TO tb_b;                  -- 一张
RENAME TABLE tb_a TO tb_b, tb_c TO tb_d;    -- 多张,一次原子完成
RENAME TABLE db1.tb_a TO db2.tb_a;          -- 跨库搬
ALTER TABLE tb_a RENAME TO tb_b;            -- 只能一张、同库
写法 一次改几张 能跨库 语法位置
RENAME TABLE 任意多张 能 独立语句
ALTER TABLE ... RENAME TO 只能一张 不能 ALTER 的一个子句

日常改名用哪个都行,需要批量或跨库才非 RENAME TABLE 不可。 但知道它有第三条能力的人不多:多张之间是原子的。

二、改名为什么是秒回

因为表名只存在数据字典里,改名就是把那个字符串换掉。 数据页、索引页、磁盘文件一个字节都不用挪。 这一点和「改列名」同理,和「改类型」完全相反。 所以「表名拼错了」这种事故,修复成本几乎为零;「字段类型选错了」才要命。 但秒回不等于没有副作用。 改名之后,所有「按表名硬编码」的东西都指着旧名字:视图、存储过程、触发器、应用程序里的 SQL。 数据库只负责自己那半边,应用程序那半边它管不了。

三、一次改多张是原子的

RENAME TABLE a TO b, c TO d 是「一个操作」,不是「两条语句」。 所以要么两张都改成,要么一张都不动。 这一条在发版时特别有用。 典型场景是「新表和旧表互换名字」:

-- 新数据已经灌进 tb_order_new
RENAME TABLE tb_order TO tb_order_old,
             tb_order_new TO tb_order;

这两句一起执行,业务方看到的是「表名还是 tb_order,数据变了」,中间没有空窗。 如果拆成两句执行,中间那一瞬间 tb_order 是不存在的,所有查询都会报错。 这就是原子性的实际价值:不是「性能好」,是「不会出现中间态」。 在线变更的所有手法,本质都是在消灭中间态。

四、跨库搬表

RENAME TABLE db1.tb TO db2.tb 能把表从一个库移到另一个库。 在同一台服务器上,这一步也只是改字典,不搬数据。 但有两个前提要知道。 一是两个库要在同一个文件系统上(MySQL 8.0 用 InnoDB 通用表空间时基本都满足)。 二是目标库里不能有同名表。 如果不是「搬」而是「抄一份过去」,就得用第 6.5 节那两种复制法,再加一步删源表。 那样就不是原子的了,三步里任何一步失败都会留下两份。

五、改名之后,谁会坏

数据库里存「表名文本」的地方,改名时都不会自动更新。 教材没讲这一节,但这是改名真正会出事的地方。

对象 存的是文本还是引用 改名后
视图 FROM 表名 这段文本 指旧名字,一查就报错
存储过程/函数 里面写的 SQL 文本 运行时报错
触发器 ON 表名 这段文本 可能跟着表走,要看版本
外键约束 引用的是表 ID 正常,不受影响
应用程序里的 SQL 硬编码的表名 要改代码、要发版

第一行是真会出事的。 所以改名前先查一遍:information_schema.VIEWS 的 VIEW_DEFINITION 里搜表名。 第二行更隐蔽,因为它不是建表时就报错,是三个月后跑到那个存储过程才报。 一个稳妥的做法是:改名前先在测试库上改一遍,然后把相关功能点都跑一遍。

六、动手 Demo:改名:两个写法的差别,和它带坏的视图

这一课把两个写法、原子性、跨库搬表各跑一遍。 最后会看到一个和教材说法相反的实测结果,那个结果本身值得记住。 沙箱里没有 RENAME TABLE,多张改名用事务包住两条 ALTER 来等价。

# 一、改名:数据、索引都跟着走
c.execute("ALTER TABLE tb_old RENAME TO tb_new")
print(c.execute("SELECT COUNT(*) FROM tb_new").fetchone())
print(c.execute("PRAGMA index_list(tb_new)").fetchall())

# 二、视图会不会跟着改?(这一条结论反了)
print(c.execute("SELECT * FROM v_old").fetchall())

# 三、一次改两张,中途失败会怎样
c.execute("BEGIN")
c.execute("ALTER TABLE tb_a RENAME TO tb_b")
c.execute("ALTER TABLE tb_c RENAME TO tb_d")   # 这里撞车
c.execute("ROLLBACK")

# 四、跨库搬表:ATTACH 挂第二个文件,三步走
c.execute("ATTACH DATABASE ? AS db2", (path2,))
c.execute("INSERT INTO db2.tb_move SELECT * FROM main.tb_move")
c.execute("DROP TABLE main.tb_move")

实跑输出:

--------------------------------------------------------------------
第二十八课:改名有两个写法,能力不一样
--------------------------------------------------------------------

  **教材 6.4 讲 `RENAME TABLE`。它和第 6.3 节末尾那个 `ALTER TABLE ... RENAME TO` 是两回事。**
  **一个能一次改多张、还能跨库搬;一个只能改一张、只能在同一个库里。**
  **这一课把两种写法都跑一遍,再看改名之后依赖它的东西会怎样。**

  一、两种写法摆一起
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | RENAME TABLE tb_a TO tb_b;                  -- 一张
  | RENAME TABLE tb_a TO tb_b, tb_c TO tb_d;    -- 多张,一次原子完成
  | RENAME TABLE db1.tb_a TO db2.tb_a;          -- 跨库搬
  | ALTER TABLE tb_a RENAME TO tb_b;            -- 只能一张、同库
  ① 教材原码,本沙箱不执行


  写法                         一次改几张  能跨库  是不是原子
  -------------------------------------------------------------------------
  `RENAME TABLE`               任意多张    能      能(多张一起成或一起败)
  `ALTER TABLE ... RENAME TO`  只能一张    不能    单张当然原子
  **第三列是很多人不知道的:`RENAME TABLE a TO b, c TO d` 是原子的。**
  **要么两张都改成,要么一张都不动,没有「改了一半」的中间态。**
  **这一条在发版时很有用:新表和旧表互换名字,业务方看不到中间空窗。**
  **所以「先建一张新表,再换名」是标准的在线切换手法。**

  二、改名之后,数据、索引、视图会怎样
  --------------------------------------------------------------------

  mysql> ALTER TABLE tb_old RENAME TO tb_new
  **数据和索引都跟着走:新名字下 3 行,索引是 i_name。**
  **因为改名只动数据字典里那一个字符串,数据页和索引页一个字节都不用挪。**
  **所以再大的表改名也是秒回——这是它和「改字段类型」最大的区别。**
  **接下来试那个视图。按常识它应该坏掉,因为视图里存的是 `FROM tb_old` 这句文本。**
  **我查了一次,结果和预期相反:**

  动作                           结果
  -------------------------------------
  SELECT * FROM v_old(改名后)  还能查
  **居然还能查。因为 SQLite 从 3.25 起会在改名时顺手把视图定义里的表名一起改掉。**
  **这是它的体贴,不是所有库的通用规律。**
  **MySQL 不这么做。视图里那句 `FROM tb_old` 就是一段文本,表改名之后它照样指着旧名字。**
  **所以这条要在真 MySQL 上验,结论是「会坏」。**
  **实践上的动作是:改名前先查一遍谁引用了它。**
  **MySQL 里可以查 `information_schema.VIEWS` 的 `VIEW_DEFINITION`,把表名搜一遍。**

  三、一次改两张,中途失败会怎样
  --------------------------------------------------------------------
  **我故意让第二句失败:`tb_d` 已经存在了。**
  **第一句是把 `tb_a` 改成 `tb_b`,它是合法的;第二句要撞上已有的表。**

  时刻          表有哪些
  --------------------------------------------------------------------------
  动手之前      tb_a、tb_c、tb_d
  第二句报错    there is already another table or index with this name: tb_d
  事务回滚之后  tb_a、tb_c、tb_d
  **结果是没有出现 `tb_b`,第一句也跟着退了。**
  **这就是原子:两句在一个事务里,要么全成要么全不成。**
  **真 MySQL 的 `RENAME TABLE a TO b, c TO d` 也是这样,它内部就是一个操作,不是两条语句。**
  **这一点在切换发布时很关键:不会出现「一半改了、一半没改」的中间态。**

  四、跨库搬表
  --------------------------------------------------------------------
  **沙箱里没有「一个服务器多个库」的概念,用 `ATTACH` 挂第二个文件来模拟。**
  **真 MySQL 一句 `RENAME TABLE db1.tb TO db2.tb` 就完事,这里要三步:**

  步骤                 结果
  -----------------------------------------------------
  在 db2 建同名表结构  成功
  把数据拷过去         db2 里有 3 行
  删掉 db1 的源表      db1 里还剩 0 张名叫 tb_move 的表
  **三步不是原子的,中间任何一步失败都会留下两张表或者一份数据。**
  **真 MySQL 那一句是原子的,这也是「能一句就别拆三步」的典型例子。**
  **还有一个细节:跨库搬表在同一台服务器里只是改字典,不搬数据。**
  **但如果两个库在同一个 InnoDB 表空间下,它连文件都不用动。**

第一段:改名之后,新名字下 3 行,索引还是 i_name。 数据和索引都跟着走,因为改名只动字典里那一个字符串。 第二段是这一课最值得记的地方。按常识视图应该坏掉,因为它存的是 FROM tb_old 这句文本。 我查了一次,它居然还能查——因为 SQLite 从 3.25 起会在改名时顺手把视图定义里的表名一起改掉。 这是 SQLite 的体贴,不是通用规律。 MySQL 不这么做:视图里那句 FROM tb_old 就是一段文本,改完名它照样指着旧名字。 所以这条要在真 MySQL 上验,结论是「会坏」。 第三段:第二句故意撞上已存在的 tb_d,报错之后回滚,tb_b 没出现。 表清单回到 tb_a、tb_c、tb_d,第一句也跟着退了。 这就是原子——真 MySQL 的 RENAME TABLE a TO b, c TO d 内部就是一个操作。 第四段跨库搬表要三步:建结构、拷数据、删源表。 db2 里有 3 行,db1 里剩 0 张。 三步不是原子的,真 MySQL 一句 RENAME TABLE db1.t TO db2.t 才是。

30 秒实验

  1. 把改名那一步改成 RENAME TABLE 多张互换的写法,想想为什么中间不会有空窗。 因为两句合成一个操作,外面看不到「两个名字都不存在」的那一瞬间。
  2. 建一个存储过程,里面写 SELECT * FROM tb_old,再改名,然后调用它。 会报错。存储过程里的表名也是文本。 这一条在真 MySQL 上一定成立,SQLite 不支持存储过程所以验不了。
  3. 把 tb_a 改名成 TB_A(只改大小写),看会不会成功。 在 Windows 上可能报「已存在」,因为文件名不区分大小写。 Linux 上要看 lower_case_table_names 参数。

常见误区

  1. 「改名之后视图会自动跟着改。」 MySQL 不会。视图里存的是文本,改名之后它就指着旧名字。 SQLite 3.25 之后会自动改,但那是它的特性,不能当通用规律。
  2. 「一次改两张就是执行两条语句。」 RENAME TABLE a TO b, c TO d 是一个操作,原子。 拆成两条单独执行才不是原子的,中间会出现两个名字都空着的瞬间。
  3. 「跨库搬表会把数据复制一遍。」 同一台服务器上只是改字典,不搬数据。 跨服务器才需要导出再导入,那是另一件事。
  4. 「改名是小事,不用通知人。」 应用程序里的 SQL、报表脚本、定时任务都可能硬编码了表名。 数据库这边秒回,业务那边可能整晚报错。
自测三问(点开对答案) 1. `RENAME TABLE` 和 `ALTER TABLE ... RENAME TO` 差在哪? —— **前者能一次改多张、能跨库,多张之间是原子的。** **后者只能改一张、只能在同一个库里。** 2. 为什么改名再大的表也是秒回? —— **因为表名只存在数据字典里,改名就是换个字符串,数据页和索引页一个字节都不动。** 3. 改名之后哪一类对象最容易悄悄坏掉? —— **视图和存储过程。它们存的是表名文本,不会跟着改名更新,而且不一定当场报错。**

6.4 到这儿结束。下一篇 6.5 讲复制表——建新表再换名这个手法,前半截就是「复制表」。

相关笔记