🪟 第5章 数据表操作
📄 一句话概括

INSERT 一定要写列名:不写就依赖列顺序,表一加列这条语句就废了。 UPDATE 和 DELETE 忘写 WHERE,影响的是全表;演示里一次误操作把平均分从 80 拉到 0。 DELETE 逐行记日志能回滚,TRUNCATE 只记页的释放所以快,但不支持 WHERE。

📝 本章和前面几章的关系

表建好了,接下来就是往里放数据、改数据、删数据。 这一节是三对关键字:INSERT、UPDATE、DELETE。 教材 5.3 的三小节按这个顺序排, 每一节都配了「在 SSMS 里操作」和「用语句操作」两种做法。实际项目里只用语句—— 界面点一行改一行,一万行要点到什么时候。 演示里真跑了一次「不带 WHERE 的 UPDATE」,平均分从 80 直接掉到 0, 再用 SAVEPOINT 回滚回来。这一段的目的不是教语法,是让你看见代价。

一、INSERT:三种写法,差在速度

写法 长什么样 什么时候用
单行 VALUES (值1, 值2) 手工补一条
多行 VALUES (...), (...), (...) 一次插几行
插入查询 INSERT INTO ... SELECT 从别的表搬

单行和多行的差别不只是写法,是往返次数。 每条 INSERT 一次网络往返、一次事务提交; 一次插一千行,往返一千次。这就是为什么批量导数据要拼多行 VALUES。 INSERT INTO 表 SELECT ... 是从别的表搬数据。 它不写 VALUES,值来自那条 SELECT。 列数、类型、顺序都要对上,对不上报的是「列名或数目与表定义不匹配」。 一定要写列名。 INSERT INTO tb VALUES (...) 依赖列的定义顺序——表一加列,这条语句要么报错, 要么把值插到错误的列里。后者更危险,因为不报错。

二、UPDATE:忘写 WHERE 的代价

演示里跑了一次:先正常加 5 分,再执行一次不带 WHERE 的 UPDATE, 平均分从 80 出头直接变成 0,因为全表的分数都被改成了 0。

UPDATE tb_score SET score = score + 5 WHERE sno = '2023001'   -- 影响 3 行
UPDATE tb_score SET score = 0                                -- 影响全表

两行只差一个 WHERE,后果差一个量级。 而且第二行不会有任何提示—— SSMS 只会告诉你「3 行受影响」或者「6 行受影响」,不会问「你确定要改全表吗」。 所以生产库上的操作顺序是:先用同样的 WHERE 跑一遍 SELECT COUNT(*), 数对了再改成 UPDATE。 这一步只花三秒,能省掉一次事故。 演示里能回滚,是因为我加了 SAVEPOINT。 真在 SSMS 里点「执行」, 如果没开显式事务(BEGIN TRAN),那一秒就自动提交了——回滚不了。 还有一个更隐蔽的坑:UPDATE 里用子查询。 比如「把每个学生的总分更新到学生表」, 子查询写错关联条件,结果可能是「全表被更新成同一个值」,而且不报错。

三、DELETE 与 TRUNCATE

对比项 DELETE TRUNCATE
删什么 符合条件的行 所有行
WHERE 支持 不支持
记日志 每行都记 只记页的释放
自增列 不重置 重置成种子值
有外键引用 可以删 不能删

DELETE 的日志开销是它的代价,也是它的保险。 因为每行都记了,所以能回滚; TRUNCATE 只记「这几页被释放了」,快得多,但你不能只删一部分。 TRUNCATE 会重置自增列。 清空之后下一条插入的 id 又回到 1。 如果业务上靠 id 唯一性做判断(比如订单号),这一步会让历史订单号被复用——很危险。 SQLite 没有 TRUNCATE。 等效做法是 DELETE FROM 表 不带 WHERE, 但它的行为跟 DELETE 一样,不会重置自增列。这是两者的一处实际差异。

四、三种语句的影响范围对照

