🛢️ 第4章 数据库操作
📄 一句话概括

ALTER DATABASE 只能改两样:默认字符集和默认校对规则。 它改的是「以后新建的表」,已有的表一张都不动。 要改老表得 CONVERT TO,那一步会重写数据、锁表,代价按小时算。

📝 本章和前面几章的关系

上一篇选库,这一篇改库。 这一篇的价值不在语法,在「只改以后」这四个字。 很多人以为改完库就万事大吉,结果一个月后发现老表还是老字符集。 和第 5 章的关系:改完库字符集,第 5 章新建的表就继承新默认值。

一、能改的就两样

ALTER DATABASE 库名 CHARACTER SET 字符集 COLLATE 校对规则; 两个参数可以只给一个,也可以都给。

想改 语句 生效范围
默认字符集 ALTER DATABASE 库名 CHARACTER SET utf8mb4 以后新建的表
默认校对规则 ALTER DATABASE 库名 COLLATE utf8mb4_bin 以后新建的表
两个一起 ALTER DATABASE 库名 CHARACTER SET ... COLLATE ... 以后新建的表

注意三行的「生效范围」写的是同一句话。 这不是我抄懒了,是它的全部含义就这一句。 库名、库的创建时间、库的属主,这些都不能用 ALTER DATABASE 改。 库名要改得用 RENAME DATABASE(老版本有,8.0 已经删了)。 所以库名基本等于「一次定终身」——建的时候想清楚。

二、「只改以后」到底是什么意思

表在创建时,会把「当时的库默认值」抄一份记在自己的定义里。 所以库里改了默认值,已经存在的表定义不变。

对象 字符集从哪来
建表时没写 CHARSET 抄当时的库默认值
建表时写了 CHARSET 用它写的那个
改库默认值之后新建的表 抄新的默认值
改库默认值之前建的表 还是老的,一动不动

为什么这么设计?因为改字符集要重写每一行数据。 几百 G 的表能跑一整晚,全程锁表——数据库不敢替你决定这件事。 所以「改库」和「改表」是两个动作,必须分开做。 而且改表的顺序有讲究:先小表后大表,先低峰后高峰。

三、要改老表,只能一张张来

ALTER TABLE 表 CONVERT TO CHARACTER SET utf8mb4; 这一条会把表里所有字符列一起改掉,并且重写数据。

改什么 代价 注意
库默认值 瞬间 只改元数据
一张小表 几秒 期间会锁表
一张大表 几十分钟到几小时 要挑低峰,要留磁盘
几百张表 按加起来算 得写脚本批量跑,还要能续跑

第四行是真干活的形态:没人手动一张张敲。 得先查出所有表的字符集,生成一批 ALTER 语句,再一条条执行、记日志、失败重试。 还有一个坑:CONVERT TO 会重建索引。 所以它占的临时空间大约是「数据 + 索引」的总和,磁盘不够会中途失败。

四、改之前先摸清有多少要改

一条查询就能列出「哪些表还是老字符集」。

查什么 从哪查
每张表的字符集 information_schema.TABLES 的 TABLE_COLLATION
每个列的字符集 information_schema.COLUMNS 的 COLLATION_NAME
库的默认值 information_schema.SCHEMATA 的 DEFAULT_CHARACTER_SET_NAME

三处都要看。 表级改了、列级没改,join 时照样隐式转换;库级改了、表级没改,等于没改。 规范里那句「同库同表同列字符集必须一致」,说的就是这三处要对齐。

五、动手 Demo:改库只改「以后」,这是它最反直觉的地方

这一课把「改之前 / 改之后 / 老表有没有跟着变」三件事做实。 模型里每个库有一份 meta.json 记字符集,改库就是改这份元数据。

cat = Catalog(DATA)
cat.create("db_database28", charset="utf8", collate="utf8_general_ci")
cat.use("db_database28")
c = cat.conn()
c.execute("CREATE TABLE tb_old (id INTEGER PRIMARY KEY, name TEXT)")

# 改之前
print(cat.show_create("db_database28"))

# 改:字符集和校对规则一起换
old, new = cat.alter("db_database28",
                      charset="utf8mb4",
                      collate="utf8mb4_0900_ai_ci")
print(old, "->", new)

# 改之后
print(cat.show_create("db_database28"))

# 老表动没动?再建一张新表对比
c.execute("CREATE TABLE tb_new (id INTEGER PRIMARY KEY, name TEXT)")

实跑输出:

