前言
为什么你需要 UPDATE FROM?
在日常开发中,我们经常会遇到这样的场景:需要根据另一张表的数据来更新当前表的字段。新手通常的第一反应是写一个子查询:
UPDATE user_info
SET dept_name = (SELECT dept_name FROM dept WHERE id = user_info.dept_id);
这个写法虽然能工作,但存在两个致命问题:
-
性能灾难:子查询会为每一行都执行一次,当数据量达到万级、十万级时,更新速度会急剧下降
-
功能局限:当需要根据聚合结果(如
SUM、COUNT)来更新时,子查询写法会变得非常臃肿甚至难以实现
而 UPDATE FROM 语法正是为了解决这两个痛点而生的。它采用集合操作的思路:先通过 JOIN 将目标表和来源表关联成一张临时结果集,然后一次性完成更新。无论是一对一的关联更新,还是需要先 GROUP BY 聚合再更新,UPDATE FROM 都能以清晰、高效的 SQL 完成。
PostgreSQL、SQL Server 等数据库原生支持这种语法,而 MySQL 用户则需要用多表更新的方式来实现类似效果。这不是固定的语法,而是一种通用的解决思路。
语法差异
📌 语法差异说明 :
PostgreSQL / SQL Server / SQLite 使用 UPDATE ... FROM ... 语法法
MySQL 使用 UPDATE ... JOIN ... SET ... 语法
本文以 PostgreSQL 语法为主,MySQL 用户可将 FROM 子句改写为 JOIN,将 WHERE 关联条件移到 ON 中。。
UPDATE FROM
基础UPDATE语法:
sql
-- update sql结构
UPDATE table_name
SET column1 = value1, column2 = value2, ...
WHERE condition;
UPDATE FROM 用法:
sql
UPDATE 目标表 t
SET 字段 = 来源表.字段
FROM 来源表
WHERE t.id = 来源表.id;
PS: UPDATE的主表永远是目标表 t。
demo的表结构
sql
create table user_info(
id bigint,
name varchar(50),
dept_id bigint,
dept_name varchar(50)
);
create table dept(
id bigint,
dept_name varchar(50)
);
关联更新
sql
update user_info u
set dept_name = d.dept_name
from dept d
where u.dept_id = d.id
执行逻辑 :
- 先查询
select * from user_info u join dept d on u.dept_id = d.id;得到结果 - 再把
d.dept_name更新到u.dept_name
子查询聚合后更新
sql
update sale_order s
set total_amount = t.amount
from (
select order_id,
sum(qty * price) as amount
from order_item
group by order_id
) t
where s.id = t.order_id;
使用场景:
- 更新库存余额
- 更新总金额
- 更新已发数量
- 更新已入库数量
- 订单明细sale_order主表;order_item
多字段同时更新
sql
update sale_order s -- 常见用法
set total_qty = t.qty,
total_amount = t.amount,
item_count = t.cnt
from (
select order_id,
sum(qty) as qty,
sum(amount) as amount,
count(*) as cnt
from order_item
group by order_id
) t
where s.id = t.order_id;
使用 WITH + UPDATE FROM
sql
with tmp as ( -- 进阶用法
select order_id,
sum(amount) as total_amount
from order_item
group by order_id
)
update sale_order s
set total_amount = t.total_amount
from tmp t
where s.id = t.order_id;
递归结果更新
sql
with recursive tree as (
select id,parent_id,1 as level
from dept
where parent_id is null
union all
select d.id,d.parent_id,t.level + 1
from dept d
join tree t on d.parent_id = t.id
)
update dept d
set dept_level = t.level
from tree t
where d.id = t.id;
批量 CASE WHEN 更新 vs UPDATE FROM
sql
update test
set status =
case
when id = 1 then 'A'
when id = 2 then 'B'
end;
update test t -- 推荐下面这种写法,适用于 Java 批量更新、导入 Excel 或临时修正数据。据
set status = x.status
from (
values
(1,'A'),
(2,'B')
) as x(id,status)
where t.id = x.id;
UPDATE FROM + EXISTS
sql
update stock s
set status = 'EMPTY'
from (
select item_id
from stock
group by item_id
having sum(qty) <= 0
) t
where s.item_id = t.item_id;
- 避免重复 提高安全性
最危险的问题
多对一更新问题,
sql
update a
set name = b.name
from b
where a.id = b.id;
-- 正确做法 from 后面的结果唯一
-- group by
select id,max(name)
group by id
-- distinct on
select distinct on(id)
id,name
from b
order by id,create_time desc
总结
| 场景 | 推荐写法 | 核心要点 |
|---|---|---|
| 普通关联更新 | UPDATE ... FROM ... WHERE |
确保关联键唯一,避免多对一覆盖 |
| 聚合统计更新 | 子查询 GROUP BY + UPDATE FROM |
企业系统中最常见的模式 |
| 多字段同时更新 | 子查询返回多个聚合字段 | 一次扫描,多处更新 |
| 复杂逻辑更新 | WITH CTE + UPDATE FROM |
逻辑清晰,易于维护 |
| 批量定点更新 | VALUES() 构造临时表 |
Java 批量导入、Excel 修正数据的最佳实践 |
| 安全检查 | 加 EXISTS 或提前去重 |
防止误更新,提高健壮性 |
注意事项
⚠️关联键的唯一性 :当 FROM 子句中的来源表有多条记录匹配目标表的一条记录时,最终更新结果是不确定的(取决于最后匹配到的哪一行)。务必使用 GROUP BY、DISTINCT ON 或窗口函数确保来源表的关联键唯一。
适用场景
- 数据同步:从维度表更新事实表的冗余字段
- 统计回写:订单汇总金额写回主表、库存余额汇总
- 批量修正:Excel 导入临时数据批量更新
- 递归更新:组织架构层级、BOM 成本分摊
UPDATE FROM 是相对高级更新技巧,它能让你的代码更高效、更简洁。