🦏 第7章 数据修改
📄 一句话概括

delete from 表 where 条件,删的是「行」不是「列」。 不写 where 就是清空整表。 两种「删不掉」:外键会拦住(一班的四个学生指着它, 报 FOREIGN KEY constraint failed);truncate 在本沙箱没有,那是 MySQL 的语句。 删了自增值不退回去(删掉 id 4,下一个新行拿到 5)。 软删除是把「删」变成「标记」:加一列 deleted,改数据不改结构。

📝 本章和前面几章的关系

上一篇改,这一篇删——三类语句里后果最重的一种。 update 至少还留着行,delete 之后连行都没了。 而且平时觉得「删了就删了」,真出事时往往要的是「那一行原来是什么」。

一、基本写法:删的是行,不是列

delete from 表 where 条件。 Demo 里先插一个「四班」再删掉它,影响 1 行。 为什么挑四班删:原来三个班都有学生指着, 删不动——下一段就撞上。 这也说明「删一行」这件事,并不总是能成。 delete 后面不能跟列名。 删列是 alter table … drop column, 那是改表结构,不是删数据。 这两件事经常被混在一起问。

二、删不掉之一:外键拦着

DELETE FROM classes WHERE id = 1 报 FOREIGN KEY constraint failed。 因为 students 表里有四个学生的 class_id 是 1。 MySQL 那边的报错是 Cannot delete or update a parent row,理由一样。 这就是外键的价值:它不让你留下「指向不存在的班」的学生。 要真想删,两条路:先把那些学生的 class_id 改到别的班, 或者先删掉那些学生。 两条路都得你自己决定数据怎么处理,数据库不替你猜。

三、删不掉之二:truncate 这里没有

delete from 表(不带 where)清空整表,但是逐行删的,每删一行都要记日志,大表很慢。 truncate table 表 是「把表推倒重建」,快得多, 但不可回滚、也不触发触发器。 这是 MySQL 的语句。 本沙箱没有 truncate,报 near "TRUNCATE": syntax error。 在这边清空只能 DELETE FROM 表。 注意清空也受外键约束:classes 有子表指着, 直接清空会被拦,得先清子表再清父表。 truncate 在 MySQL 里倒是会直接失败(有外键引用时不允许)。

四、删了以后,自增值不会退回去

删掉 id = 4,再插一行,拿到的是 5,不是 4。 自增值只增不减,删掉的行不会「让号」。 这是刻意设计的:如果复用刚删掉的 id, 别的地方还留着的旧引用(日志、订单快照、外部系统)就会突然指向一个新对象。 那比空号危险得多。 truncate 会重置自增值(相当于重建表),delete 不会——这一条是两者最实际的区别, 也是「我删了表为什么 id 从 1000 开始」的答案。

五、软删除:把「删」变成「标记」

做法:给表加一列 deleted(默认 0), 「删除」时改成 1,查询时统一加 WHERE deleted = 0。 Demo 里把孙悦那行标成 1,全表还是 12 行, 未删的 11 行。 三个好处:能恢复(改回 0 就行)、历史订单还能查到是谁买的、外键不会断。 一个代价:所有查询都得记得带那个条件, 漏一处,删掉的数据就冒出来了。 常见做法是建一个「只看未删」的视图,让业务层只查视图。 第 14 章讲视图时会回来看这个用法。

六、动手 Demo:删一行、被外键拦、自增不退、软删除

Demo 五段:删一行看效果,撞一次外键,撞一次 truncate,验证自增值不退回,最后做一次软删除。 中间两段都是「删不掉」——它们说明了「删除」在真实库里从来不是自由动作。

import _q46 as Q
c = Q.con()

# 先插一个没有学生的班,再删它
Q.run(c, "INSERT INTO classes (name, teacher, room) "
          "VALUES ('四班', '唐老师', 'B301')")
Q.run(c, "DELETE FROM classes WHERE id = 4")
# 删有学生的班:外键拦住
c.execute("DELETE FROM classes WHERE id = 1")
# truncate:本沙箱没有
c.execute("TRUNCATE TABLE classes")
# 自增值不退回
c3 = Q.con()
Q.run(c3, "INSERT INTO classes (name, teacher) VALUES ('四班', '唐老师')")
Q.run(c3, "DELETE FROM classes WHERE id = 4")
Q.run(c3, "INSERT INTO classes (name, teacher) VALUES ('五班', '何老师')")
# 软删除:加一列当标记
c4 = Q.con()
Q.run(c4, "ALTER TABLE students ADD COLUMN deleted INTEGER "
          "NOT NULL DEFAULT 0")
Q.run(c4, "UPDATE students SET deleted = 1 WHERE id = 12")

实跑输出:

