加列可以,而且能带默认值,老行会自动补上。
但「非空且无默认值」的列加不上,因为老行没法填。
加 CHECK 可以(ADD CONSTRAINT 名字 CHECK (…)),但它会立刻校验老数据,
有脏数据这条 ALTER 就直接失败。 加 UNIQUE 和 PRIMARY KEY 这里不行。
加不上的只能重建表:建新表 → 搬数据 → 删旧表 → 改名。
搬数据时那句 COALESCE 就是把「脏数据怎么办」这个决定显式写下来。
前九篇讲的都是「建表时怎么写」,这一篇讲「表已经跑起来了,怎么补」。 这是现实中最常见的情况: 表是两年前建的,现在要加一条约束。 而「补约束」比「一开始就写对」麻烦得多。
一、加列:可以,而且能带默认值
ALTER TABLE 表 ADD COLUMN 列 类型 默认值,
老行会一次性补上那个默认值。 Demo 里加了 status INTEGER NOT NULL DEFAULT 0,老行拿到 0。
注意这里三个属性一起写都没问题:非空 + 默认值。
因为默认值给了「老行的这一列填什么」一个答案。
反过来,加「非空且无默认值」的列会直接失败,
报 Cannot add a NOT NULL column with default value NULL。
所以给老表加列,默认值几乎必写。
二、加 CHECK:可以,而且立刻校验老数据
ALTER TABLE 表 ADD CONSTRAINT 名字 CHECK (条件) 是支持的,约束名会写进表定义,
以后报错就带这个名字。
但它会立刻拿老数据去校验一遍——表里要是有不满足的数据,这条 ALTER 直接失败,约束加不上。
报错是 constraint failed,不带细节。 要先自己查一遍有多少行违规:
SELECT COUNT(*) FROM 表 WHERE NOT (条件),
查清楚再改,别硬试。 「先清数据、再加约束」,顺序不能反。
三、加 UNIQUE 和 PRIMARY KEY:这里不行
ADD UNIQUE (列) 报 near "UNIQUE"、ADD PRIMARY KEY (列) 报 near "PRIMARY"。
SQLite 的 ALTER TABLE 能力很有限,只能改名、加列、删列、改列名、加 CHECK。
MySQL 全都能做:ADD UNIQUE (列)、ADD PRIMARY KEY (列)、MODIFY 列 类型 NOT NULL、
ADD CONSTRAINT 外键 …,一条语句一个动作。
所以教材里这些语法在真 MySQL 上都能跑,在这边得换路。
这条差异要记住:不是「教材错了」,是「本沙箱能力少」。
四、加不上的,只能重建表
四步:建新表(结构写对)→ 搬数据 → 删旧表 → 改名。 没有捷径,但每一步都能验证。
Demo 里第一行走不通:
INSERT INTO t_new SELECT id, name FROM t_old 直接报 NOT NULL constraint failed,
因为老数据里有一行 name 是 NULL。
第二行加了 COALESCE(name, '未填') 就过了。 这一句不是语法技巧,是业务决定:
「没填名字的人,算作『未填』」——这个决定必须有人做,数据库不会替你猜。
把它显式写在 SQL 里,比在脑子里想一遍强得多。
四步走完,表名没变、数据都在、约束也加上了。 但整个过程要在事务里做(第 18 章),
否则中间失败会留下一张半成品。
五、重建表时必须想到的四件事
一是索引会丢。 旧表上的索引不会跟到新表,
重建完要一条条补回来。 先 PRAGMA index_list 看一眼有几个,别凭记忆。
二是外键会断。 别的表指过来的外键,在删旧表那一步会失败(外键拦着),
得先关外键开关再重建。 这一步在真 MySQL 上更麻烦,还要考虑别的连接。
三是数据量大时很慢。 insert into … select 是全量搬,
百万行级别可能要几分钟到几十分钟,期间业务读写都受影响。
所以这类操作要挑低峰期,或者分批搬。
四是自增值。 新表的自增会从 1 重新开始,
如果老表已经涨到几十万,新表的号会不连续但不会冲突——
只要搬数据时把 id 一起搬过去,就没事。 不搬 id 才是真出事:所有引用都会指错人。
六、能不动就别动
这一课看完最该得到的结论,不是「怎么改」,是「尽量别改」。
建表时多想十分钟,能省掉后面几小时的重建。
真到非改不可,两条纪律:一是先在副本上跑一遍(第 8 章那个 WHERE 1 = 0 的招);
二是备份再动手——CREATE TABLE 备份 AS SELECT * FROM 原表 一行就够,代价几乎为零。
再加一条:改动的时间点要挑。 业务高峰做 ALTER,等于给自己造一次故障;
放在凌晨或者发版窗口,同样的操作风险小一个量级。
七、动手 Demo:加列、加 CHECK、撞两次方言,最后重建一次表
Demo 五段:加一列带默认值、加一条带名字的 CHECK、撞 ADD UNIQUE 和 ADD PRIMARY KEY、
然后完整走一遍「重建表」的四步,中间还撞一次脏数据。
最后那段重建是这一篇的核心——它会先失败一次,让你看清「脏数据怎么办」这个决定绕不过去。
import _q46 as Q
c = Q.blank()
Q.run(c, "CREATE TABLE demo_up (id INTEGER PRIMARY KEY, name TEXT)")
# 加列:能带默认值
Q.run(c, "ALTER TABLE demo_up ADD COLUMN status INTEGER NOT NULL DEFAULT 0")
# 加 CHECK:支持,而且立刻校验老数据
Q.run(c, "ALTER TABLE demo_up ADD CONSTRAINT ck_id CHECK (id > 0)")
# 加 UNIQUE 和 PRIMARY KEY:这里不行
c.execute("ALTER TABLE demo_up ADD UNIQUE (name)")
c.execute("ALTER TABLE demo_up ADD PRIMARY KEY (name)")
# 加不上就重建:建新表 -> 搬数据 -> 删旧表 -> 改名
Q.run(c, "CREATE TABLE t_new (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE)")
Q.run(c, "INSERT INTO t_new SELECT id, COALESCE(name, '未填') FROM t_old")
Q.run(c, "DROP TABLE t_old")
Q.run(c, "ALTER TABLE t_new RENAME TO t_old")
实跑输出:
--------------------------------------------------------------------
第五十一课:给已有的表加约束
--------------------------------------------------------------------
加列:可以,而且能带默认值
SQL> CREATE TABLE demo_up (id INTEGER PRIMARY KEY, name TEXT)
完成
SQL> INSERT INTO demo_up (id, name) VALUES (1, '甲')
影响 1 行
SQL> ALTER TABLE demo_up ADD COLUMN status INTEGER NOT NULL DEFAULT 0
完成
SQL> SELECT id, name, status FROM demo_up
id name status
-----------------------
1 甲 0
(1 行)
**老行自动补上了 0。** **但注意**:**加「非空且无默认值」的列会失败**,
**因为老行没法填。** 报错是 `Cannot add a NOT NULL column with default value NULL`。
加 CHECK:可以,而且立刻校验老数据
SQL> ALTER TABLE demo_up ADD CONSTRAINT ck_id CHECK (id > 0)
完成
SQL> SELECT sql FROM sqlite_master WHERE name = 'demo_up'
CREATE TABLE demo_up (
id INTEGER PRIMARY KEY,
name TEXT,
status INTEGER NOT NULL DEFAULT 0,
CONSTRAINT ck_id CHECK (id > 0)
)
**`ADD CONSTRAINT 名字 CHECK (…)` 是支持的**,**约束名会写进表定义**,以后报错就带这个名字。
**但它会立刻拿老数据去校验一遍**——
**表里要是有不满足的数据,这条 `ALTER` 直接失败,约束加不上。**
**所以「先清数据、再加约束」,顺序不能反。**
加 UNIQUE 和 PRIMARY KEY:这里不行
SQL> ALTER TABLE demo_up ADD UNIQUE (name)
报错:near "UNIQUE": syntax error
SQL> ALTER TABLE demo_up ADD PRIMARY KEY (name)
报错:near "PRIMARY": syntax error
**两条都报 `near "UNIQUE"`/`near "PRIMARY"`。** **SQLite 的 `ALTER TABLE` 能力很有限**,
**只能改名、加列、删列、改名、加 CHECK。**
**MySQL 全都能做**:`ALTER TABLE 表 ADD UNIQUE (列)`、`ADD PRIMARY KEY (列)`、
`MODIFY 列 类型 NOT NULL`、`ADD CONSTRAINT 外键 …`,**一条语句一个动作。**
加不上的,只能重建表
**重建表四步**:
**建新表(结构写对)→ 搬数据 → 删旧表 → 改名。** **这四步没有捷径,但每一步都能验证。**
SQL> CREATE TABLE t_old (id INTEGER PRIMARY KEY, name TEXT)
完成
SQL> INSERT INTO t_old (id, name) VALUES (1, NULL)
影响 1 行
SQL> INSERT INTO t_old (id, name) VALUES (2, '乙')
影响 1 行
SQL> SELECT id, name FROM t_old ORDER BY id
id name
------------
1 NULL
2 乙
(2 行)
**现在想给 `name` 加「非空 + 唯一」,直接 `alter` 做不到。**
**而且第一行的 `name` 是 NULL,搬过去会撞非空。**
SQL> CREATE TABLE t_new (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE)
完成
**先试着原样搬一遍**,看会报什么:
SQL> INSERT INTO t_new SELECT id, name FROM t_old
报错:NOT NULL constraint failed: t_new.name
**`NOT NULL constraint failed`。** 说明脏数据真的挡住了。
**第二步:先把脏数据补成合法值,再搬。**
SQL> INSERT INTO t_new SELECT id, COALESCE(name, '未填') FROM t_old
影响 2 行
SQL> SELECT id, name FROM t_new ORDER BY id
id name
------------
1 未填
2 乙
(2 行)
SQL> DROP TABLE t_old
完成
SQL> ALTER TABLE t_new RENAME TO t_old
完成
SQL> SELECT id, name FROM t_old ORDER BY id
id name
------------
1 未填
2 乙
(2 行)
**四步走完,表名没变、数据都在、约束也加上了。** **`COALESCE(name, '未填')` 那一句是这次的关键**
——**它替你把「脏数据怎么办」这个业务决定显式写了下来。**
重建表时必须想到的四件事
**一是索引会丢**:旧表上的索引不会跟到新表,
**重建完要一条条补回来。** **先 `PRAGMA index_list` 看一眼有几个,别凭记忆。**
**二是外键会断**:别的表指过来的外键,**在删旧表那一步会失败**(外键拦着),
**得先关外键开关再重建。** **这一步在真 MySQL 上更麻烦,还要考虑别的连接。**
**三是数据量大时很慢**:`insert into … select` 是全量搬,
**百万行级别可能要几分钟到几十分钟,期间业务读写都受影响。**
**四是自增值**:新表的自增会从 1 重新开始,
**如果老表已经涨到几十万,新表的号会不连续但不会冲突**——
**只要搬数据时把 `id` 一起搬过去,就没事。**
能不动就别动
**这一课看完最该得到的结论,不是「怎么改」,是「尽量别改」。**
**建表时多想十分钟,能省掉后面几小时的重建。**
**真到非改不可,两条纪律**:**一是先在副本上跑一遍**(第 8 章讲的 `WHERE 1 = 0`);
**二是备份再动手**——**`CREATE TABLE 备份 AS SELECT * FROM 原表` 一行就够,代价几乎为零。**
第一段加列,老行补上 status = 0。
第二段加 CHECK 成功,建表语句里多了一行 CONSTRAINT ck_id CHECK (id > 0)。
第三段两条都报语法错:near "UNIQUE"、near "PRIMARY"。
第四段重建表:先原样搬,报 NOT NULL constraint failed: t_new.name;
加 COALESCE 之后影响 2 行,顺利搬完。
第五段没有 SQL,讲重建表必须想到的四件事和「能不动就别动」。
30 秒实验
- 给一张有数据的表加
status INTEGER NOT NULL(不给默认值)会怎样? - 表里已经有一行不满足
CHECK (id > 0),现在ADD CONSTRAINT会怎样? - 重建表时忘了搬
id,会产生什么后果?
常见误区
- 以为
ALTER TABLE什么都能加。 这个沙箱加不了 UNIQUE 和 PRIMARY KEY,得重建表。 - 有脏数据时直接
ADD CONSTRAINT。 会直接失败,该先查清违规行数、 定好怎么处理(改掉还是删掉),再动手。 - 在原表上直接改,不备份也不在副本上试。 一行
CREATE TABLE 备份 AS SELECT *就能留后路, 没有理由不做。
自测三问(点开对答案)
1. 给老表加列时,为什么默认值几乎必写? —— 老行需要一个填充值。不给默认值又要求非空,`ALTER` 会直接失败。 2. 加 CHECK 为什么可能失败? —— 它会立刻拿老数据校验一遍,有违规行就直接失败,约束加不上。要先处理数据。 3. 重建表的四步是什么?最容易漏的是哪一步? —— 建新表、搬数据、删旧表、改名。最容易漏的是搬数据时没搬 `id`,或者没把索引、外键补回来。第 9 章到这儿完了: 默认值、非空、自增、检查、唯一、主键、外键、注释,九个属性都会用了。 下一篇进第 10 章——多表查询,把两张表连起来看。
- ☐ (本篇完,下一篇 10.1 多表查询简介)