在日常的数据库开发中,你是否遇到过这样的尴尬场景:执行完一条 INLINECODEbf61bf0d 语句插入新数据后,紧接着就要写第二条 INLINECODE1039c8ac 语句,仅仅是为了获取数据库自动生成的 ID 或默认值?或者在执行 INLINECODEbfb7cff5 和 INLINECODEcc6681a9 操作后,为了确认数据是否按预期修改,不得不手动查询一次?这种“多次往返”的交互方式不仅让代码显得冗余,还在无形中增加了数据库的负担。
特别是在 2026 年的今天,当我们的应用架构正向着 Serverless(无服务器)和 Edge Computing(边缘计算)飞速演进时,每一次数据库的额外往返都可能成为延迟的瓶颈。今天,我们将重新审视 PostgreSQL 中这项被严重低估的功能——RETURNING 子句。在 AI 辅助编程和高度并发的现代开发环境下,它不再仅仅是一个“便捷技巧”,而是构建高性能、原子化业务逻辑的基石。
目录
为什么我们需要 RETURNING 子句?—— 从 2026 年视角审视
在传统的 SQL 编程模式中,数据修改(DML)与数据查询是分离的。这在处理诸如“创建记录并获取其自增主键”这样的需求时,通常会导致代码结构变得松散。例如,在使用带有 INLINECODE03889d3c 或 INLINECODE772317e0 类型的表时,如果刚插入数据就需要知道这条数据的 ID(通常是为了将其作为外键关联到其他表),开发者往往需要依赖特定于驱动程序的功能,或者执行额外的查询。
但随着 Agentic AI(自主 AI 代理)开始介入代码编写,我们更强调“操作的原子性”。AI 生成的代码如果不注重原子性,很容易在两次查询之间产生竞态条件。PostgreSQL 的 RETURNING 子句通过引入一种“修改即查询”的机制,优雅地解决了这个问题。它遵循 SQL 标准,确保了数据的一致性,并且极大地优化了性能。
核心语法与 CTE 的现代结合
INLINECODEf5dbc8bb 子句的用法非常直观。但在现代复杂查询中,我们通常将其与 公用表表达式(CTE,即 INLINECODE1867c90e 子句) 结合使用,以实现更强大的数据流控制。
基本语法回顾
你可以选择返回特定的列,也可以使用通配符 * 返回所有列。
- INSERT 操作中
这通常用于获取系统生成的值,如默认的时间戳或自增 ID。
INSERT INTO table_name (column1, column2)
VALUES (value1, value2)
RETURNING id, column1, column2; -- 或者使用 RETURNING *
- UPDATE 操作中
这有助于验证数据是否按预期进行了变更,特别是当更新逻辑涉及计算或触发器时。
UPDATE table_name
SET column1 = value1
WHERE condition
RETURNING *;
- DELETE 操作中
这在数据归档或日志记录场景中非常有用,可以在删除前捕获被删除的数据快照。
DELETE FROM table_name
WHERE condition
RETURNING *;
实战演练:场景驱动的深度解析
为了让你更直观地理解这一功能,让我们建立一个虚拟的 INLINECODE89324785 表,并通过一系列实际的开发场景来演示 INLINECODEe35b3848 的威力。
准备工作
首先,我们创建一个包含自增主键和默认薪资的表:
-- 创建员工表
CREATE TABLE employees (
id SERIAL PRIMARY KEY, -- 自增主键
name VARCHAR(100) NOT NULL,
role VARCHAR(50),
salary NUMERIC(10, 2) DEFAULT 50000.00, -- 默认薪资
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
场景一:INSERT——解决“获取插入 ID”的痛点
需求:我们需要插入一名新员工 “Alice”,职位为工程师。由于我们设置了默认薪资,我们在插入时不指定薪资,但我们需要立即获取分配给她的 ID 和系统默认填入的入职时间,以便在日志中记录。
如果不使用 INLINECODEd783f652,你可能需要先 INLINECODEd3ead65a,然后 INLINECODE7adeace2,或者查询 INLINECODE4bfd7846(这并不安全,因为名字可能重复)。
使用 RETURNING 的解决方案:
-- 插入数据并返回生成的ID、姓名和默认时间戳
INSERT INTO employees (name, role)
VALUES (‘Alice‘, ‘Engineer‘)
RETURNING id, name, salary, last_updated;
可能的输出结果:
name
last_updated
:—
:—
Alice
2023-10-27 10:00:00深度解析:
在这个例子中,请注意我们并没有在 INLINECODE4dc55f84 语句中指定 INLINECODE1e92ca57 和 INLINECODEf5eb0e73。PostgreSQL 自动填充了这些默认值。通过 INLINECODE0d3c55de,我们不仅拿到了 id,还拿到了数据库帮我们计算好的默认值。这消除了数据不一致的风险——我们拿到的绝对是刚刚写入的那一行数据。
场景二:UPDATE——实时验证计算结果
需求:公司决定给 Alice 发放奖金,我们需要将她的薪水更新为 65,000。但是,在更新语句执行后,我们需要确认更新确实生效了,并且可能需要向用户界面返回最新的状态。
使用 RETURNING 的解决方案:
-- 更新薪资并返回更新后的完整记录
UPDATE employees
SET salary = 65000
WHERE name = ‘Alice‘ AND role = ‘Engineer‘
RETURNING *;
可能的输出结果:
name
salary
:—
:—
Alice
65000.00
深度解析:
这里有一个关键的细节:如果你在表定义中设置了触发器(Trigger)或者 INLINECODEb6b9947c,那么 INLINECODE043240a5 返回的是修改后的最终状态。在上面的例子中,INLINECODE6fb6fb42 字段(如果有触发器更新它)会自动变为最新时间。这比先执行 INLINECODEcca77e5a 再执行 SELECT 要安全得多,特别是在高并发环境下,避免了两次查询之间数据被其他事务修改的“幻读”风险。
场景三:DELETE——安全的数据归档与确认
需求:假设 Alice 决定离职,我们需要从 employees 表中删除她的记录。但在物理删除之前,我们需要通过应用层记录一份离职档案,这就要求我们在删除的那一刻,必须拿到她所有的原始数据。
使用 RETURNING 的解决方案:
-- 删除记录并将被删除的数据返回给应用层进行处理
DELETE FROM employees
WHERE name = ‘Alice‘
RETURNING *;
可能的输出结果:
name
salary
:—
:—
Alice
65000.00深度解析:
这是一个非常强大的“软删除”或“归档”策略。应用层代码接收到这些数据后,可以将其插入到 INLINECODE49f894c4 表或写入日志文件,然后再提交事务。如果 INLINECODEc55cf12d 语句影响了多行(例如 INLINECODE410ff4bf),INLINECODE48441002 会返回所有被删除的行集合,让你可以批量处理被清理的数据。
进阶技巧:RETURNING 与复杂表达式
RETURNING 的强大之处不仅在于返回原始列数据,你还可以在子句中使用表达式、类型转换甚至函数。
示例:计算并返回变更差异
假设我们不仅想看更新后的薪水,还想看看涨了多少。我们可以直接在 RETURNING 中进行计算。
-- 假设我们要涨薪 10%,并返回旧薪水、新薪水以及涨幅金额
UPDATE employees
SET salary = salary * 1.1
WHERE name = ‘Bob‘ -- 假设还有个员工 Bob
RETURNING
name AS "员工姓名",
salary AS "新薪资",
salary / 1.1 AS "旧薪资", -- 反向计算旧值(仅作演示)
(salary * 0.1) AS "涨幅金额";
这种写法将业务逻辑的计算压力转移到了数据库端,减少了应用层的代码复杂度。
2026 开发新范式:RETURNING 在 CTE 与级联操作中的神级应用
在现代的 PostgreSQL 开发中,尤其是当我们面对复杂的业务流程时,单纯的 SQL 语句往往不够用。我们经常需要在一次事务中完成“插入 A 表,获取 A 的 ID,然后插入 B 表”。
过去,我们可能会在应用层代码(如 Python 或 Node.js)中分两步执行。但在 2026 年,为了降低延迟并提高事务完整性,我们推崇 Database-First Logic(数据库优先逻辑)。利用 INLINECODE5b40e96c 子句(CTE)和 INLINECODEe59c83e7,我们可以将这一连串逻辑完全封装在 SQL 中。
场景:级联插入——用户与权限初始化
假设我们正在开发一个 SaaS 系统,当创建一个新用户时,我们需要:
- 在 INLINECODEc6034f43 表中创建用户并获取生成的 INLINECODE4e1508ac。
- 立即在 INLINECODEbde8f633 表中记录“用户创建”的操作,包含 INLINECODE9dd89d69。
- 同时初始化该用户的默认权限设置到
user_permissions表。
传统做法(低效):
INSERT INTO users ...SELECT last_insert_id()INSERT INTO audit_logs ... (user_id)INSERT INTO user_permissions ... (user_id)
2026 最佳实践(原子化 SQL):
我们可以使用一个带有 RETURNING 的 CTE 来链式操作,完全不需要中间层的介入。
-- 定义一个 CTE(公用表表达式)块
WITH new_user AS (
-- 步骤1: 插入用户,并通过 RETURNING 返回新生成的 ID 和创建时间
INSERT INTO users (name, email, role)
VALUES (‘Alice‘, ‘[email protected]‘, ‘Admin‘)
RETURNING id, name, CURRENT_TIMESTAMP AS created_at
),
audit_log AS (
-- 步骤2: 利用上一个 CTE (new_user) 返回的数据,直接写入日志表
-- 这里实现了数据的“零延迟”流转,不需要应用层知道 ID 是多少
INSERT INTO audit_logs (user_id, action_message, log_time)
SELECT id, ‘User created via automated workflow‘, created_at
FROM new_user
RETURNING *
),
user_perm AS (
-- 步骤3: 再次利用 new_user 的 ID 初始化权限
INSERT INTO user_permissions (user_id, permission_level)
SELECT id, ‘full_access‘ FROM new_user
RETURNING *
)
-- 最终返回给客户端的结果:确认用户创建成功,并附带初始权限 ID
SELECT
u.id AS user_id,
u.name AS user_name,
p.permission_level
FROM new_user u
JOIN user_perm p ON u.id = p.user_id;
为什么这是“神级”应用?
- 原子性:这三步操作要么全成功,要么全失败。不存在“用户创建了但日志没写”的中间状态,这在金融或安全敏感系统中至关重要。
- 性能极致:数据从未离开过数据库引擎。没有网络往返(Network Round-trips),没有应用层的序列化/反序列化开销。在 Serverless 架构中,这能显著减少冷启动带来的延迟感。
- AI 友好:当使用像 Cursor 或 Copilot 这样的 AI 辅助工具时,这种结构化的 SQL 模块更容易被理解和维护,而不是散落在代码库各个角落的零散查询。
进阶技巧:RETURNING 与复杂表达式
RETURNING 的强大之处不仅在于返回原始列数据,你还可以在子句中使用表达式、类型转换甚至函数。
示例:计算并返回变更差异
假设我们不仅想看更新后的薪水,还想看看涨了多少。我们可以直接在 RETURNING 中进行计算。
-- 假设我们要涨薪 10%,并返回旧薪水、新薪水以及涨幅金额
UPDATE employees
SET salary = salary * 1.1
WHERE name = ‘Bob‘ -- 假设还有个员工 Bob
RETURNING
name AS "员工姓名",
salary AS "新薪资",
salary / 1.1 AS "旧薪资", -- 反向计算旧值(仅作演示)
(salary * 0.1) AS "涨幅金额";
这种写法将业务逻辑的计算压力转移到了数据库端,减少了应用层的代码复杂度。
常见陷阱与最佳实践
虽然 RETURNING 非常方便,但在实际使用中,有几个细节需要特别注意,以避免潜在的 Bug。
- 返回多行的情况:如果你的 INLINECODE9dd2f2e6 或 INLINECODE0e80fc4a 语句影响了多行记录(例如没有使用 INLINECODE80f0dbde 或唯一索引的 INLINECODE62608d4e 条件),INLINECODEb669bcb0 将返回所有受影响的行。确保你的客户端代码(如 Python 的 INLINECODE78b7a4b3 或 Node.js 的查询处理)能够处理结果集(Result Set),而不仅仅是单行数据。
- 与触发器的交互:如果你的表上配置了 INLINECODE982fde48 触发器修改了数据,INLINECODE7252904d 返回的是触发器执行之后的数据。这是一个巨大的优势,因为它确保了你看到的是最终落盘的数据。
- NULL 值的处理:如果某一行在更新后没有实际变化(PostgreSQL 的优化机制可能会跳过写入),或者某些列原本就是 NULL,
RETURNING依然会返回这些行。不要假设返回的所有值都是非空的。
- 性能考虑:虽然 INLINECODE3ff179af 减少了网络往返,但它确实会带来一定的数据传输开销。如果你在 INLINECODEebce3bdd 操作中误删了 100 万行数据并且使用了 INLINECODE7eaee067,可能会导致网络拥塞。在批量操作中,请谨慎选择要返回的列,或者添加 INLINECODE9620b8b5。
真实世界的挑战:在分布式环境下的幂等性与并发
让我们思考一下在高并发场景下可能遇到的问题。在微服务架构中,同一个资源可能会被多个节点同时请求更新。
场景:库存扣减。
如果我们先 INLINECODE3e707844 查询库存,然后在应用层判断是否足够,最后再 INLINECODE6d317191,这就典型的“检查然后行动”模式,存在严重的竞态条件。
2026 解决方案:直接利用 RETURNING 进行条件更新并获取结果。
-- 尝试将商品 ID 为 1001 的库存减少 1
-- 只有在当前库存大于 0 时才执行
UPDATE inventory
SET stock = stock - 1,
last_updated = NOW()
WHERE id = 1001 AND stock > 0
RETURNING
id,
stock AS "剩余库存", -- 这是更新后的库存
(stock + 1) AS "扣减前库存"; -- 计算出之前的库存用于日志
分析:
如果 SQL 返回了一行数据,说明扣减成功,应用层直接告知用户“下单成功”。
如果 SQL 返回空集(0 rows),说明库存不足(stock > 0 条件不满足),应用层告知用户“卖光了”。
这整个过程在数据库内部是一把锁(通过行级隐式锁)完成的,完全不需要应用层的分布式锁,极大地简化了代码并提升了并发性能。
总结:让数据库交互更上一层楼
通过这篇文章的探索,我们不仅学习了 RETURNING 子句的基本语法,更重要的是,我们理解了它如何改变我们与数据库交互的方式。
- 效率提升:它将原本需要的“两个步骤”(INSERT + SELECT 或 UPDATE + SELECT)合并为一个原子操作,显著减少了数据库连接的 I/O 开销。
- 代码整洁:它消除了获取生成 ID 的冗余代码,让业务逻辑更加流畅。
- 数据一致性:在处理触发器、默认值和计算列时,它保证了你获取的数据就是数据库中真实存在的数据,避免了并发环境下的竞态条件。
下次当你发现自己正在编写紧随 DML 语句之后的查询语句时,请停下来想一想:“我是不是可以用 RETURNING 子句来代替?” 这一小小的思维转变,结合 CTE 的使用,将会让你的 PostgreSQL 应用程序在 2026 年乃至未来更加健壮和高效。