CREATE TABLE 的三块:列定义、约束、ON 文件组;自增是 IDENTITY(1,1)。
ALTER TABLE 加列删列是秒完成(只改元数据),改列类型要重写整张表。
DROP TABLE 连结构带数据一起删;TRUNCATE 删所有行但保留结构,而且更快。
5.2a 讲界面,这一节讲语句。界面上点得动的,语句都能做;语句能做的,界面未必能。
教材 5.2b 分三小节:CREATE TABLE、ALTER TABLE、DROP TABLE。
这三条是后面所有章节的基础,第 18 到 20 章的项目实战里,建表脚本就是这一节的内容。
演示里把教材的建表语句贴出来(IDENTITY 之类是本沙箱跑不了的部分),再用 SQLite 跑等价写法,
最后演示「改列类型为什么要重建表」。
一、CREATE TABLE:一张商品表
教材给的语句是这样的:
CREATE TABLE tb_goods
(
id INT IDENTITY(1,1) PRIMARY KEY,
gname NVARCHAR(50) NOT NULL,
price DECIMAL(10,2) NOT NULL DEFAULT 0,
stock INT NOT NULL DEFAULT 0,
addtime DATETIME NOT NULL DEFAULT GETDATE()
)
五列里有三列带了 DEFAULT,这就是为什么插入时只给两个值也能成。
addtime 那个 GETDATE() 是函数默认值——每次插入时现算当前时间,不是建表时算一次。
IDENTITY(1,1) 的两个参数是「种子」和「增量」。 种子 1 增量 1 就是 1、2、
3……改成 (100, 10) 就是 100、110、120。这个用法在按区间分段编号时有用。
等价写法在 SQLite 里是 INTEGER PRIMARY KEY AUTOINCREMENT。注意 INTEGER 必须写全,
写 INT 不生效——这是 SQLite 的一个古怪规则,因为只有 INTEGER PRIMARY KEY 才是行号的别名。
DATETIME DEFAULT GETDATE() 在 SQLite 里对应 DEFAULT (datetime('now','localtime'))。
括号不能少:不加括号会被当成字符串字面量,每次插入都是同一个固定时间。
二、ALTER TABLE:三种动作,代价完全不同
| 动作 | SQL Server 写法 | 代价 |
|---|---|---|
| 加列 | ADD 列名 类型 | 秒完成,只改元数据 |
| 删列 | DROP COLUMN 列名 | 秒完成,空间不马上还 |
| 改列类型 | ALTER COLUMN 列名 新类型 | 重写整张表 |
| 加约束 | ADD CONSTRAINT | 要校验已有数据 |
加列快,是因为 SQL Server 只改「表长什么样」的元数据,不碰已有的行。
新列在老行里读出来就是 NULL(或者默认值),不需要写盘。
删列也快,但磁盘空间不会马上还回来。 被删的列还占着页里的位置,要重建聚集索引才真正释放。
所以「删了列磁盘还是满的」是正常现象。
改列类型慢,因为它要重写。 从 int 改成 bigint,每一行都要按新类型重新编码、重新写盘。
一张一千万行的表,这一步可能锁几十分钟。
加约束要校验已有数据。 加一个 CHECK (age >= 0),SQL Server 得扫一遍全表看有没有负数。
大表上这个扫描很慢——所以可以先加约束但标记为「不检查已有数据」,用 WITH NOCHECK,
代价是已有的坏数据不会被发现。
三、改列类型为什么要重建表
演示里在 SQLite 上试了改列类型,它直接不支持。 这不只是 SQLite 的毛病,关系库在这件事上都是同一个套路:
1. 建新表 tb_new,列类型写新的
2. INSERT INTO tb_new SELECT ... FROM tb
3. DROP TABLE tb
4. ALTER TABLE tb_new RENAME TO tb
SQL Server 的 ALTER COLUMN 内部做的也是这件事,只是它替你做了,还带事务。
所以「改列类型慢」不是某个产品的缺陷,是关系库的共同代价——数据要按新格式重排一遍。
这四步里最危险的是第 3 步。 如果第 2 步搬数据时因为类型不兼容丢了一半,第 3 步一删就真没了。
所以手工做这件事,一定要先备份、或者在事务里做。
实践中的做法是「加新列、搬数据、删旧列、改名」。 这样中间任何一步失败都能回退,
而且可以分批搬(一次一万行),不用锁整表。
四、DROP TABLE 与 TRUNCATE
| 对比项 | DROP TABLE | TRUNCATE TABLE |
|---|---|---|
| 删什么 | 表结构和数据全删 | 只删数据,结构留着 |
| 能带 WHERE 吗 | 不能 | 不能 |
| 速度 | 快 | 比 DELETE 快得多 |
| 自增列 | 表没了 | 重置成种子值 |
| 能回滚吗 | 能(在事务里) | 能(在事务里) |
DROP TABLE 之后表就没了。 再查它报的是「对象名无效」,不是「表为空」——这两个错误完全不同,
看到「对象名无效」说明表根本不存在。
TRUNCATE 快是因为它不逐行记日志。 它只在日志里记「这几页被释放了」,
而 DELETE 要给每一行写一条日志。一百万行的表,差距是秒和分钟的区别。
TRUNCATE 有两个限制:不能带 WHERE,不能在有外键引用的时候用。所以它不是「更快的 DELETE」,
是「另一种操作」——清空整表。
DROP 和 DELETE 差一个字母,后果差一个量级。 DELETE 删行能回滚;
DROP 删表,删完连表长什么样都忘了。在正式库上敲之前,看一眼当前连的是哪个库。
五、动手 Demo:建表、加列、删表
这一段贴教材的建表语句,再用 SQLite 跑等价写法:建带自增和默认值的商品表、插四行看默认值怎么补、 加一列、再演示「改列类型只能重建表」和「删表的后果」。
import sqlite3
c = sqlite3.connect(u":memory:")
c.execute(u"""CREATE TABLE tb_goods (
id INTEGER PRIMARY KEY AUTOINCREMENT,
gname TEXT NOT NULL,
price NUMERIC NOT NULL DEFAULT 0,
stock INT NOT NULL DEFAULT 0,
addtime TEXT NOT NULL DEFAULT (datetime('now','localtime')))""")
c.executemany(u"INSERT INTO tb_goods (gname, price, stock) VALUES (?,?,?)",
[('笔记本', 4999.00, 12), ('键盘', 199.00, 40)])
for r in c.execute(u"SELECT id, gname, price, addtime FROM tb_goods"):
print(r)
# 加列:只改元数据,秒完成
c.execute(u"ALTER TABLE tb_goods ADD COLUMN memo TEXT")
print(u"加列后共 %d 列" % len(c.execute(u"PRAGMA table_info(tb_goods)").fetchall()))
# 改列类型:SQLite 只能重建表
c.execute(u"CREATE TABLE g2 (id INTEGER PRIMARY KEY, gname TEXT, price REAL)")
c.execute(u"INSERT INTO g2 SELECT id, gname, CAST(price AS REAL) FROM tb_goods")
print(u"新表搬过来 %d 行" % c.execute(u"SELECT COUNT(*) FROM g2").fetchone()[0])
c.execute(u"DROP TABLE g2")
try:
c.execute(u"SELECT COUNT(*) FROM g2")
except sqlite3.OperationalError as e:
print(u"DROP 之后:", e)
实跑输出:
--------------------------------------------------------------------
CREATE / ALTER / DROP 三件套
--------------------------------------------------------------------
① 教材原码:建一张商品表
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| CREATE TABLE tb_goods
| (
| id INT IDENTITY(1,1) PRIMARY KEY,
| gname NVARCHAR(50) NOT NULL,
| price DECIMAL(10,2) NOT NULL DEFAULT 0,
| stock INT NOT NULL DEFAULT 0,
| addtime DATETIME NOT NULL DEFAULT GETDATE()
| )
关键是那个 IDENTITY(1,1):标识种子 1、增量 1,也就是自增列。
IDENTITY 列不能自己插值,除非先 SET IDENTITY_INSERT tb_goods ON。
本沙箱没有 SQL Server,等价写法是 INTEGER PRIMARY KEY AUTOINCREMENT。
插了四行,id 和 addtime 都是自动来的:
id gname price stock addtime
1 笔记本 4999 12 2026-10-31 02:43:46
2 键盘 199 40 2026-10-31 02:43:46
3 鼠标 89.5 0 2026-10-31 02:43:46
4 显示器 1299 7 2026-10-31 02:43:46
四个值里只给了三个,id 和 addtime 由默认值补上——这就是默认值的用处。
② ALTER TABLE:加列、改列、删列
--------------------------------------------------------------------
| 动作 | SQL Server 写法 | SQLite 支持吗 |
|---|---|---|
| 加列 | ADD 列名 类型 | 支持 |
| 删列 | DROP COLUMN 列名 | 支持(3.35 起) |
| 改列类型 | ALTER COLUMN 列名 新类型 | 不支持,得重建表 |
| 加约束 | ADD CONSTRAINT | 只支持部分 |
加了一列 memo:现在列数 6
给 id=1 那行写上备注,有备注的行数:1
加列是「秒完成」的——SQL Server 只改元数据,不重写数据。
删列也是秒完成,但磁盘空间不会马上还回来,得重建聚集索引才真正释放。
改列类型就麻烦了:SQL Server 要按新类型重写整张表,大表可能锁几十分钟。
③ SQLite 要改列类型,只能重建表
--------------------------------------------------------------------
SQLite 的做法是四步:
1. 建新表 tb_goods_new,列类型写新的
2. INSERT INTO tb_goods_new SELECT ... FROM tb_goods
3. DROP TABLE tb_goods
4. ALTER TABLE tb_goods_new RENAME TO tb_goods
按这四步走前两步,新表里有 4 行
SQL Server 的 ALTER COLUMN 内部做的也是这件事,只是它替你做了,还带事务。
所以「改列类型慢」不是 SQLite 的毛病,是关系库的共同代价——要重写数据。
④ DROP TABLE:删表就是删表和它的数据
--------------------------------------------------------------------
DROP 前 g2 有 4 行,DROP 后:表不存在了
DROP TABLE 连结构带数据一起删,没有回收站。
跟 DELETE 的区别:DELETE 删行,表还在,能回滚;DROP 删表,表都没了。
还有 TRUNCATE TABLE:删所有行但保留结构,比 DELETE 快,因为它不记日志。
第一段插了两行只给了三个值,id 和 addtime 都是自动来的。
注意 addtime 每行都不一样——因为 DEFAULT 是函数,插入时现算,不是建表时算一次。
加列那句是秒完成的,演示里没感觉到耗时,因为它只改元数据。
一千万行的表加一列也是这个速度——前提是列允许 NULL 或者给了默认值。
改列类型那一段是手工做了 SQL Server 内部做的四步。
CAST(price AS REAL) 那一步如果类型不兼容(比如把字符串改成数字),
搬的时候就会失败或者变成 NULL——这就是为什么改类型之前一定要备份。
最后 DROP 之后报的是「no such table」,不是「0 行」。 这两个错误要分清:前者是表没了,
后者是表在但没数据。排查的时候看一眼错误信息就能少走很多弯路。
30 秒实验
- 把演示里那张
tb_goods的建表语句自己写一遍,不看代码。 写不出来的地方,就是没记住的地方——多半是IDENTITY和DEFAULT。 - 在 SSMS 里对一张有数据的表执行
ALTER TABLE 表名 ADD 新列 INT NOT NULL DEFAULT 0, 然后马上查一次全表,看新列在不在。 大表上你会发现它是瞬间完成的。 - 想一想:为什么
TRUNCATE不能带WHERE? 因为它只记「页被释放了」,不逐行记。 一旦要按条件删,就必须逐行判断, 那它的快就没了——所以干脆不支持。
常见误区
- 「加列之后磁盘会变大」。只改元数据,不动数据,磁盘几乎不变。 真正变大是等到你往新列里写值的时候,那时候行要重排、页可能分裂。
- 「
DROP COLUMN之后空间就还回来了」。没有。 被删列还占着页里的位置,要重建聚集索引才释放。所以大表删列之后,磁盘看着没变是正常的。 - 「
TRUNCATE就是快一点的 DELETE」。不是一回事。 它不能带WHERE、不能有外键引用、会重置自增列。用错场景的后果比慢更严重—— 比如以为只删了符合条件的一部分。
自测三问(点开对答案)
1. `IDENTITY(1,1)` 这两个 1 分别是什么? —— **种子和增量。** 种子 1 增量 1 就是 1、2、3;改成 `(100,10)` 就是 100、110、120。 **增量不是「每次都加这么多」,是先加增量得到下一个值**,所以顺序是种子、种子+增量、再+增量。 2. 为什么 `ALTER COLUMN` 改类型会慢? —— **因为它要重写整张表的数据。** 每一行都要按新类型重新编码、重新写盘。 加列只改元数据所以快,改类型要动数据所以慢——**这是两类操作的本质区别。** 3. `DROP TABLE` 之后查询会报什么错? —— **报「对象名无效」或「no such table」,不是「0 行」。**「0 行」说明表在但没数据; 「对象名无效」说明表不存在。**这两个错误的排查方向完全不同。**下一篇 5.3 讲增删改。三种语句各有一个经典失误:INSERT 不写列名、UPDATE 忘写 WHERE、
DELETE 忘写 WHERE——后两个的代价差一个量级。
- ☐ (本篇完,下一篇 5.3 管理数据表)