--------------------------------------------------------------------
第三十四课:delete,以及「删不掉的」和「删不干净的」
--------------------------------------------------------------------

  基本写法:where 定范围
  SQL> SELECT id, name FROM classes ORDER BY id
  id     name
  ------------
  1      一班
  2      二班
  3      三班
  (3 行)

  SQL> INSERT INTO classes (name, teacher, room) VALUES ('四班', '唐老师', 'B301')
  影响 1 行

  SQL> DELETE FROM classes WHERE id = 4
  影响 1 行

  SQL> SELECT id, name FROM classes ORDER BY id
  id     name
  ------------
  1      一班
  2      二班
  3      三班
  (3 行)

  **先插了个「四班」,再把它删掉,影响 1 行。**
  **为什么挑它:原来三个班都有学生指着,删不动(下一段就撞)。**
  **`delete from 表 where 条件`**,删的是「行」。
  **`delete` 后面不能跟列名**——它是删行,不是删列。

  外键会拦住你:删不掉的班
  SQL> DELETE FROM classes WHERE id = 1
  报错:FOREIGN KEY constraint failed

  **一班的四个学生还在 `students` 表里指着它,所以删不掉。**
  **MySQL 那边的报错是 `Cannot delete or update a parent row`,理由一样。**
  **这就是外键的价值**:
  **它不让你留下「指向不存在的班」的学生。**
  **要真想删,得先把那些学生的 `class_id` 改掉或者删掉。**

  delete 与 truncate:两个都清空,代价不同
  **`delete from 表`(不带 `where`)清空整表,但它是逐行删的。**
  **`truncate table 表` 是「把表推倒重建」,快得多,但不可回滚、也不触发触发器。**
  **本沙箱没有 `truncate`**,教材原码在 MySQL 上跑:`TRUNCATE TABLE classes`。
  报错:near "TRUNCATE": syntax error

  **在这边要清空,只能 `DELETE FROM 表`。** **注意 `classes` 有外键指着,直接清空也会被拦**——
  **得先清子表再清父表。**

  删了以后,自增值不会退回去
  SQL> INSERT INTO classes (name, teacher) VALUES ('四班', '唐老师')
  影响 1 行

  SQL> SELECT MAX(id) AS max_id FROM classes
  max_id
  -------
  4
  (1 行)

  SQL> DELETE FROM classes WHERE id = 4
  影响 1 行

  SQL> INSERT INTO classes (name, teacher) VALUES ('五班', '何老师')
  影响 1 行

  SQL> SELECT id, name FROM classes ORDER BY id
  id     name
  ------------
  1      一班
  2      二班
  3      三班
  4      五班
  (4 行)

  **新插入的那行拿到的是 5,不是 4。** **自增值只增不减,删掉的行不会「让号」。**
  **这是刻意设计的**:如果复用刚删掉的 id,**别的地方还留着的旧引用就会突然指向一个新对象**,
  那比空号危险得多。
  **`truncate` 会重置自增值**(相当于重建表),**`delete` 不会**——**这一条也是两者最实际的区别。**

  软删除:把「删」变成「标记」
  SQL> ALTER TABLE students ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0
  完成

  SQL> UPDATE students SET deleted = 1 WHERE id = 12
  影响 1 行

  SQL> SELECT COUNT(*) AS all_rows FROM students
  all_rows
  ---------
  12
  (1 行)

  SQL> SELECT COUNT(*) AS alive FROM students WHERE deleted = 0
  alive
  ------
  11
  (1 行)

  **孙悦那行还在表里,只是被标了 `deleted = 1`。**
  **查询时统一加 `where deleted = 0`,看上去就是「删掉了」。**
  **软删除的三个好处**:**能恢复**、**历史订单还能查到是谁买的**、**外键不会断**。
  **代价是所有查询都得记得带那个条件**——
  **漏一处,删掉的数据就冒出来了。** **常见做法是建一个「只看未删」的视图,让业务层只查视图。**

第一段删一行。 先插四班、再删四班,影响 1 行,表回到三条。 第二段撞外键。 删一班报 FOREIGN KEY constraint failed。 第三段撞方言。 TRUNCATE 在这里不存在,报语法错。 第四段自增值。 删掉 id 4 之后新插的那行是 5——号没退回去。 第五段软删除。 加一列 deleted,把孙悦标成 1;全表 12 行,未删 11 行。

30 秒实验

  1. 删掉 students 里 id = 12 的那行,能成功吗?为什么?
  2. DELETE FROM order_detail 与 DELETE FROM orders 谁该先执行?
  3. 软删除之后,SELECT COUNT(*) FROM students 给的是几?为什么?

常见误区

  1. delete 不写 where。 那是清空整表,而且逐行删、很慢,删完还没有撤销。
  2. 以为自增值会补上空号。 删掉 4 之后新行是 5,不是 4; 要重置只能 truncate(MySQL)或者重建表。
  3. 软删除之后忘了带 deleted = 0。 漏一处,删掉的数据就出现在页面上。
自测三问(点开对答案) 1. `delete` 和 `truncate` 有什么区别? —— `delete` 逐行删、可回滚、不重置自增值;`truncate` 重建表、快、不可回滚、重置自增值。 本沙箱没有 `truncate`。 2. 为什么删一班会被拦?怎么才能删掉? —— `students` 里有四个学生指向它,外键拦住了。要先把那些学生的 `class_id` 改掉或删掉那些学生。 3. 软删除的代价是什么? —— 所有查询都得带 `WHERE deleted = 0`,漏一处删掉的数据就会冒出来。

数据这块讲完了:增、改、删都有了。 下一篇换个层级——不再动数据,动「表」本身:建库、建表、改表、删表。

相关笔记