PostgreSQL 18 正式发布,带来了多项重要改进,其中 RETURNING 子句的增强尤为突出。该特性在 MERGE RETURNING 场景下实现了关键突破,可显著简化应用架构,并提升数据变更追踪能力。
RETURNING 子句的演进
RETURNING 子句长期以来用于在 INSERT、UPDATE、DELETE 操作后返回受影响行的数据,从而避免额外的 SELECT 查询,减少数据库往返次数并提升性能。然而,在 PostgreSQL 18 之前,该子句在功能上存在明显限制,迫使开发实践中采用各种折中方案。
在 PostgreSQL 17 中,首次为 MERGE 语句引入 RETURNING 支持(提交 c649fa24a),这是一次重要进展。MERGE 语句自 PostgreSQL 15 引入,用于在单条语句中完成条件化的 INSERT、UPDATE 或 DELETE 操作,但在缺乏 RETURNING 支持的情况下,无法直观获取实际执行结果。
PostgreSQL 18 的新特性
PostgreSQL 18 通过引入 OLD 与 NEW 别名(提交 80feb727c8,由 Dean Rasheed 提交,Jian He 与 Jeff Davis 评审),将 RETURNING 子句能力提升至新的层级。该增强使 DML 操作期间的数据捕获方式发生了根本性变化。
PostgreSQL 18 之前的限制
在早期版本中,RETURNING 子句在不同语句类型下存在以下差异化限制:
INSERT与UPDATE仅能返回新值或当前值DELETE仅能返回旧值MERGE根据内部实际执行的操作类型(INSERT、UPDATE或DELETE)返回结果
在需要对比更新前后数据、或精确追踪字段变化时,可选方案较为有限,包括:
- 在修改前额外执行
SELECT查询 - 编写复杂的触发器函数
- 在应用层实现变更跟踪逻辑
- 通过系统列(如 xmax)进行间接判断
上述方式普遍增加了实现复杂度与访问延迟,并降低了代码可维护性。
解决方案:OLD 与 NEW 别名
PostgreSQL 18 引入了特殊别名 old 与 new,可在单条语句中同时访问数据的修改前状态与修改后状态。该机制适用于 INSERT、UPDATE、DELETE 以及 MERGE 等全部 DML 操作。
基本语法示例如下:
UPDATE table_name
SET column = new_value
WHERE condition
RETURNING old.column AS old_value, new.column AS new_value;
为避免与现有列名冲突,或在触发器环境中使用,可对别名进行重命名:
UPDATE accounts
SET balance = balance - 50
WHERE account_id = 123
RETURNING WITH (OLD AS previous, NEW AS current)
previous.balance AS old_balance,
current.balance AS new_balance;
MERGE + RETURNING:能力整合
在 PostgreSQL 18 中,MERGE 与 RETURNING 的组合为 Upsert 场景提供了完整能力,可在单条原子操作中同时完成数据写入与变更结果获取。
实践示例:产品库存系统
在产品库存管理场景中,需要从外部数据源同步数据,实现新增产品、更新已有产品,并准确记录每一行的处理结果。
步骤 1:创建数据表
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
product_code VARCHAR(50) UNIQUE NOT NULL,
product_name VARCHAR(200) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock_quantity INTEGER NOT NULL DEFAULT 0,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE product_staging (
product_code VARCHAR(50),
product_name VARCHAR(200),
price DECIMAL(10, 2),
stock_quantity INTEGER
);
步骤 2:插入初始数据
INSERT INTO products (product_code, product_name, price, stock_quantity)
VALUES
('LAPTOP-001', 'Premium Laptop', 999.99, 50),
('MOUSE-001', 'Wireless Mouse', 29.99, 200),
('KEYBOARD-001', 'Mechanical Keyboard', 79.99, 150);
INSERT INTO product_staging (product_code, product_name, price, stock_quantity)
VALUES
('LAPTOP-001', 'Premium Laptop Pro', 1099.99, 45), -- Update existing
('MONITOR-001', '4K Monitor', 399.99, 75), -- New product
('MOUSE-001', 'Wireless Mouse', 29.99, 200); -- No actual change
基础版:搭配 RETURNING 子句的 MERGE 操作
MERGE INTO products p
USING product_staging s ON p.product_code = s.product_code
WHEN MATCHED THEN
UPDATE SET
product_name = s.product_name,
price = s.price,
stock_quantity = s.stock_quantity,
last_updated = CURRENT_TIMESTAMP
WHEN NOT MATCHED THEN
INSERT (product_code, product_name, price, stock_quantity)
VALUES (s.product_code, s.product_name, s.price, s.stock_quantity)
RETURNING
p.product_code,
p.product_name,
merge_action() AS action_performed;
返回结果示例:
product_code | product_name | action_performed
---------------+---------------------+------------------
LAPTOP-001 | Premium Laptop Pro | UPDATE
MONITOR-001 | 4K Monitor | INSERT
MOUSE-001 | Wireless Mouse | UPDATE
进阶版:搭配 OLD 与 NEW 别名的 MERGE 操作
通过 OLD 与 NEW 别名,可同时获取字段的修改前与修改后值,从而实现精细化变更追踪与审计。
以下查询可从受影响行中,同时获取 product_name 与 price 列的修改前旧值和修改后新值。通过为其设置别名(old_name、new_name、old_price、new_price),可便捷对比 MERGE 操作前后的列值变化,为变更追踪与审计日志记录提供支撑。
MERGE INTO products p
USING product_staging s ON p.product_code = s.product_code
WHEN MATCHED THEN
UPDATE SET
product_name = s.product_name,
price = s.price,
stock_quantity = s.stock_quantity,
last_updated = CURRENT_TIMESTAMP
WHEN NOT MATCHED THEN
INSERT (product_code, product_name, price, stock_quantity)
VALUES (s.product_code, s.product_name, s.price, s.stock_quantity)
RETURNING
p.product_code,
merge_action() AS action,
old.product_name AS old_name,
new.product_name AS new_name,
old.price AS old_price,
new.price AS new_price,
old.stock_quantity AS old_stock,
new.stock_quantity AS new_stock,
(old.price IS DISTINCT FROM new.price) AS price_changed,
(old.stock_quantity IS DISTINCT FROM new.stock_quantity) AS stock_changed;
INSERT 场景下旧值为 NULL,而 UPDATE 场景下可完整呈现字段变更情况。
product_code | action | old_name | new_name | old_price | new_price | old_stock | new_stock | price_changed | stock_changed
---------------+--------+-------------------+---------------------+-----------+-----------+-----------+-----------+---------------+--------------
LAPTOP-001 | UPDATE | Premium Laptop | Premium Laptop Pro | 999.99 | 1099.99 | 50 | 45 | t | t
MONITOR-001 | INSERT | NULL | 4K Monitor | NULL | 399.99 | NULL | 75 | NULL | NULL
MOUSE-001 | UPDATE | Wireless Mouse | Wireless Mouse | 29.99 | 29.99 | 200 | 200 | f | f
构建审计日志
借助增强后的 RETURNING 子句,可在不使用触发器的前提下构建完整审计链路。
步骤 1:创建审计表
CREATE TABLE product_audit (
audit_id SERIAL PRIMARY KEY,
product_code VARCHAR(50),
action VARCHAR(10),
old_values JSONB,
new_values JSONB,
changes JSONB,
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
步骤 2:执行带详细审计追踪的 MERGE 操作
WITH merge_results AS (
MERGE INTO products p
USING product_staging s ON p.product_code = s.product_code
WHEN MATCHED THEN
UPDATE SET
product_name = s.product_name,
price = s.price,
stock_quantity = s.stock_quantity,
last_updated = CURRENT_TIMESTAMP
WHEN NOT MATCHED THEN
INSERT (product_code, product_name, price, stock_quantity)
VALUES (s.product_code, s.product_name, s.price, s.stock_quantity)
RETURNING
p.product_code,
merge_action() AS action,
jsonb_build_object(
'name', old.product_name,
'price', old.price,
'stock', old.stock_quantity
) AS old_values,
jsonb_build_object(
'name', new.product_name,
'price', new.price,
'stock', new.stock_quantity
) AS new_values
)
INSERT INTO product_audit (product_code, action, old_values, new_values, changes)
SELECT
product_code,
action,
old_values,
new_values,
CASE
WHEN action = 'INSERT' THEN new_values
WHEN action = 'DELETE' THEN old_values
ELSE (
SELECT jsonb_object_agg(key, value)
FROM jsonb_each(new_values)
WHERE value IS DISTINCT FROM old_values->key
)
END AS changes
FROM merge_results;
步骤 3:查询审计追踪结果
select * from product_audit;
audit_id | product_code | action | old_values |
new_values | changes | changed_at
----------+--------------+--------+---------------------------------------------------------------+-------------
--------------------------------------------------+---------+----------------------------
1 | LAPTOP-001 | UPDATE | {"name": "Premium Laptop Pro", "price": 1099.99, "stock": 45} | {"name": "Pr
emium Laptop Pro", "price": 1099.99, "stock": 45} | | 2025-12-12 16:27:14.760125
2 | MONITOR-001 | UPDATE | {"name": "4K Monitor", "price": 399.99, "stock": 75} | {"name": "4K
Monitor", "price": 399.99, "stock": 75} | | 2025-12-12 16:27:14.760125
3 | MOUSE-001 | UPDATE | {"name": "Wireless Mouse", "price": 29.99, "stock": 200} | {"name": "Wi
reless Mouse", "price": 29.99, "stock": 200} | | 2025-12-12 16:27:14.760125
(3 rows)
示例中通过 CTE 获取 MERGE 结果,并将旧值、新值及差异以 JSONB 形式写入审计表,实现单条原子操作内的数据同步与审计记录生成。
未来展望
PostgreSQL 18 版本的 RETURNING 子句增强特性,是该数据库提升开发友好性、减少复杂替代方案使用的重要举措。单原子操作中同时调用数据新旧值的能力,可简化应用开发中的多种通用实现模式。
该功能在后续版本中或可从以下方向进一步升级:
- 扩展 MERGE 语句能力,新增更多 WHEN 子句,实现更复杂的条件操作.
- 新增聚合功能支持,支持对 RETURNING 子句的返回结果直接进行聚合计算。
- 实现跨表返回,支持在单操作中返回关联表的数据信息。
技术细节与提交记录参考
针对关注技术实现细节的人员,可参考以下信息:
- MERGE RETURNING (PostgreSQL 17):迪恩・拉希德提交,记录编号
c649fa24a。 - OLD/NEW Support (PostgreSQL 18):迪恩・拉希德提交,何健与杰夫・戴维斯评审,记录编号
80feb727c8。 - Discussion Thread:https://postgr.es/m/CAEZATCWx0J0-v=Qjc6gXzR=KtsdvAE7Ow=D=mu50AgOe+pvisQ@mail.gmail.com
该功能的实现涉及多个组件的修改,包括:
- 执行器(execExpr.c、execExprInterp.c、nodeModifyTable.c)
- 解析器(parse_target.c)
- 优化器(createplan.c、setrefs.c、subselect.c)
- 节点模块(makefuncs.c、nodeFuncs.c)
总结
PostgreSQL 18 对 RETURNING 子句的增强,尤其是 OLD 与 NEW 别名的引入,为 INSERT、UPDATE、DELETE 与 MERGE 操作提供了完整的数据变更可视性。这一能力显著减少了对触发器与额外查询的依赖,使数据同步、变更追踪与审计实现更加简洁、高效且易于维护。
MERGE 与增强型 RETURNING 的结合,为 Upsert 场景提供了前所未有的控制能力与透明度,是 PostgreSQL 在开发友好性与工程实用性方面的重要进展。
原文链接:
作者:Ahsan Hadi
HOW 2026 议题招募中
2026 年 4 月 27-28 日,由 IvorySQL 社区联合 PGEU(欧洲 PG 社区)、PGAsia(亚洲 PG 社区)共同打造的 HOW 2026(IvorySQL & PostgreSQL 技术峰会) 将再度落地济南。届时,PostgreSQL 联合创始人 Bruce Momjian 等顶级大师将亲临现场。
自开启征集以来,HOW 2026 筹备组已感受到来自全球 PostgreSQL 爱好者的澎湃热情。为了确保大会议题的深度与广度,我们诚邀您在 2026 年 2 月 27 日截止日期前,提交您的技术见解。