2116 字
约 7 分钟
17
S11-A26 · `UPDATE ... JOIN`:批量改数为什么"改多了"
2024-12-31
2026-11-11

一次"按订单状态回填"的操作写成 UPDATE orders o JOIN refunds r ON o.id = r.order_id SET o.status = 'refunded'——执行完"退款单里 3 条重复记录"把"1 个订单"改了三次(结果一样但"影响行数"是 3),更糟的是另一处:UPDATE ... JOIN user u ON o.user_id = u.id SET o.region = u.region 把"10 万订单"改成了"2 万"——因为 u.id 不唯一(账号合并的脏数据)。

UPDATE ... JOIN 是"批量改数"的利器,而它有三个陷阱:一对多(重复命中)、关联键不唯一(改错范围)、以及JOIN 条件不完整(笛卡尔).

1. "三个陷阱"的机制

陷阱 机制
一对多 左表一行"匹配右表多行"
关联键不唯一 改成"最后一次匹配的值"
条件不完整 变成"交叉连接"(行数爆炸)

这个"不报错"的机制是"逐行匹配、逐行覆盖":"同一订单被 3 个 user 命中"时,region 被写 3 次——而"最终值"取决于"引擎扫描顺序",两次执行可能不同.
第二项"最隐蔽":
"值被覆盖"不报错**——只是"最终值"取决于"匹配顺序"(不确定).

2. "影响行数"的三种读法

读法 含义
ROW_COUNT() "被扫描并尝试修改"的行数
SELECT COUNT(*) 先算 "实际会改多少"(推荐)
EXPLAIN "改的范围"(是否走索引)

第一项会"高估"(一对多时"同一行算多次")这个"高估"会"误导验收":"影响 3 行"时你以为"改了 3 个订单",而实际"只有 1 个"——**于是"对账"会用错基线.
——**于是"看到影响 3 行"不代表"改了 3 行".

"先算会改多少行"只要两条 SQL,而它"能挡掉九成事故":

-- 两条"预演"
SELECT COUNT(*) FROM orders o JOIN refunds r ON o.id = r.order_id;      -- 含重复:3000
SELECT COUNT(DISTINCT o.id) FROM orders o JOIN refunds r ON o.id = r.order_id; -- 实际:1000
-- 判据:两个数"不等" → 存在"一对多",必须 DISTINCT 或窗口

3. "安全批量改数"的三步

步骤 做法
1 先"SELECT"出"目标行数与主键集" 确认范围
2 用"主键 IN 子查询"改 避免一对多
3 用"事务 + 备份表"兜底 可回滚

这个"DISTINCT"的收益很直接:"3 条退款单 → 1 个订单 id"——于是"影响行数"与"实际改动"重新对齐.
第二步是"关键":
UPDATE orders SET status='refunded' WHERE id IN (SELECT DISTINCT order_id FROM refunds)——"DISTINCT"把"一对多"压成"唯一"**.

"窗口取一条"的写法三行,而它把"不确定"变成"确定":

UPDATE orders o JOIN (
  SELECT user_id, region,
         ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC) rn
  FROM user) u ON o.user_id = u.user_id AND u.rn = 1
SET o.region = u.region;
-- 判据:每个 user_id 只贡献 1 行,基数确定

4. "关联键不唯一"的三种处理

处理 说明
先"清洗右侧"(去重) 最稳(要"定规则")
用"窗口函数取一条"(ROW_NUMBER) 精确(要"排序依据")
加"唯一约束" 治本(要改表)

第二项是"可解释"的做法:**"每个 user_id 取'最新一条'" **——**于是"取哪条"由"显式规则"决定,而不是"引擎顺序".

"分批提交"要"按主键区间",否则"每批的查询本身"就是"全表扫":

-- 分批:按主键区间切,每批 5000
UPDATE orders SET status='archived'
WHERE id BETWEEN 1 AND 5000 AND status='closed';
-- 循环:id 区间 + 5000,直到"影响行数 = 0"
-- 判据:每批"用主键范围"而不是"LIMIT"(LIMIT 在 UPDATE 里语义不同)

5. "事务与备份"的三种做法

做法 说明
先"建备份表"(CREATE TABLE ... AS SELECT) 最稳
事务 + 分批提交 大批量必须
binlog 回滚 需"工具+DBA"

