🛢️ 第6章 数据表操作
📄 一句话概括

「删」有三种: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 秒实验

  1. 把 DELETE FROM tb_log 改成 DELETE FROM tb_log WHERE id < 50000,看文件和计数器的变化。 文件还是不变,计数器还是停在 100000。 删一部分和全删,在这两项上没有区别。
  2. 在事务里跑 BEGIN; DELETE FROM tb_log; ROLLBACK;,看行数回来没有。 回来了。这就是 DELETE 能救的原因。 把 DELETE 换成 DROP TABLE 再试一次——回滚救不回来。
  3. 想一想:一张表 5000 万行、带 3 个索引,DELETE FROM 大概要多久? 要按「每行都要改 4 棵 B 树」来估,比空表慢一个数量级以上,可能几十分钟到几小时。 这种量级该用「建新表留需要的行,再换名」。

常见误区

  1. 「DROP TABLE 写在事务里能回滚。」 不能。它是 DDL,执行前隐式提交,事务包不住。 BEGIN; DROP TABLE t; ROLLBACK; 救不回来。
  2. 「删完数据磁盘空间就回来了。」 DELETE 只是标记页可重用。 要回收得跑 OPTIMIZE TABLE(或 VACUUM 这类等价动作)。
  3. 「TRUNCATE 和 DELETE 全删是一样的。」 结果一样,代价差很多:一个逐行删、能回滚、不重置自增;一个换表、不能回滚、自增归零。
  4. 「DELETE 忘了写 WHERE,赶紧 ROLLBACK 就行。」 前提是你还在事务里、而且没提交。 很多客户端默认自动提交,DELETE 一执行就落盘了,这时候只能靠备份。
自测三问(点开对答案) 1. `DELETE`、`TRUNCATE`、`DROP` 三者,哪个能回滚? —— **只有 `DELETE`。** **另两个是 DDL,隐式提交,事务包不住。** 2. 为什么 `DELETE` 全删之后磁盘没变? —— **它只把页标记成「可重用」,不缩文件。** **要回收得重建表(`OPTIMIZE TABLE`)。** 3. 线上要清空一张日志表,但表结构要留着,选哪个? —— **`TRUNCATE`。快、回收空间、自增归零。** **前提是不需要回滚、也不靠触发器记日志。**

第 6 章到这儿结束。下一篇进第 7 章,讲 MySQL 基础—— 表和库都会建了,接下来该往里写数据、算数据,从运算符和流程控制开始。

相关笔记