UPDATE 只有一句语法,三种 DML 里它最容易出事故。
因为忘了 WHERE 会把全表改成同一个值,而且不报错。
Rows matched 和 Changed 是两个数:前者是选中的行,后者是真变了的行。
线上改数据四步:BEGIN → UPDATE → 看行数 → COMMIT。
8.1 讲往里塞新行,这一篇讲改已有的行。
它是 UPDATE、DELETE 两条「会伤数据」的语句里的第一条,下一篇的删除更狠。
这一篇的事务自保手法,在每一章涉及改数据的地方都适用。
一、骨架:改哪张表、改成什么、改哪些行
UPDATE 的语法就三行,正好对应三件事。
UPDATE tb_goods
SET price = price * 1.1, updated_at = NOW()
WHERE cat_id = 2;
第一行说改哪张表,第二行说改成什么,第三行说改哪些行。
三条里只有第三行可以不写——而它就是事故的入口。
SET 那行可以改多列,用逗号隔开。
多列同时改有它的意义:它们在同一条语句里完成,要么都改要么都不改。
如果拆成两条 UPDATE,中间失败就会出现「一半改了」的脏状态。
二、WHERE 缺了会怎样
不写 WHERE 的 UPDATE 不是语法错,它会把整张表的每一行都改掉。
而且它不问你、不提示、不报错。
| 写法 | 改了哪些行 | 耗时 |
|---|---|---|
WHERE cat = 2 |
3334 行(三分之一) | 0.0019 秒 |
不带 WHERE |
10000 行(全部) | 0.0033 秒 |
两行的耗时只差一倍不到,因为瓶颈都在「改多少行」。
这就是它危险的地方:从执行结果上看不出区别,只有影响行数不同。
而很多人根本不看影响行数。
客户端打一行 Query OK, 10000 rows affected,扫一眼就过去了。
所以有一个很实用的习惯:改数据之前,先把 UPDATE ... SET 换成 SELECT *,条件一个字不改。
先看选中了哪些行,确认了再改。
三、用表达式改:每一行拿自己的旧值算
SET price = price * 1.1 这句话里有两个 price,它们不是一个东西。
左边是「要写到的新值」,右边是「这一行原来的值」。
因为 UPDATE 是逐行执行的,每一行都拿自己的旧值算新值。
所以一百个商品各自涨 10%,而不是全变成同一个数。
这一点决定了「改成平均值」不能这么写:
-- 错的:报错,聚合函数不能出现在 SET 里
UPDATE tb_goods SET price = AVG(price);
-- 对的:先用子查询算出平均值
UPDATE tb_goods SET price = (SELECT AVG(price) FROM tb_goods);
子查询那种写法还有个坑:SELECT 里查的就是正在被改的这张表。
MySQL 会报 You can't specify target table for update in FROM clause。
绕法是套一层临时表,或者先算出来存到变量里。
四、Rows matched 和 Changed 不是一回事
这是很多人栽过的一个坑:UPDATE 执行成功了,可它说「影响了 0 行」。
不是没执行,是「值本来就等于新值,所以不用改」。
| 指标 | 含义 | 什么情况下会不同 |
|---|---|---|
| Rows matched | 符合 WHERE 的行数 |
总是等于选中的行 |
| Changed | 值真的变了的行数 | 新值等于旧值时,这个数是 0 |
MySQL 客户端会把两个数一起打出来:Rows matched: 10000 Changed: 0。
只看 Changed 会以为语句没生效,然后去查半天权限、连接、锁。
要让它无论如何都算「改了」,可以顺手加一句 SET updated_at = NOW()。
时间戳每次都变,Changed 就永远等于 matched。
这一条还有一个副作用要知道:Changed 少,说明这次 UPDATE 几乎没有产生新版本。
在有索引的列上,这可能意味着「索引也不用更新」——所以它快是应该的,不是坏了。
五、用事务自保:四步流程
UPDATE 是 DML,能回滚——前提是你还没提交。
这一条决定了线上改数据的标准动作。
BEGIN;
UPDATE tb_keep SET price = 0; -- 假装忘了写 WHERE
SELECT SUM(price) FROM tb_keep; -- 看一眼,不对
ROLLBACK; -- 退回去
上面四行真跑出来的数是 5050 变成 0、又变回 5050。
关键是第三行:改完先别提交,先回头看一眼结果对不对。
所以线上改数据的标准动作是这四步:
BEGIN → UPDATE → 看影响行数 → 对了才 COMMIT。
很多客户端默认是自动提交模式,这四步不存在。
UPDATE 一执行就落盘,那时候只能靠 binlog 或备份——而 binlog 恢复是另一门手艺。
所以第一条建议不是「小心点」,是「先确认你的客户端是不是自动提交」。
命令行客户端默认自动提交,图形工具通常有个开关,看一眼再动手。
六、分批改大表
整批改和分批改,耗时未必差,但「事务有多长」差很多。
| 写法 | 10 万行耗时 | 事务 |
|---|---|---|
一条 UPDATE 改完 |
0.038 秒 | 一个大事务 |
| 分 10 批,每批一万行 | 0.142 秒 | 10 个小事务 |
第二行反而慢了几倍,因为每批都要单独提交。
所以分批不是为了快,是为了「不把别人堵住」。
一个大事务会长时间持有行锁、写满 undo log,期间别人改同一张表就得排队。
更糟的情况是撞上 innodb_log_file_size 上限,事务直接失败。
所以大表改数据按「一万到十万行一批」切,是稳妥做法。
分批的写法有两种:按主键区间(WHERE id > 0 AND id <= 10000),
或者按 LIMIT(MySQL 支持 UPDATE ... LIMIT 10000)。
七、动手 Demo:UPDATE:两个影响行数,和一句忘写的 WHERE
这一课真跑一次「忘写 WHERE」,再用事务把自己拦住。
同时把 Rows matched 和 Changed 这两个数摆清楚。
注意沙箱不区分这两个数,所以那张表里有两列,一列是沙箱报的、一列是 MySQL 报的。
# 一、带 WHERE 与不带 WHERE
c.execute("UPDATE tb_goods SET price = price WHERE cat = 2")
c.execute("UPDATE tb_goods SET price = price") # 全表
# 二、表达式改:每一行拿自己的旧值算
c.execute("UPDATE tb_goods SET price = price * 1.1 WHERE cat = 2")
# 三、两个影响行数
base = c.total_changes
c.execute("UPDATE tb_goods SET price = price WHERE cat = 3")
matched = c.total_changes - base # SQLite 只有这一个数
# 四、事务自保:改完先别提交
c.execute("BEGIN")
c.execute("UPDATE tb_keep SET price = 0") # 忘了 WHERE
print(c.execute("SELECT SUM(price) FROM tb_keep").fetchone()) # 0.0
c.execute("ROLLBACK")
# 五、分批改 10 万行
for start in range(0, 100000, 10000):
c.execute("UPDATE tb_big SET v = v + 1 WHERE id > ? AND id <= ?", (start, start + 10000))
c.commit()
实跑输出:
--------------------------------------------------------------------
第三十四课:UPDATE 的两个数,和一句忘写的 WHERE
--------------------------------------------------------------------
**教材 8.2 讲 `UPDATE`。语法只有一句,但它是四种 DML 里最容易出事故的一种。**
**因为 `DELETE` 忘了 `WHERE` 还能靠备份救,`UPDATE` 忘了 `WHERE` 会把全表改成同一个值。**
**这一课真跑一次「忘写 WHERE」,再演示怎么用事务把自己拦住。**
一、骨架:改哪张表、改成什么、改哪些行
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| UPDATE tb_goods
| SET price = price * 1.1, updated_at = NOW()
| WHERE cat_id = 2;
④ 教材原码,本沙箱不执行
**三行对应三件事,缺一不可。**
**`WHERE` 那行是可以不写的,而它就是事故的入口。**
**先造一万行数据,`cat` 分三类(1、2、3),价格 0 到 99。**
**一共 10000 行。**
二、带 WHERE 与不带 WHERE
--------------------------------------------------------------------
写法 改了哪些行 耗时
-----------------------------------------------
`WHERE cat = 2` 3334 行(三分之一) 0.0024 秒
不带 `WHERE` 10000 行(全部) 0.0034 秒
**两行都是「改」,但一行改三分之一,一行改全表。**
**这里我把 `price = price` 写成「值没变」,所以数据不受影响——这是在演示里保护自己。**
**真忘了 `WHERE` 而且写的是 `price = 0`,那一万行就全变 0 了。**
**而且 `UPDATE` 是 DML,能回滚——前提是你还没提交。**
三、改一个表达式:全体涨价 10%
--------------------------------------------------------------------
code price
------------
G00001 1
G00004 4
G00007 7
3 rows in set
code price
------------
G00001 1.1
G00004 4.4
G00007 7.7
3 rows in set
**两张表是同一批数据,改前改后。**
**`SET price = price * 1.1` 里的 `price` 指的是「这一行原来的值」,不是某一行的值。**
**这一点很重要:`UPDATE` 是按行执行的,每一行都拿自己的旧值算新值。**
**所以「把所有价格改成平均值」不能写 `SET price = AVG(price)`——那要用子查询。**
四、Rows matched 和 Changed 不是一回事
--------------------------------------------------------------------
指标 含义 沙箱报的 MySQL 报的
-----------------------------------------------------------
Rows matched 符合 `WHERE` 的行数 3333 3333
Changed 值真的变了的行数 3333(不分) 0
**第三列是沙箱报的,第四列是 MySQL 报的,两者只在 `Changed` 那一行不同。**
**SQLite 不区分这两个数,它把「符合条件」的都算成「影响」;MySQL 区分。**
**这是很多人栽过的一个坑:`UPDATE` 执行成功了,可 MySQL 说 `Changed: 0`。**
**不是没执行,是「值本来就等于新值,所以不用改」。**
**真 MySQL 的客户端会打 `Rows matched: 10000 Changed: 0`,两个数摆在一起就好懂了。**
**只看 `Changed` 会以为语句没生效,然后去查半天权限和连接。**
**要让它无论如何都算「改了」,可以在 `UPDATE` 里加一句 `SET updated_at = NOW()`。**
**时间戳每次都变,`Changed` 就永远等于 `matched`。**
五、忘写 WHERE,用事务把自己拦住
--------------------------------------------------------------------
时刻 price 总和 说明
---------------------------------------------
改之前 5050.0 1 加到 100
UPDATE 之后(未提交) 0.0 全变 0 了
ROLLBACK 之后 5050.0 原样回来了
**三行就是一套完整的自保流程:先看总和、再改、发现不对就回滚。**
**关键是「改完先别提交」——只要没提交,一切都还来得及。**
**所以线上改数据的标准动作是这四步:**
**`BEGIN` → `UPDATE` → 看一眼影响行数对不对 → 对了才 `COMMIT`。**
**自动提交模式下这四步不存在,`UPDATE` 一执行就落盘,那时候只能靠 binlog 或备份。**
六、一次改一万行:分批还是整批
--------------------------------------------------------------------
写法 10 万行耗时 事务大小
----------------------------------------------
一条 UPDATE 改完 0.039 秒 一个大事务
分 10 批,每批一万行 0.145 秒 10 个小事务
**这一格分批反而慢了几倍,因为每批都要单独提交一次。**
**所以分批的意义不在快,在「不把别人堵住」。**
**一个大事务会长时间持锁、写满 undo log,期间别人改同一张表就得等。**
**真 MySQL 里还有个硬指标:事务太大可能撞上 `innodb_log_file_size` 限制,直接报错。**
**所以大表改数据按「一万到十万行一批」切,是稳妥做法。**
第一段两张表:带 WHERE 改 3334 行,不带改 10000 行。
我把 price = price 写成「值没变」,是在演示里保护自己——真忘了 WHERE 写的是 price = 0,
那一万行就全变 0 了。
第二段改前是 1、4、7,改后是 1.1、4.4、7.7。
每一行涨自己的 10%,不是全变成同一个数。
这就是「按行执行」的意思。
第三段那张表是这一课最该看懂的地方。
沙箱把「符合条件」的都算成影响,所以两行都是 3333;MySQL 会分开,Changed 那一行会给 0。
看到 0 不要以为没执行——它只是说「值没变,不用改」。
第四段三行数是 5050、0、5050。
中间那个 0 是「全表被改成 0」的样子,第三个 5050 是回滚之后。
这一套动作建议背下来,它是线上改数据的最低保护。
第五段两行是 0.038 秒对 0.142 秒——分批反而慢。
所以要清楚分批买的是什么:买的是「事务短」,不是「速度快」。
30 秒实验
- 把事务那一段的
ROLLBACK换成COMMIT,再想想怎么恢复。只能靠备份或 binlog。 这就是「提交」这两个字的分量。 - 给
UPDATE加上ORDER BY id DESC LIMIT 100(MySQL 才有),想想什么时候用得上。 「先改最近的一百条」这种场景。 沙箱没有这个语法,等价写法是子查询选 id。 - 跑一次
UPDATE tb_goods SET price = (SELECT AVG(price) FROM tb_goods),看报什么错。 在 MySQL 上会报「不能在 FROM 子句里指定正在更新的表」。 绕法是套一层(SELECT AVG(price) FROM (SELECT price FROM tb_goods) t)。
常见误区
- 「
UPDATE不写WHERE会报错提醒我。」 不会。它会把全表改掉,然后告诉你「成功」。 - 「影响 0 行说明语句没执行。」
是执行了,但新值等于旧值,所以不用改。
看
Rows matched才对。 - 「
UPDATE出错了直接重新跑一遍就行。」 如果那条语句是SET count = count + 1,重跑就多加了一次。 这类语句不幂等,重跑前先确认它有没有执行成功。 - 「分批改比整批改快。」 通常更慢,因为提交次数变多。 分批买的是「事务短、不堵别人」,不是速度。
自测三问(点开对答案)
1. `Rows matched` 和 `Changed` 差在哪? —— **前者是「选中了几行」,后者是「几行的值真的变了」。** **新值等于旧值时,后者是 0 而前者不为 0。** 2. 线上改数据,改完提交之前应该做什么? —— **先查一遍结果对不对,比如求和、看几行、抽查一条。** **确认了再 `COMMIT`,不对就 `ROLLBACK`。** 3. 为什么 `SET price = price * 1.1` 不是把全表改成同一个数? —— **因为 `UPDATE` 逐行执行,右边的 `price` 是「这一行自己的旧值」。** **每一行都按自己的值算,所以各自涨 10%。**8.2 到这儿结束。下一篇 8.3 讲删除数据——
DELETE 和 TRUNCATE 的差别,比 UPDATE 和 INSERT 之间的差别大得多。
- ☐ (本篇完,下一篇 8.3 删除数据)