语句 改什么 最怕的失误
INSERT 加行 不写列名,值串位
UPDATE 改行 忘 WHERE,全表被改
DELETE 删行 忘 WHERE,全表被删

三种语句的风险不对等。 INSERT 出错通常报错(列数不匹配),顶多插错一行; UPDATE 和 DELETE 出错不报错,而且一次影响全表。 DELETE 比 UPDATE 更狠。 改错了还能再改回来(如果记得原值),删错了要么有备份、要么就没了。 所以生产库上删数据,先 SELECT 一遍。 这三条语句合起来叫 DML(数据操作语言),跟 CREATE/ALTER/DROP(DDL)是两类。区别在于: DML 能回滚,DDL 在很多情况下不能。这也是为什么删表比删行危险。

五、动手 Demo:一次不带 WHERE 的 UPDATE

这一段建一张成绩表,用三种写法插六行,然后正常改一次、再故意做一次不带 WHERE 的改, 看平均分怎么从 80 掉到 0,最后用 SAVEPOINT 回滚回来。

import sqlite3

c = sqlite3.connect(u":memory:")
c.execute(u"CREATE TABLE tb_score (id INTEGER PRIMARY KEY AUTOINCREMENT,"
          u" sno TEXT NOT NULL, subject TEXT NOT NULL, score INT)")
c.executemany(u"INSERT INTO tb_score (sno, subject, score) VALUES (?,?,?)",
              [('2023001', '语文', 88), ('2023001', '数学', 92),
               ('2023001', '英语', 79), ('2023002', '语文', 95),
               ('2023002', '数学', 61), ('2023003', '语文', 73)])

# 正常的 UPDATE,带 WHERE
c.execute(u"UPDATE tb_score SET score = score + 5 WHERE sno = '2023001'")
print(u"平均分 %.2f" % c.execute(u"SELECT AVG(score) FROM tb_score").fetchone()[0])

# 故意忘写 WHERE
c.execute(u"SAVEPOINT sp1")
c.execute(u"UPDATE tb_score SET score = 0")
print(u"忘写 WHERE 之后平均分 %.2f"
      % c.execute(u"SELECT AVG(score) FROM tb_score").fetchone()[0])
c.execute(u"ROLLBACK TO sp1")
print(u"回滚之后平均分 %.2f"
      % c.execute(u"SELECT AVG(score) FROM tb_score").fetchone()[0])

实跑输出:

--------------------------------------------------------------------
INSERT / UPDATE / DELETE
--------------------------------------------------------------------


  ① 三种插入写法
  --------------------------------------------------------------------
  单行插两次、批量插一次,一共 6 行
  批量插比逐条插快得多:一次网络往返、一次事务提交。
  教材里那种一条一条的 INSERT,实际项目里基本看不到——几万条要跑几分钟。

  ② UPDATE 忘写 WHERE 会怎样
  --------------------------------------------------------------------
  加了 WHERE,影响 3 行(2023001 的三科都加了 5 分)
  现在 85 分以上的有 3 行
  再执行一次不带 WHERE 的 UPDATE:
    平均分从 83.83 变成 0.00,改动行数 6
    回滚之后平均分回到 83.83
  这就是为什么生产库上跑 UPDATE 要先 SELECT 一遍同样的 WHERE。
  演示里能回滚,是因为我加了 SAVEPOINT。真在 SSMS 里点了「执行」,
  没开显式事务的话,那一秒就提交了——回滚不了。

  ③ DELETE 和 TRUNCATE 的差别
  --------------------------------------------------------------------
| 对比项 | DELETE | TRUNCATE |
|---|---|---|
| 删什么 | 符合条件的行 | 所有行 |
| WHERE | 支持 | 不支持 |
| 记日志 | 每行都记 | 只记页的释放 |
| 能回滚吗 | 能 | 能,但更快 |
| 自增列 | 不重置 | 重置成种子值 |

  DELETE 掉 70 分以下的,剩 5 行
  DELETE 掉全部,剩 0 行,表还在
  TRUNCATE 在 SQLite 里没有对应语句,等效做法是 DELETE FROM 不带 WHERE。
  SQL Server 的 TRUNCATE 快,是因为它不逐行记日志,只记「这几页被释放了」。
  代价是它不能带 WHERE,也不能在有外键引用的时候用。

  ④ 三种语句的影响范围
  --------------------------------------------------------------------
