PostgreSQL 18 RETURNING 增强:现代应用的重要进展

PostgreSQL 18 正式发布,带来了多项重要改进,其中 RETURNING 子句的增强尤为突出。该特性在 MERGE RETURNING 场景下实现了关键突破,可显著简化应用架构,并提升数据变更追踪能力。

RETURNING 子句的演进

RETURNING 子句长期以来用于在 INSERTUPDATEDELETE 操作后返回受影响行的数据,从而避免额外的 SELECT 查询,减少数据库往返次数并提升性能。然而,在 PostgreSQL 18 之前,该子句在功能上存在明显限制,迫使开发实践中采用各种折中方案。

在 PostgreSQL 17 中,首次为 MERGE 语句引入 RETURNING 支持(提交 c649fa24a),这是一次重要进展。MERGE 语句自 PostgreSQL 15 引入,用于在单条语句中完成条件化的 INSERTUPDATEDELETE 操作,但在缺乏 RETURNING 支持的情况下,无法直观获取实际执行结果。

PostgreSQL 18 的新特性

PostgreSQL 18 通过引入 OLD 与 NEW 别名(提交 80feb727c8,由 Dean Rasheed 提交,Jian He 与 Jeff Davis 评审),将 RETURNING 子句能力提升至新的层级。该增强使 DML 操作期间的数据捕获方式发生了根本性变化。

PostgreSQL 18 之前的限制

在早期版本中,RETURNING 子句在不同语句类型下存在以下差异化限制:

  • INSERTUPDATE 仅能返回新值或当前值
  • DELETE 仅能返回旧值
  • MERGE 根据内部实际执行的操作类型(INSERTUPDATEDELETE)返回结果

在需要对比更新前后数据、或精确追踪字段变化时,可选方案较为有限,包括:

  • 在修改前额外执行 SELECT 查询
  • 编写复杂的触发器函数
  • 在应用层实现变更跟踪逻辑
  • 通过系统列(如 xmax)进行间接判断

上述方式普遍增加了实现复杂度与访问延迟,并降低了代码可维护性。

解决方案:OLD 与 NEW 别名

PostgreSQL 18 引入了特殊别名 oldnew,可在单条语句中同时访问数据的修改前状态与修改后状态。该机制适用于 INSERTUPDATEDELETE 以及 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 子句增强特性,是该数据库提升开发友好性、减少复杂替代方案使用的重要举措。单原子操作中同时调用数据新旧值的能力,可简化应用开发中的多种通用实现模式。

该功能在后续版本中或可从以下方向进一步升级:

  1. 扩展 MERGE 语句能力,新增更多 WHEN 子句,实现更复杂的条件操作.
  2. 新增聚合功能支持,支持对 RETURNING 子句的返回结果直接进行聚合计算。
  3. 实现跨表返回,支持在单操作中返回关联表的数据信息。

技术细节与提交记录参考

针对关注技术实现细节的人员,可参考以下信息:

该功能的实现涉及多个组件的修改,包括:

  • 执行器(execExpr.c、execExprInterp.c、nodeModifyTable.c)
  • 解析器(parse_target.c)
  • 优化器(createplan.c、setrefs.c、subselect.c)
  • 节点模块(makefuncs.c、nodeFuncs.c)

总结

PostgreSQL 18 对 RETURNING 子句的增强,尤其是 OLD 与 NEW 别名的引入,为 INSERTUPDATEDELETEMERGE 操作提供了完整的数据变更可视性。这一能力显著减少了对触发器与额外查询的依赖,使数据同步、变更追踪与审计实现更加简洁、高效且易于维护。

MERGE 与增强型 RETURNING 的结合,为 Upsert 场景提供了前所未有的控制能力与透明度,是 PostgreSQL 在开发友好性与工程实用性方面的重要进展。

原文链接:

https://www.pgedge.com/blog/postgresql-18-returning-enhancements-a-game-changer-for-modern-applications

作者: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 日截止日期前,提交您的技术见解。

投递链接:https://jsj.top/f/uebqBc

相关推荐
ʚB҉L҉A҉C҉K҉.҉基҉德҉^҉大5 小时前
自动化机器学习(AutoML)库TPOT使用指南
jvm·数据库·python
哈__5 小时前
多模融合 一体替代:金仓数据库 KingbaseES 重构企业级统一数据基座
数据库·重构
老邓计算机毕设5 小时前
SSM医院病人信息管理系统e7f6b(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·医院信息化·ssm 框架·病人信息管理
2601_949613025 小时前
flutter_for_openharmony家庭药箱管理app实战+药品分类实现
大数据·数据库·flutter
dyyx1116 小时前
使用Scikit-learn进行机器学习模型评估
jvm·数据库·python
踢足球09296 小时前
寒假打卡:2026-01-27
数据库
不想写bug呀6 小时前
MySQL索引介绍
数据库·mysql
weixin_499771557 小时前
使用Seaborn绘制统计图形:更美更简单
jvm·数据库·python
青春男大7 小时前
Redis和RedisTemplate快速上手
java·数据库·redis·后端·spring·缓存
冉冰学姐7 小时前
SSM医院预约挂号管理系统q9ig2(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·毕业设计·医院预约挂号系统·ssm 框架