🛢️ 第3章 使用MySQL图形化管理工具
📄 一句话概括

建表要想清楚四件事:字段类型、约束、引擎、字符集。 前两件错了要改数据,后两件错了要重建表。 改表就是 ALTER TABLE,能加列、改列、删列、改名,但每一种的代价差很远。 删表 DROP 连结构带数据全没;清空 TRUNCATE 只清数据,结构还在。

📝 本章和前面几章的关系

上一篇建库,这一篇建表。库是容器,表才是真正装数据的地方。 它是第 6 章(数据类型)和第 7 章(约束)的预告片。 这一篇只讲「怎么建」,类型和约束的细节在第 6、7 章展开。 和第 20 章(优化)的关系:索引也是在表上建的,这一篇只提一句。

一、建表:四件事想清楚

一条 CREATE TABLE 里塞着四类决定,混在一起看容易漏。

决定 写在哪 错了怎么改
字段与类型 列名 + 类型长度 改类型要 MODIFY,大表会重写
约束 NOT NULL、PRIMARY KEY、UNIQUE、DEFAULT 加约束要扫全表,脏数据会挡下来
存储引擎 ENGINE=InnoDB 改引擎等于整表重建
字符集 DEFAULT CHARSET=utf8mb4 改字符集也等于整表重建

最容易后悔的是第二件:建表时没加非空约束,数据里就全是 NULL。 事后再加 NOT NULL,得先把那些 NULL 填上,否则一条语句都改不动。 所以建表时那一串 NOT NULL 不是形式主义,它是把「以后要清洗数据」这件事提前做掉。

二、改表:六种动作,代价不一样

ALTER TABLE 能干很多事,常用的六种,代价从「瞬间」到「重建整表」都有。

动作 写法 代价
加列 ADD COLUMN 列 类型 DEFAULT 值 8.0 上多数瞬间完成
改列名 CHANGE COLUMN 旧 新 类型 瞬间,只改元数据
改列类型 MODIFY COLUMN 列 新类型 可能重写整表
删列 DROP COLUMN 列 瞬间(8.0 起是原子的)
加索引 ADD INDEX 名 (列) 要扫全表建索引,大表慢
改表名 RENAME TO 新名 瞬间,但所有 SQL 都要跟着改

加索引那一行最容易被低估:它只写一行,但要扫一遍全表。 一亿行的表上,这一行能跑十几分钟,期间写入还会变慢。

三、删表与清空:一字之差

DROP TABLE 表; 连结构带数据一起删。 TRUNCATE TABLE 表; 只清数据,表还在,字段和索引都不动。

对比项 TRUNCATE DROP
表结构 留着 一起删掉
能不能回滚 不能(隐式提交) 不能
速度 很快,几乎和行数无关 很快
自增值 重置回 1 没意义,表都没了
典型用途 清空测试数据、重置自增 废弃这张表

最容易踩的坑是以为 TRUNCATE 能回滚。 它不行:TRUNCATE 是 DDL,执行前会隐式提交,事务包不住它。 DELETE FROM 表; 看起来和 TRUNCATE 一样,其实差很远: 它逐行删、能回滚、不重置自增,大表上还特别慢。

四、图形工具里的表结构页

phpMyAdmin 的「结构」页,画的就是这张表的字段清单。 改一个字段的类型,它发出去的是一条 ALTER TABLE ... MODIFY。 它有一个地方比手写 SQL 好用:改之前会告诉你「这次改动会重建表」。 大表上看到这句话,就该换个时间做了。 也有一个地方容易出事:它能一键「清空表」,而那一键就是 TRUNCATE。 点之前手会抖一下,那就对了。

五、动手 Demo:表结构那点事,一条 ALTER 与三步重建

这一课真建一张表,再改它五次,看哪些改动 MySQL 一条语句就够、SQLite 得绕路。 绕路的那种要「建新表、拷数据、换名字」,这三步在大表上就是几十分钟的停机。

import sqlite3