| 语句 | 改什么 | 最怕的失误 |
|---|---|---|
| INSERT | 加行 | 列顺序写错,值串位 |
| UPDATE | 改行 | 忘写 WHERE,全表被改 |
| DELETE | 删行 | 忘写 WHERE,全表被删 |

  INSERT 的坑是「不写列名」——INSERT INTO tb VALUES (...) 依赖列顺序,
  表结构一改(加了一列),这条语句就废了,而且报的错未必指向真正的原因。
  UPDATE 和 DELETE 的坑是同一个:忘写 WHERE。这两个的代价不对等,DELETE 更狠。

第一段六个值里三行是同一个学生的三科——这就是「一对多」在数据上的样子,学生表里一条记录, 成绩表里三条。 第二段的平均分是 83.83。 六个分数是 93、97、84、95、61、73,加起来 503,除 6 得 83.83。 这个数不重要,重要的是它下面那个 0。 第三段就是这一节要演的事:一条不带 WHERE 的 UPDATE,把全表 6 行都改成了 0,平均分变 0。 而 SQLite 只回了一句「6 行受影响」,没有任何警告。 第四段的回滚能成功,全靠那个 SAVEPOINT。 真在 SSMS 里点执行、没开 BEGIN TRAN, 那一秒就自动提交了——这就是为什么生产库上要先 SELECT 再 UPDATE。

30 秒实验

  1. 建一张测试表,插十行,然后执行 UPDATE 表名 SET 某列 = 某值(不带 WHERE), 看 SSMS 报「10 行受影响」的时候,你会不会有一秒的迟疑。
  2. 写一条 INSERT INTO 表名 VALUES (...)(不写列名),然后给表加一列,再跑一次。 看它报什么错——这就是「一定要写列名」的理由。
  3. 想一想:为什么 TRUNCATE 比 DELETE 快,却不能用在大表的部分删除上? 因为它不逐行记日志,也不支持 WHERE。 它只能「整表清空」这一种用法。

常见误区

  1. 「UPDATE 之前不用 SELECT,直接改就行」。改错一次可能就找不回来了。 先用同样的 WHERE 跑 SELECT COUNT(*),数对了再改成 UPDATE。 这三秒钟是性价比最高的一步。
  2. 「DELETE 和 TRUNCATE 都能删数据,随便用哪个」。 TRUNCATE 会重置自增列、不能带 WHERE、不能有外键引用。用错场景的后果不是慢, 是「删多了」或者「id 被复用」。
  3. 「INSERT 不写列名省事」。表一加列,这条语句要么报错、要么把值插错列。 后者不报错,更难发现。写列名多打十几个字,换的是「表结构改了这句还能跑」。
自测三问(点开对答案) 1. `UPDATE` 忘写 `WHERE` 会怎样? —— **全表的行都会被改。** 而且不会有任何警告,只告诉你「N 行受影响」。 **如果没开显式事务,这一秒就提交了,回滚不了。** 所以先 `SELECT` 数一遍行数。 2. `TRUNCATE` 和 `DELETE` 三个关键区别是什么? —— **一、`TRUNCATE` 不能带 `WHERE`;二、它重置自增列;三、它不能用在有外键引用的表上。** 快是它的优点,但这三条限制决定了它只能用于「清空整表」。 3. 为什么 `INSERT` 一定要写列名? —— **因为不写就依赖列的定义顺序。** 表一加列,原来的语句要么报「列数不匹配」, 要么把值插进错误的列——**后者不报错,数据就悄悄错了。**

下一篇 5.4 讲约束。 前面几节都在讲「怎么写」,这一节讲「怎么不许写坏数据」——主键、外键、CHECK、DEFAULT、 UNIQUE 五种,各有各的拦法。

相关笔记