--------------------------------------------------------------------
第二十一课:改库只改「以后」,这是它最反直觉的地方
--------------------------------------------------------------------

  **教材 4.5 节讲修改数据库,一条 `ALTER DATABASE`。**
  **它只能改两样:默认字符集和默认校对规则。**
  **最反直觉的是「只改以后」:已有的表一张都不动。**
  **这一课把这件事做实。**

  一、教材原码
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | ALTER DATABASE db_database28 CHARACTER SET utf8mb4;
  | ALTER DATABASE db_database28 COLLATE utf8mb4_0900_ai_ci;
  | ALTER DATABASE db_database28 CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
    (改字符集、改校对规则、两个一起改)


  二、真跑:改之前和改之后
  --------------------------------------------------------------------
  **改之前:**
    CREATE DATABASE `db_database28` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE
        utf8_general_ci */
  **改之后:**
    CREATE DATABASE `db_database28` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE
        utf8mb4_0900_ai_ci */

  项        改之前           改之后
  ---------------------------------------------
  字符集    utf8             utf8mb4
  校对规则  utf8_general_ci  utf8mb4_0900_ai_ci

  三、老表动没动
  --------------------------------------------------------------------

  表      建的时间  它继承到的字符集
  -------------------------------------------
  tb_old  改之前    utf8(没跟着变)
  tb_new  改之后    utf8mb4(继承了新默认值)
  **这就是「只改以后」的实样:老表还是老字符集。**
  **要改老表,得一张张 `ALTER TABLE 表 CONVERT TO CHARACTER SET utf8mb4`。**
  **为什么数据库不自动改?因为那要重写每一行数据。**
  **几百 G 的表能跑一整晚,全程锁表,数据库不敢替你决定。**

  四、改字符集的三档代价
  --------------------------------------------------------------------

  改什么        语句                                代价
  ------------------------------------------------------------------------
  库默认值      ALTER DATABASE ... CHARACTER SET    瞬间,只改元数据
  表(含数据)  ALTER TABLE ... CONVERT TO ...      重写整表,大表按小时算
  某一列        ALTER TABLE ... MODIFY 列 新字符集  重写这一列,同样慢
  **第三档最容易被顺手做:只改一个字段看着聪明,其实照样要重写。**
  **而且改完这一列和别的列字符集不同,join 时又要隐式转换。**

第一段那个库是故意用 utf8 建的——模拟一个 5.7 时代留下来的老库。 这一节讲的所有麻烦,都是从这种库开始的。 alter 返回了「改之前」和「改之后」两组值,这里打出来是为了让你看清只有这两个字段变了。 库名、创建时间、里面的表,一个都没被碰。 第三段那两张表是关键:tb_old 是改之前建的,tb_new 是改之后建的。 它们继承到的默认字符集不一样——这就是「只改以后」的实样。 最后那张三档代价表里,第三档最容易被顺手做。 「只改一个字段」看着聪明,其实照样重写。 改完这一列和别的列不一致,join 时又要隐式转换。

30 秒实验

  1. 把 Demo 里 cat.alter 那一句的 charset 参数去掉,只改 collate。 字符集不变,校对规则变了。 这说明两个参数是独立的,可以只改一个。
  2. 把 utf8mb4_0900_ai_ci 换成 utf8mb4_bin,想想哪些查询结果会变。 所有 =、IN、DISTINCT、GROUP BY 的大小写敏感性都会变。
  3. 想一想:一亿行的表要转字符集,怎么做得更稳? 分三次:先加一个影子列同步写、再分批回填、最后切换。 一次做完的风险是「跑了一晚上,中途失败,回滚又要一晚」。

常见误区

  1. 「改完库的字符集,整库就升级了。」 只改「以后新建的表」。 老表得一张张 CONVERT TO,而且会重写数据、锁表。
  2. 「CONVERT TO 只是改个定义,很快。」 它要重写每一行、重建索引,占的临时空间约等于「数据加索引」。 磁盘不够会中途失败。
  3. 「库名起错了,改一下就行。」 8.0 已经没有 RENAME DATABASE 了。 能做的只有「建新库、导数据、删旧库」,这也是一次停机。
自测三问(点开对答案) 1. `ALTER DATABASE ... CHARACTER SET` 的生效范围是什么? —— **只对以后新建的表生效。** **已有的表定义不变,要改变得 `ALTER TABLE ... CONVERT TO`。** 2. 为什么数据库不自动把老表一起改掉? —— **因为改字符集要重写每一行数据,大表能跑一整晚,全程锁表。** **这个决定不该由数据库替你做。** 3. 改字符集之前要摸清哪三处? —— **库默认值、每张表的字符集、每个列的字符集。** **三处不对齐,join 时照样隐式转换、索引失效。**

下一篇是本章最后一节,也是最危险的一节:删库。 删之前量什么、删错了怎么救、以及比备份更该做的三件事。

相关笔记