第一项的"配套"是"记录'改了哪些主键'":"备份表 + 主键清单"——于是"回滚"只需"按主键回写",而不是"整表还原".
第一项只有一句:
CREATE TABLE orders_bak_20261111 AS SELECT * FROM orders WHERE ...**——**而它"在改错时"是最快的救命绳.

"改少了"要先"看未匹配",而"LEFT JOIN 数一遍"就能看出:

-- 反向检查:有多少订单"没匹配到"
SELECT COUNT(*) FROM orders o
LEFT JOIN refunds r ON o.id = r.order_id
WHERE r.order_id IS NULL;
-- 判据:这个数"远超预期" → 关联条件太严(如多了 AND r.status='ok')

6. 排错:五种"批量改数问题"

现象 成因 处理方向
改多了 关联键不唯一 先去重/窗口
改少了 关联条件太严 查"未匹配"
影响行数"虚高" 一对多 用 COUNT 先算
卡死/锁表 一次改太多 分批 + 索引
改错无法回 无备份 建备份表

"备份表"要"只备份'将被改的行'",否则"备份本身"就是一次"大 IO":

CREATE TABLE orders_bak_20261111 AS
SELECT * FROM orders WHERE status='closed' AND id BETWEEN 1 AND 5000;
-- 判据:备份表"行数 = 本次要改的行数"

7. 边界:什么情况"一条 SQL 改全量"可以

场景 为什么 做法
表很小(< 1 万行) 锁影响可控 一条 SQL
低峰维护窗口 无人在用 一条 SQL
条件"只按主键/索引" 不加锁扫全表 一条 SQL
有"备份 + 可回滚" 风险可控 一条 SQL

判断口径一句话:"这条 UPDATE 的'关联键'是不是'唯一且非空'"。是(一对一),UPDATE ... JOIN 很安全;不是(一对多/有空值),就必须"DISTINCT/窗口 + 备份表 + 分批"——而"批量改数"的事故,几乎都能归到"没先算'会改多少行'。

8. 自测

  1. 三个陷阱分别是什么机制?哪一个"最隐蔽"?
  2. ROW_COUNT() 为什么会"高估"?"实际会改多少"该怎么算?
  3. "安全三步"里,第二步为什么要用"主键 IN 子查询"?
  4. "关联键不唯一"时,"窗口函数取一条"为什么"可解释"?
  5. "备份表"为什么是"最快的救命绳"?
  6. 判断"能否一条 SQL 改全量"的那个问题是什么?

补一句"流程"建议:"批量改数"应当"两人复核"——"写 SQL 的人"与"跑 SQL 的人"分开,且"跑之前把'两条预演 SQL'的结果贴出来";因为这类操作"不可撤销性"最强,而"复核"的成本只有"五分钟"。

再补一个"认知"要点:"UPDATE ... JOIN 的语义是'逐行匹配后写',而不是'按集合写'"——于是"只要右表'一对多',左表那行就会被'写多次'(值可能不同)。这与"INSERT ... SELECT"的思维不同,而"把它当'集合操作'想"的人,正是"改多了/改错了"的高发人群。想清楚"逐行"这件事,"DISTINCT + 窗口 + 备份"就会变成"自然动作",而不是"额外的谨慎"。

而"自然动作"这四个字,正是"把事故变成流程"的关键。

再补一条"线上"经验:"改数的窗口"要"在低峰且'有回滚时间'"——"周五下午改 10 万行"这类决定,通常"出事时没人值班";而"改完立刻核对'三条预演 SQL'的结果",能"在五分钟内"判断"要不要回滚"。

而"五分钟能判断"这件事,只有"提前备好预演 SQL"才成立。

而"提前"这两个字,也解释了为什么"改数"失败往往"不是技术问题"。

而是"流程问题",而流程"改一次就够了"。

9. 一句话总结

UPDATE ... JOIN 的风险全在"关联的基数":一对一安全,一对多会"重复命中",而"关联键不唯一"会"改成不确定的值"。于是"批量改数"的固定动作是:先 SELECT COUNT 确认范围 → 用 DISTINCT/窗口把基数压成 1 → 建备份表 → 分批提交。而"直接一条 SQL 上生产",是把"能不能回滚"交给了运气。

S11-A26 · `UPDATE ... JOIN`:批量改数为什么"改多了"
https://www.clxhxhhr.top/posts/6713/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。