conn = sqlite3.connect(db)
conn.execute("CREATE TABLE tb_goods (id INTEGER PRIMARY KEY,"
             " name TEXT NOT NULL, price REAL NOT NULL DEFAULT 0.00)")

# 三件 SQLite 一条语句能做的
conn.execute("ALTER TABLE tb_goods ADD COLUMN stock INTEGER DEFAULT 0")
conn.execute("ALTER TABLE tb_goods RENAME COLUMN stock TO qty")
conn.execute("ALTER TABLE tb_goods DROP COLUMN qty")

# 两件做不到的
try:
    conn.execute("ALTER TABLE tb_goods ALTER COLUMN memo TYPE VARCHAR(20)")
except sqlite3.Error as e:
    print("改列类型:", e)
try:
    conn.execute("ALTER TABLE tb_goods ADD CONSTRAINT c1 CHECK (price > 0)")
except sqlite3.Error as e:
    print("加约束:", e)

# 绕路:建新表、拷数据、删旧表、改名
conn.execute("CREATE TABLE tb_goods_new (...)")
conn.execute("INSERT INTO tb_goods_new SELECT id, name, price FROM tb_goods")
conn.execute("DROP TABLE tb_goods")
conn.execute("ALTER TABLE tb_goods_new RENAME TO tb_goods")

实跑输出:

