「删」有三种:DELETE 删数据、TRUNCATE 清空并重置、DROP 连结构一起删。
只有 DELETE 能回滚,也只有它删完不回收磁盘。
线上删数据前先 BEGIN,看一眼行数,再 COMMIT——这三步救过很多人。
6.5 讲怎么多一张表,这一篇讲怎么少一张。第 6 章到这儿结束。 它和第 4 章的「删库」是同一层意思:DDL 都没有回收站。 这一篇的三者对照表,在第 20 章讲备份恢复时会当成「误删能不能救」的判断依据。
一、DROP TABLE:结构和数据一起没
它删的是「表」这个对象,不是「表里的行」。 所以结构、数据、索引、约束一起消失,磁盘文件也一起没。
DROP TABLE tb_log;
DROP TABLE IF EXISTS tb_log;
IF EXISTS 的作用是「表不存在也不报错」。
它适合清理脚本,不适合手工操作——手工操作时报错是好事,能提醒你「删错名字了」。
DROP 是 DDL,隐式提交,事务包不住。
所以在事务里写 BEGIN; DROP TABLE t; ROLLBACK; 是救不回来的,DROP 那一刻已经提交了。
这一点和 DELETE 完全不同,也是误删事故里最常见的认知差。
二、DELETE FROM:只删数据
它删的是行,不是表。所以能加 WHERE、能回滚、能只删一部分。
DELETE FROM tb_log; -- 全删
DELETE FROM tb_log WHERE id < 1000; -- 删一部分
它有三个特点,都来自「逐行删」这件事:慢、能回滚、不回收磁盘。
慢是因为每一行都要记日志、都要维护索引;能回滚是因为日志;
不回收磁盘是因为它只是把页标记成「可重用」。
所以「删了 100 万行,磁盘怎么没空出来」这个问题的答案是:要再跑一次回收。
MySQL 里对应 OPTIMIZE TABLE,本质是把表重写一遍。
还有一条:DELETE 不带 WHERE 就是全删。
它和 TRUNCATE 的结果看起来一样,但代价完全不同——一个是逐行,一个是换表。
三、TRUNCATE:清空并重置
TRUNCATE TABLE 表 在 MySQL 里等于「DROP 掉,再按原结构重建一张空表」。
所以它不是 DML,是 DDL。
| 特性 | DELETE |
TRUNCATE |
|---|---|---|
| 是不是逐行 | 是 | 否,直接重建 |
| 能不能回滚 | 能 | 不能 |
| 自增计数器 | 不重置 | 归零 |
| 触发触发器 | 会 | 不会 |
| 磁盘空间 | 不回收 | 回收 |
五行里最容易忽略的是第四行:TRUNCATE 不会触发 DELETE 触发器。
如果业务靠触发器记审计日志,TRUNCATE 会让日志一条不留,这属于「静默的数据丢失」。
第三行「归零」也有两面。
好处是表重新开始、号不浪费;坏处是旧号可能被重新发一遍,如果那些号在别处被引用过就撞了。
所以对外的单号不能用自增,这一条在第 6.1 节说过一次。
四、三者怎么选
按「要删什么」和「删错了能不能救」两个问题走。
| 操作 | 删什么 | 能回滚 | 自增 | 文件缩 |
|---|---|---|---|---|
DELETE FROM |
数据(可带条件) | 能 | 不重置 | 不缩 |
TRUNCATE |
全部数据 | 不能 | 归零 | 缩 |
DROP TABLE |
数据+结构 | 不能 | 没了 | 缩 |
第一行是唯一「能删一部分」的,所以它也是唯一「能救」的。
后两行都是「一次到底」,区别只在结构留不留。
实践上的选择顺序是:
① 要删一部分 → 只能 DELETE;② 要清空但留表 → TRUNCATE;③ 表不要了 → DROP。
还有一个不在表里的操作:DELETE 之后想回收空间,得 OPTIMIZE TABLE。
它同样是重建,同样要两倍磁盘,同样要窗口。
五、删表之前的三步确认
这一节教材没讲,但它比语法重要。
第一步:确认删的是哪张表。
SHOW CREATE TABLE 表名 看一眼结构,别只看名字——名字像的表往往有好几张。
第二步:量一下要删多少。
SELECT COUNT(*) FROM 表 拿到行数,再想想这个数合不合理。
「应该几十行,结果三百万」,这时候停手就还来得及。
第三步:能先改名就先改名。
RENAME TABLE 表 TO 表_del_20261030,跑一周没人喊再真删。
改名是秒回的、可撤销的,删不是。
如果是 DELETE,还有第四步:包在事务里。
BEGIN; DELETE ...; SELECT COUNT(*)...; ROLLBACK; 先跑一遍看行数对不对,确认了再 COMMIT。
这三行命令能拦住绝大多数「忘写 WHERE」的事故。
六、动手 Demo:三种「删」,代价与后果实测
这一课造一张 10 万行的日志表,然后分别用三种方式删,量耗时、文件大小、自增计数器。
沙箱里没有 TRUNCATE,等价动作是「删表再按原结构建回来」。
其中有一项实测结果和教材的预期相反,那一项我会照实写。
# 造 10 万行
c.execute("CREATE TABLE tb_log (id INTEGER PRIMARY KEY AUTOINCREMENT, v TEXT)")
c.executemany("INSERT INTO tb_log (v) VALUES (?)", [(u'line-%d' % i,) for i in range(100000)])
# 一、DELETE:计时,量文件
t0 = time.perf_counter(); c.execute("DELETE FROM tb_log"); c.commit()
print(time.perf_counter() - t0, os.path.getsize(dbfile))
# 二、VACUUM:回收空间
c.execute("VACUUM")
print(os.path.getsize(dbfile))
# 三、TRUNCATE 的等价动作:DROP + CREATE
ddl = c.execute("SELECT sql FROM sqlite_master WHERE name = 'tb_log'").fetchone()[0]
c.execute("DROP TABLE tb_log"); c.execute(ddl); c.commit()
实跑输出:
--------------------------------------------------------------------
第三十课:三种「删」,代价差一个数量级
--------------------------------------------------------------------
**教材 6.6 讲 `DROP TABLE`。但「删」这个字对应三种操作,代价完全不同。**
**`DROP` 删结构带数据,`DELETE` 只删数据,`TRUNCATE` 清数据留结构。**
**这一课拿 10 万行真量一遍:耗时、文件大小、自增计数器。**
**先造一张 10 万行的日志表,文件 1848.0 KB,自增计数到 100000。**
一、DELETE FROM:逐行删,能回滚,文件不缩
--------------------------------------------------------------------
项 删之前 DELETE 之后
----------------------------------
行数 100000 0
文件大小 1848.0 KB 1848.0 KB
自增计数器 100000 100000
耗时 — 0.014 秒
**四行里最反直觉的是第二行:文件一点没缩。**
**`DELETE` 只是把每一行标记成「可重用」,页还在文件里。**
**所以「删了 100 万行,磁盘怎么没空出来」这个问题,答案是:要再跑一次 `VACUUM`。**
**它是把整个文件重写一遍,代价等于把表拷一次。**
项 DELETE 之后 VACUUM 之后
----------------------------------
文件大小 1848.0 KB 12.0 KB
耗时 0.014 秒 0.013 秒
**`VACUUM` 把文件从 1848.0 KB 缩到 12.0 KB,代价是 0.013 秒,而且期间要有两倍磁盘。**
**真 MySQL 里对应的动作是 `OPTIMIZE TABLE`,同样是重建、同样要两倍空间。**
**所以「删了数据要回收空间」这件事,别指望它自动发生。**
二、TRUNCATE:清空并重置,是 DDL 不是 DML
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| TRUNCATE TABLE tb_log;
④ 教材原码,本沙箱不执行
**`TRUNCATE` 在 MySQL 里等于「`DROP` 掉再按原结构重建」,所以它有三条特性:**
**一是快(不逐行删),二是不能回滚(DDL 会隐式提交),三是自增计数器归零。**
特性 `DELETE` `TRUNCATE`
----------------------------------------------------
是不是逐行 是,删多少行就记多少条日志 否,直接重建
能不能回滚 能 不能
自增计数器 不重置 归零
触发触发器 会 不会
**沙箱里没有 `TRUNCATE`,等价动作是「删表再按原结构建回来」。**
**我拿 10 万行真跑了一遍:**
项 结果
----------------------------------------
删 10 万行之后插一行,拿到 id 100001
重建之后再插一行,拿到 id 1
重建耗时 0.0259 秒
对比:DELETE 10 万行耗时 0.014 秒
**第二行是重点:重建之后 id 回到 1,而删数据不会。**
**因为计数器是表的一个属性,表都重建了,属性自然重来。**
**第三行和第四行有点意外:重建用了 0.0259 秒,DELETE 用了 0.014 秒。**
**重建没有更快,反而慢了一点。**
**原因在这个沙箱的实现上:`DELETE` 把 10 万行标记成可重用,一个事务就完了。**
**它既没有撤销日志要写,也没有二级索引要维护;而 DROP 加 CREATE 要改文件结构、要落盘。**
**真 MySQL 上 `TRUNCATE` 的优势来自「不写 undo log、不逐行改索引」,行数越多差距越明显。**
**这一次的差距量不出来,所以我只给方向、不给倍数。**
三、DROP TABLE:结构也一起没
--------------------------------------------------------------------
mysql> DROP TABLE tb_log
时刻 名叫 tb_log 的表有几张
---------------------------------
删之前 1
DROP 之后 0
**结构没了,数据也就无从谈起——数据文件跟着没了。**
**所以三种「删」里只有这一种是真把磁盘空出来的,因为它把整张表扔了。**
**它也是最快、最不可逆的那一个。**
**`DROP TABLE` 在 MySQL 里同样是 DDL,隐式提交,事务包不住,也没有回收站。**
四、三者对照
--------------------------------------------------------------------
操作 删什么 能回滚 自增 文件缩 速度
-------------------------------------------------------
`DELETE FROM` 数据 能 不重置 不缩 慢
`TRUNCATE` 数据 不能 归零 缩 快
`DROP TABLE` 数据+结构 不能 没了 缩 快
**六列里只要记住三列:能回滚、自增、文件缩。**
**这三列决定了「删错了能不能救」和「删完了空间还不还」。**
**还有一个操作不在这张表里但必须提:`DELETE` 忘了写 `WHERE`。**
**它属于第一行,能回滚——前提是你在一个事务里,而且还没提交。**
**所以线上删数据前先 `BEGIN`,再看一眼行数,最后才 `COMMIT`。**
第一段:10 万行删完,行数 0,文件大小一点没变。
DELETE 只是把每一行标记成「可重用」,页还在文件里。
这就是「删了数据磁盘没空出来」的实样。
第二段:VACUUM 把文件缩到十几 KB,代价是一次全文件重写。
真 MySQL 里对应 OPTIMIZE TABLE,同样要两倍磁盘。
第三段是自增那一格,也是这一课最该记住的:删完再插一行拿到 100001,重建之后插一行拿到 1。
因为计数器是表的属性,表换了,属性就重来了。
这里有一项和教材的预期相反:重建耗时 0.025 秒,DELETE 耗时 0.013 秒,重建没有更快。
原因在沙箱的实现上:这里的 DELETE 不写撤销日志、不维护二级索引,一个事务就完了。
真 MySQL 上 TRUNCATE 的优势来自「不写 undo log、不逐行改索引」,行数越多差距越明显。
这一次的差距量不出来,所以我只给方向、不给倍数。
最后一段 DROP TABLE 之后,表从字典里消失,1 张变成 0 张。
这一种才真把磁盘空出来,因为它把整张表扔了。
30 秒实验
- 把
DELETE FROM tb_log改成DELETE FROM tb_log WHERE id < 50000,看文件和计数器的变化。 文件还是不变,计数器还是停在 100000。 删一部分和全删,在这两项上没有区别。 - 在事务里跑
BEGIN; DELETE FROM tb_log; ROLLBACK;,看行数回来没有。 回来了。这就是DELETE能救的原因。 把DELETE换成DROP TABLE再试一次——回滚救不回来。 - 想一想:一张表 5000 万行、带 3 个索引,
DELETE FROM大概要多久? 要按「每行都要改 4 棵 B 树」来估,比空表慢一个数量级以上,可能几十分钟到几小时。 这种量级该用「建新表留需要的行,再换名」。
常见误区
- 「
DROP TABLE写在事务里能回滚。」 不能。它是 DDL,执行前隐式提交,事务包不住。BEGIN; DROP TABLE t; ROLLBACK;救不回来。 - 「删完数据磁盘空间就回来了。」
DELETE只是标记页可重用。 要回收得跑OPTIMIZE TABLE(或VACUUM这类等价动作)。 - 「
TRUNCATE和DELETE全删是一样的。」 结果一样,代价差很多:一个逐行删、能回滚、不重置自增;一个换表、不能回滚、自增归零。 - 「
DELETE忘了写WHERE,赶紧ROLLBACK就行。」 前提是你还在事务里、而且没提交。 很多客户端默认自动提交,DELETE一执行就落盘了,这时候只能靠备份。
自测三问(点开对答案)
1. `DELETE`、`TRUNCATE`、`DROP` 三者,哪个能回滚? —— **只有 `DELETE`。** **另两个是 DDL,隐式提交,事务包不住。** 2. 为什么 `DELETE` 全删之后磁盘没变? —— **它只把页标记成「可重用」,不缩文件。** **要回收得重建表(`OPTIMIZE TABLE`)。** 3. 线上要清空一张日志表,但表结构要留着,选哪个? —— **`TRUNCATE`。快、回收空间、自增归零。** **前提是不需要回滚、也不靠触发器记日志。**第 6 章到这儿结束。下一篇进第 7 章,讲 MySQL 基础—— 表和库都会建了,接下来该往里写数据、算数据,从运算符和流程控制开始。
- ☐ (本篇完,下一篇 7.1 运算符)