更新一条记录之后马上想知道它变成了什么样,这是数据库开发里非常高频的需求。比如把库存数量减一之后要拿到剩余库存、把订单状态改成已支付之后要返回更新时间,传统做法是先执行UPDATE,再执行一次SELECT。这两步操作不仅浪费一次网络往返,在高并发场景下还可能读到别的会话修改过的数据。RETURNING子句正是为解决这个问题而生的,它让UPDATE语句在执行的同时把受影响行的字段值直接返回,一条语句完成更新和读取。

RETURNING子句的基本语法与工作原理
RETURNING的核心思想很简单:UPDATE语句执行完毕后,数据库会把每一条被更新行的指定字段返回给客户端,效果类似于在这些行上执行了一次SELECT。语法结构如下:
UPDATE products SET stock = stock - 1 WHERE id = 1001 RETURNING id, stock, updated_at;
这条语句执行后,结果集中会包含一行数据,分别是商品的编号、扣减之后的库存值以及更新时间。注意RETURNING返回的是更新之后的新值,如果想同时拿到旧值做对比,PostgreSQL还支持引用旧数据:
UPDATE products
SET price = price * 0.8
WHERE category = 'book'
RETURNING id,
price AS new_price,
(SELECT price FROM products p_old WHERE p_old.id = products.id) AS old_price;从原理上讲,RETURNING是在行级更新发生的同一事务上下文中立即取值的,所以它读到的永远是本次更新写入的真实值,不存在并发窗口问题。这一点和先UPDATE再SELECT的两段式写法有本质区别,后者两次查询之间行数据可能已经被其他事务改写。另外RETURNING返回的是结果集,如果WHERE条件命中了多行,客户端就会收到多行结果,这在进行批量更新结果分析时特别有用。
不同数据库对RETURNING的支持情况
RETURNING并不是SQL标准的一部分,但主流数据库大多提供了类似能力,只是写法上有差异。PostgreSQL是最早支持也是支持最完整的,RETURNING后面可以跟任意表达式、函数调用甚至子查询。SQLite从3.35版本开始也支持RETURNING语法,用法和PostgreSQL基本一致。Oracle则使用RETURNING...INTO子句,把值写入PL/SQL变量,通常需要借助存储过程或在程序中绑定输出参数。
比较遗憾的是MySQL和MariaDB至今不支持RETURNING。MySQL用户的替代方案有几种:一是使用MySQL特有的连接协议特性,执行UPDATE后立即用LAST_INSERT_ID()配合会话级变量取值,但只适用于自增主键;二是把更新逻辑改成原子性更强的UPDATE加SELECT放在同一个事务里,配合行锁保证一致性;三是改用存储过程封装。下面这个Oracle的例子展示了RETURNING INTO的典型写法:
DECLARE
v_new_stock INT;
BEGIN
UPDATE products
SET stock = stock - 1
WHERE id = 1001
RETURNING stock INTO v_new_stock;
DBMS_OUTPUT.PUT_LINE('更新后库存: ' || v_new_stock);
END;各数据库支持情况可以简单归纳为:PostgreSQL全功能支持,SQLite 3.35以上支持,Oracle用RETURNING INTO,SQL Server只能在INSERT场景用OUTPUT子句,UPDATE场景需借助OUTPUT deleted或inserted临时表实现类似效果。跨数据库项目中如果要用这个特性,务必先确认目标库的版本和语法细节。
在应用程序中的实际用法示例
以Python配合PostgreSQL为例,psycopg2执行带RETURNING的语句后,可以直接用fetchone()拿到返回结果,和普通SELECT完全一样:
import psycopg2
conn = psycopg2.connect("dbname=shop user=postgres")
cur = conn.cursor()
cur.execute(
"UPDATE products SET stock = stock - %s "
"WHERE id = %s AND stock >= %s "
"RETURNING stock",
(1, 1001, 1)
)
row = cur.fetchone()
if row is None:
print("库存不足或商品不存在")
else:
print("扣减成功,剩余库存:", row[0])
conn.commit()
cur.close()
conn.close()这段代码还有一个隐藏的好处:WHERE条件里加了stock >= 1的判断,如果库存不足,UPDATE不会命中任何行,fetchone()返回None。这样一条语句同时完成了库存校验、扣减和结果回取三个动作,天然是原子的,不需要额外的SELECT FOR UPDATE加锁,性能也比悲观锁方案好得多。
再看看Go语言的写法。Go的database/sql标准库用QueryRow或Query执行即可,取决于预期命中一行还是多行:
var newStock int
err := db.QueryRow(
"UPDATE products SET stock = stock - 1 WHERE id = $1 RETURNING stock",
1001,
).Scan(&newStock)
if err == sql.ErrNoRows {
fmt.Println("库存不足")
return
}
if err != nil {
log.Fatal(err)
}
fmt.Println("剩余库存:", newStock)批量更新场景下RETURNING同样好用。比如给某个部门所有员工加薪百分之十,需要生成一份调薪清单,直接在RETURNING里返回员工姓名和新旧工资,一条SQL就能产出报表数据,避免了先更新再按条件查一遍的重复劳动,也保证了清单和实际落库数据严格一致。
使用RETURNING的注意事项
第一个要注意的点是把RETURNING和触发器结合时的行为。如果表上有BEFORE触发器修改了字段值,RETURNING返回的是触发器执行之后的最终值,而不是UPDATE语句字面上设置的值,这在排查数据不一致问题时容易让人困惑。第二个注意点是性能,RETURNING每行都要回传数据,如果一次更新几十万行并全部返回,网络开销会非常可观,大批量ETL任务建议只在需要结果集的场景使用。
第三个坑是事务语义。RETURNING返回值并不意味着已经提交,如果后续事务回滚了,客户端拿到的那些值实际上并没有落库。所以拿到返回值后如果还要继续用它去更新其他表,一定要保证这些操作在同一个事务内完成,否则可能出现主表回滚、从表却已更新的脏数据。最后提醒一点,部分ORM对RETURNING的封装有限制,比如Django ORM在较新版本才支持returning参数,MyBatis则可以直接在mapper里写原生SQL配合resultMap使用,遇到框架不支持时退回手写SQL是最稳妥的选择。
UPDATE RETURNINGSQL更新后值PostgreSQL RETURNING修改时间:2026-09-03 04:42:42