--------------------------------------------------------------------
第十一课:表结构那点事,一条 ALTER 与三步重建
--------------------------------------------------------------------

  **3.3 节讲在图形工具里建表、改表、删表。**
  **建表最麻烦的不是写 SQL,是想清楚每列的类型和约束。**
  **这一课真建一张表,再改它五次,看哪些改动 MySQL 一条语句就够、SQLite 得绕路。**

  一、教材原码:建表与改表
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | CREATE TABLE tb_goods (
  |     id INT NOT NULL AUTO_INCREMENT,
  |     name VARCHAR(50) NOT NULL,
  |     price DECIMAL(8,2) NOT NULL DEFAULT 0.00,
  |     PRIMARY KEY (id)
  | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  | ALTER TABLE tb_goods ADD COLUMN stock INT DEFAULT 0;
  | ALTER TABLE tb_goods MODIFY COLUMN name VARCHAR(100) NOT NULL;
  | ALTER TABLE tb_goods CHANGE COLUMN stock qty INT DEFAULT 0;
  | ALTER TABLE tb_goods DROP COLUMN qty;
  | RENAME TABLE tb_goods TO tb_product;
  | DROP TABLE tb_product;
    (建表、加列、改列定义、改列名、删列、改表名、删表)


  二、同样的事,在 SQLite 里能做几件
  --------------------------------------------------------------------
  mysql> CREATE TABLE tb_goods (id INTEGER PRIMARY KEY,
         name TEXT NOT NULL, price REAL NOT NULL DEFAULT 0.00)
  **建表成功。**注意 AUTO_INCREMENT 换成了 `INTEGER PRIMARY KEY`,
  **`DECIMAL` 换成了 `REAL`——这一处会在第 6 章细讲,先记住它不是等价替换。**

  mysql> ALTER TABLE tb_goods ADD COLUMN stock INTEGER DEFAULT 0

  mysql> ALTER TABLE tb_goods RENAME COLUMN stock TO qty

  mysql> ALTER TABLE tb_goods DROP COLUMN qty

  mysql> ALTER TABLE tb_goods ADD COLUMN memo TEXT

  三、两种引擎,一件事的代价不一样
  --------------------------------------------------------------------

  改动      SQLite 的写法                  结果
  --------------------------------------------------------------------------
  加一列    ALTER TABLE ... ADD COLUMN     一条就行
  改列名    ALTER TABLE ... RENAME COLUMN  一条就行
  删一列    ALTER TABLE ... DROP COLUMN    一条就行
  改列类型  ALTER COLUMN ... TYPE          不支持:near "TYPE": syntax error
  加约束    ADD CONSTRAINT                 一条就行
  **三件能做,两件不能。**
  **不能的那两件,MySQL 一条 `ALTER` 就完,SQLite 得「建新表、拷数据、换名字」。**

  四、绕路的写法:三步重建
  --------------------------------------------------------------------
  **三步走完:建新表、拷进 0 行、删旧表、改名。**
  SELECT sql
  FROM sqlite_master
  WHERE name = 'tb_goods'
  CREATE TABLE "tb_goods" (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL
  DEFAULT 0.00, memo VARCHAR(20))
  **注意这个过程里表「消失过几毫秒」。**
  **大表上拷贝要几分钟,那几分钟里谁都查不了这张表。**
  **MySQL 8.0 的在线 DDL 让大部分 `ALTER` 不用锁表,这是它比 SQLite 更适合生产的一个具体原因。**

第一段那条 CREATE TABLE 值得逐字看:AUTO_INCREMENT 换成了 INTEGER PRIMARY KEY。 这不是换个名字,是换了机制——SQLite 的 INTEGER PRIMARY KEY 就是 rowid 的别名。 DECIMAL(8,2) 换成 REAL 更要注意。 REAL 是浮点,存 0.1 会存成 0.1000000000000000055…。 钱用浮点存,迟早要对不上账(第 6 章会专门讲)。 第二段那五行里,前三行成功、后两行报错。 报错的两行在 MySQL 上都是一条语句的事,这就是两个引擎的差距。 第三段是绕路写法:建新表、拷数据、删旧表、改名。 注意中间那几毫秒里表是「不存在」的。 大表上拷贝要几分钟,那几分钟谁都查不了这张表,而 MySQL 8.0 的在线 DDL 就是为解决这个来的。

30 秒实验

  1. 把 Demo 里那段绕路写法改成「先建新表、拷完数据再删旧表」,调一下顺序。 你会发现最后那次改名必须放到最后——名字冲突,同时只能存在一张叫 tb_goods 的表。
  2. 试着给 price 加一个 CHECK (price >= 0),看 SQLite 怎么拒绝。 它不支持后加约束,只能重建表。 MySQL 8.0 支持 ADD CONSTRAINT,但会扫全表检查已有数据。
  3. 想一想:一亿行的表要加一列,为什么 MySQL 8.0 能瞬间完成? 因为它只在表的元数据里记一笔「这列默认值是多少」,不碰已有行。 但如果你给的默认值是「随机数」或者「当前时间」,它就不得不重写了。

常见误区

  1. 「TRUNCATE 能回滚,因为它只是一条语句。」 不能。TRUNCATE 是 DDL,执行前隐式提交,事务包不住它。
  2. 「DELETE FROM 表 和 TRUNCATE TABLE 表 是一回事。」 差很远:DELETE 逐行删、能回滚、不重置自增、大表上还慢。 TRUNCATE 直接丢弃数据页,快,但不可回滚。
  3. 「改字段类型就是把定义改一下,很快。」 只有「放宽」的时候才快。 VARCHAR(50) 改 VARCHAR(100) 多数瞬间完成,VARCHAR(100) 改 VARCHAR(50) 要检查并可能截断, 通常要重写整表。
自测三问(点开对答案) 1. `TRUNCATE` 和 `DROP` 差在哪? —— **`TRUNCATE` 只清数据,表结构留着;`DROP` 连表一起删。** **两个都不可回滚。** 2. `DELETE FROM 表` 和 `TRUNCATE TABLE 表` 该用哪个? —— **要能回滚、或者带条件删一部分,用 `DELETE`。** **要清空整张表、还要快和重置自增,用 `TRUNCATE`。** 3. 给一亿行的表加一个带默认值的列,为什么能瞬间完成? —— **因为 MySQL 8.0 只在元数据里记「新列的默认值」,不重写已有行。** **但如果默认值是随机数或当前时间,就必须逐行算,那就慢。**

下一篇往最里一层:表里的数据怎么增删改查。 重点不在语法,在「WHERE 写错会动多少行」这件事上。

相关笔记