Skip to content

返回数据(RETURNING) ​

写操作完成后还想知道"到底改了哪些行、改成了什么",过去只能再查一次——于是就有了"改完再 SELECT 却读到别人改的数据"这类竞态。RETURNING 让 INSERT / UPDATE / DELETE / MERGE 在修改的同时把行"交还"给你,一条语句、同一个快照、零额外往返。PostgreSQL 18 更进一步,支持用 OLD / NEW 同时取回修改前后的两份值。本节讲清它的取值语义、与 CTE 的组合套路,以及在生产代码里的定位:它是写操作的唯一可信回执。

前置知识

阅读本节前,建议先了解:批量导入导出(COPY)

基础概念 ​

RETURNING 是写语句末尾的可选子句,内容与 SELECT 的输出列表规则完全一致:可以是列名、表达式、函数调用,也可以写 *。

它的核心价值有两个:

  1. 拿到数据库生成的值——自增主键、DEFAULT now()、生成列,这些值写之前应用并不知道。
  2. 拿到"确实被改动"的行集合——RETURNING 只输出真正被插入/更新/删除的行,天然是"影响行数"的详细版。
语句不写限定词时返回什么可用的旧/新限定
INSERT插入后的新行old.*(通常为 NULL)/ new.*(等于默认)
UPDATE更新后的新行old.*(修改前)/ new.*(等于默认)
DELETE被删除的行old.*(等于默认)/ new.*(通常为 NULL)
MERGE源行 + 目标行(可能大量重名)old.* / new.* 均可用

几个关键术语:

  • 回执(Receipt):把 RETURNING 的结果当作"这次写入究竟发生了什么"的权威证据,而不是靠影响行数猜。
  • OLD / NEW:PostgreSQL 18 引入的行限定符,让你在同一行上同时看到修改前与修改后的值。new.price 与直接写 price 等价,但语义更明确。
  • 单语句快照:RETURNING 看到的是本语句自己造成的结果,不受其他并发事务干扰,因此不存在"改完再查被别人插队"的问题。
  • 与 CTE 的组合:WITH ... AS (INSERT/UPDATE/DELETE ... RETURNING ...) 可以把写操作的结果当作一张表继续用,实现"写 + 读 + 再写"的原子链路。
  • MERGE 的列重名:MERGE 的默认输出同时包含源行与目标行,因此 RETURNING * 常出现重复列名,建议用 p.* / n.* 明确限定。
  • 不支持的位置:RETURNING 只能出现在上述四种写语句里,COPY 不支持,SELECT 也不需要。

语法与用法 ​

RETURNING 出现在每条写语句的末尾,语法骨架统一为:

text
RETURNING * | output_expression [ [ AS ] output_name ] [, ...]

四种语句的最简形态:

sql
-- INSERT:拿回数据库分配的主键与默认值
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-1001', 'alice@example.com', 199.00)
RETURNING id, order_no, status, created_at;

-- UPDATE:拿回更新后的行
UPDATE orders SET status = 'paid' WHERE order_no = 'ORD-1001'
RETURNING order_no, status, updated_at;

-- DELETE:拿回被删除的行(便于归档或审计)
DELETE FROM orders WHERE status = 'cancelled'
RETURNING id, order_no, amount;

-- MERGE:拿回每行实际执行的动作
MERGE INTO inventory t USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED THEN INSERT (sku, qty) VALUES (s.sku, s.qty)
RETURNING merge_action() AS action, t.*;

PostgreSQL 18 的 OLD / NEW 写法:

sql
-- 同时看到改前与改后的价格,一次拿到差额
UPDATE products SET price = price * 1.10
WHERE price <= 99.99
RETURNING name,
          old.price                       AS old_price,
          new.price                       AS new_price,
          new.price - old.price           AS price_change;
text
   name    | old_price | new_price | price_change
-----------+-----------+-----------+--------------
 机械键盘  |     89.00 |     97.90 |         8.90
(1 row)

UPDATE 1

MERGE 中 OLD / NEW 的语义与其他语句一致:

sql
-- MERGE 里同样可以取回改前改后的值
MERGE INTO inventory t USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
WHEN NOT MATCHED THEN INSERT (sku, name, qty, price)
  VALUES (s.sku, s.name, s.qty, s.price)
RETURNING merge_action() AS action, t.sku, old.qty AS old_qty, new.qty AS new_qty;
输出写法含义使用建议
RETURNING *目标表全部列简单场景方便;MERGE 时应避免
RETURNING 表别名.*仅某张表的列MERGE / FROM 关联场景推荐
RETURNING 列 AS 别名改列名输出给应用或下游时强烈建议
RETURNING 表达式计算列如 amount * 0.1 AS tax
RETURNING old.列修改前的值(18+)审计、计算差额
RETURNING new.列修改后的值(18+)与裸列名等价,但更清晰
RETURNING merge_action()实际动作(MERGE 专用)统计增删改分布

详细说明 ​

INSERT 的 RETURNING:拿回数据库生成的值 ​

INSERT 最常见的用途是取回自增主键——因为应用在写入前根本不知道那个值。

sql
-- 拿回 IDENTITY 生成的主键与 DEFAULT 生成的默认值
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-1001', 'alice@example.com', 199.00)
RETURNING id, order_no, status, created_at;
text
 id | order_no | status  |          created_at
----+----------+---------+-------------------------------
  1 | ORD-1001 | pending | 2026-09-24 16:40:12.318+08
(1 row)

INSERT 0 1

多行插入时,RETURNING 按插入顺序返回每一行:

sql
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-1002', 'bob@example.com',   880.00),
       ('ORD-1003', 'carol@example.com', 3200.00)
RETURNING id, order_no, amount;
text
 id | order_no | amount
----+----------+---------
  2 | ORD-1002 |  880.00
  3 | ORD-1003 | 3200.00
(2 rows)

INSERT 0 2

INSERT ... SELECT 也支持 RETURNING,可以顺便做汇总:

sql
-- 批量生成订单号并直接统计插入结果
INSERT INTO orders (order_no, user_email, amount)
SELECT 'ORD-BATCH-' || g, 'batch@example.com', g * 10
FROM generate_series(1, 5) AS g
RETURNING id, order_no, amount;
text
 id |   order_no    | amount
----+---------------+--------
  4 | ORD-BATCH-1   |  10.00
  5 | ORD-BATCH-2   |  20.00
  6 | ORD-BATCH-3   |  30.00
  7 | ORD-BATCH-4   |  40.00
  8 | ORD-BATCH-5   |  50.00
(5 rows)

INSERT 0 5

ON CONFLICT DO UPDATE 时,RETURNING 返回的是最终落库的那一行(无论是新插入还是被更新的):

sql
INSERT INTO inventory (sku, name, qty, price)
VALUES ('SKU-1001', '机械键盘', 10, 399.00)
ON CONFLICT (sku) DO UPDATE
SET qty = inventory.qty + EXCLUDED.qty, updated_at = now()
RETURNING sku, qty, (xmax = 0) AS is_insert;
text
   sku    | qty | is_insert
----------+-----+-----------
 SKU-1001 | 130 | f
(1 row)

INSERT 0 1
-- is_insert = f 说明这一行走的是 UPDATE 分支

INSERT ... ON CONFLICT 配合 OLD / NEW(PostgreSQL 18+)能精确看出"这次写入把值从多少改成了多少":

sql
-- PostgreSQL 18+:冲突更新时同时取回旧值与新值
INSERT INTO inventory (sku, name, qty, price)
VALUES ('SKU-1001', '机械键盘', 10, 399.00)
ON CONFLICT (sku) DO UPDATE
SET qty = inventory.qty + EXCLUDED.qty
RETURNING sku, old.qty AS qty_before, new.qty AS qty_after;
text
   sku    | qty_before | qty_after
----------+------------+-----------
 SKU-1001 |        120 |       130
(1 row)

UPDATE 的 RETURNING:确认"真的改到了" ​

UPDATE 默认返回新行。它的实战意义在于:用一行行的详细数据替代"UPDATE 1"这种无法追溯的提示。

sql
-- 只更新真正需要改的行,并逐行确认
UPDATE inventory
SET qty = qty - 5, updated_at = now()
WHERE sku = 'SKU-1001' AND qty >= 5
RETURNING sku, qty AS qty_after, updated_at;
text
   sku    | qty_after |          updated_at
----------+-----------+-------------------------------
 SKU-1001 |       125 | 2026-09-24 16:44:03.902+08
(1 row)

UPDATE 1

UPDATE ... FROM 场景建议用表别名限定列,避免歧义:

sql
UPDATE orders o
SET status = 'settled', updated_at = now()
FROM payments p
WHERE o.order_no = p.order_no AND p.status = 'success'
RETURNING o.order_no, o.status, p.pay_no, p.amount;
text
 order_no | status  | pay_no | amount
----------+---------+--------+---------
 ORD-1001 | settled | PAY-1  |  199.00
(1 row)

UPDATE 1

RETURNING 只输出实际被更新的行,这让它成为验证 WHERE 是否写对的最佳工具:

sql
-- 条件写错(如金额下限写成 100000),返回 0 行,立刻暴露问题
UPDATE orders SET status = 'vip' WHERE amount > 100000
RETURNING order_no, amount;
text
 order_no | amount
----------+--------
(0 rows)

UPDATE 0

DELETE 的 RETURNING:删除的同时搬走数据 ​

删除是不可逆的,RETURNING 是唯一能在"同一时刻"留下痕迹的方式。

sql
-- 删除前先看一眼(用 SELECT 预演)
SELECT id, order_no, amount FROM orders WHERE status = 'cancelled';
sql
-- 删除并取回被删内容
DELETE FROM orders WHERE status = 'cancelled'
RETURNING id, order_no, amount, now() AS deleted_at;
text
 id | order_no | amount  |          deleted_at
----+----------+---------+-------------------------------
  9 | ORD-2001 |  120.00 | 2026-09-24 16:47:55.117+08
 10 | ORD-2002 |   80.00 | 2026-09-24 16:47:55.117+08
(2 rows)

DELETE 2

被删除的行在 RETURNING 里可以自由计算,例如生成归档记录:

sql
DELETE FROM orders WHERE created_at < now() - interval '365 days'
RETURNING order_no, user_email, amount, 'archived' AS archive_reason;

CTE + RETURNING:一条语句完成"写 → 读 → 再写" ​

这是 RETURNING 最有威力的用法。写语句放在 WITH 里,其结果就是一张能被外层继续消费的表。

sql
-- 删除待归档订单,并把它们写入归档表(一条语句,原子完成)
WITH moved AS (
  DELETE FROM orders
  WHERE status = 'expired'
  RETURNING id, order_no, user_email, amount, status, created_at
)
INSERT INTO orders_archive (order_no, user_email, amount, status, created_at)
SELECT order_no, user_email, amount, status, created_at FROM moved;
text
INSERT 0 3

同一个 CTE 可以被引用多次,实现"搬移 + 统计":

sql
WITH moved AS (
  DELETE FROM orders WHERE status = 'expired'
  RETURNING order_no, amount
), archived AS (
  INSERT INTO orders_archive (order_no, amount, status)
  SELECT order_no, amount, 'expired' FROM moved
  RETURNING order_no
)
SELECT (SELECT count(*) FROM archived) AS archived_rows,
       (SELECT sum(amount) FROM moved) AS archived_amount;
text
 archived_rows | archived_amount
---------------+-----------------
             3 |         2480.00
(1 row)

注意:CTE 中的写语句一定会执行,无论外层是否引用它。这与 SELECT 里"没被引用的 CTE 仍会执行"是同一套规则,因此不要在 CTE 里放"可能不该执行"的写操作。

sql
-- 危险:即使外层不用 moved,DELETE 照样会执行
WITH moved AS (
  DELETE FROM orders WHERE status = 'expired' RETURNING *
)
SELECT count(*) FROM orders;

RETURNING 在应用层的定位 ​

RETURNING 不只是"方便",它是消除竞态的正确姿势。对比两种写法:

sql
-- 反模式:插入后再查一次,期间可能有并发写入,且多一次往返
INSERT INTO orders (order_no, user_email, amount) VALUES ('ORD-3001', 'x@example.com', 10);
SELECT id, created_at FROM orders WHERE order_no = 'ORD-3001';

-- 正确:一次拿回,且在写行锁的保护下
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-3001', 'x@example.com', 10)
RETURNING id, created_at;

RETURNING 与"影响行数"的关系:

关注点影响行数RETURNING
是否成功有有(行数)
改了哪一行无有
改成了什么无有
数据库生成的值无有
网络开销极小随返回列数与行数增长

因此有一条实用原则:写操作一律带 RETURNING,把影响行数断言升级为内容断言。

实战示例 ​

下面用一个"下单 → 状态推进 → 归档"的完整流程演示四种语句的 RETURNING 用法。

sql
-- 步骤 1:准备数据
DROP TABLE IF EXISTS orders_archive;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS products;

CREATE TABLE products (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name       text          NOT NULL,
  price      numeric(12,2) NOT NULL CHECK (price >= 0),
  updated_at timestamptz   NOT NULL DEFAULT now()
);

CREATE TABLE orders (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  order_no   text          NOT NULL UNIQUE,
  user_email text          NOT NULL,
  amount     numeric(12,2) NOT NULL CHECK (amount >= 0),
  status     text          NOT NULL DEFAULT 'pending',
  created_at timestamptz   NOT NULL DEFAULT now()
);

CREATE TABLE orders_archive (LIKE orders INCLUDING ALL);

INSERT INTO products (name, price) VALUES
  ('机械键盘', 89.00),
  ('无线鼠标', 129.00),
  ('USB 集线器', 59.00);
text
DROP TABLE
DROP TABLE
DROP TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 3
sql
-- 步骤 2:INSERT 拿回主键与默认值
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-4001', 'alice@example.com', 199.00),
       ('ORD-4002', 'bob@example.com',   880.00),
       ('ORD-4003', 'carol@example.com', 3200.00)
RETURNING id, order_no, status, created_at;
text
 id | order_no | status  |          created_at
----+----------+---------+-------------------------------
  1 | ORD-4001 | pending | 2026-09-24 16:52:10.441+08
  2 | ORD-4002 | pending | 2026-09-24 16:52:10.441+08
  3 | ORD-4003 | pending | 2026-09-24 16:52:10.441+08
(3 rows)

INSERT 0 3
sql
-- 步骤 3:UPDATE 取回改前改后的值(PostgreSQL 18+ 的 OLD/NEW)
UPDATE products
SET price = round(price * 1.10, 2), updated_at = now()
WHERE price <= 99.99
RETURNING name,
          old.price             AS old_price,
          new.price             AS new_price,
          new.price - old.price AS price_change;
text
    name    | old_price | new_price | price_change
------------+-----------+-----------+--------------
 机械键盘    |     89.00 |     97.90 |         8.90
 USB 集线器  |     59.00 |     64.90 |         5.90
(2 rows)

UPDATE 2
sql
-- 步骤 4:带条件的 UPDATE —— 用 RETURNING 验证条件是否命中
UPDATE orders
SET status = 'paid', amount = amount - 20
WHERE order_no IN ('ORD-4001', 'ORD-4002') AND status = 'pending'
RETURNING order_no, status, amount;
text
 order_no | status | amount
----------+--------+---------
 ORD-4001 | paid   |  179.00
 ORD-4002 | paid   |  860.00
(2 rows)

UPDATE 2
sql
-- 步骤 5:UPDATE ... FROM + RETURNING,一次看到两表信息
UPDATE orders o
SET status = 'settled', amount = o.amount * 0.9
FROM products p
WHERE o.status = 'paid' AND p.name = '机械键盘'
RETURNING o.order_no, o.status, p.name AS matched_product, o.amount;
text
 order_no | status  | matched_product | amount
----------+---------+-----------------+---------
 ORD-4001 | settled | 机械键盘         |  161.10
 ORD-4002 | settled | 机械键盘         |  774.00
(2 rows)

UPDATE 2
sql
-- 步骤 6:DELETE + RETURNING,删除的同时取回被删内容
DELETE FROM orders
WHERE status = 'pending'
RETURNING id, order_no, amount, now() AS deleted_at;
text
 id | order_no | amount  |          deleted_at
----+----------+---------+-------------------------------
  3 | ORD-4003 | 3200.00 | 2026-09-24 16:55:02.883+08
(1 row)

DELETE 1
sql
-- 步骤 7:CTE + RETURNING 原子搬移(删除 + 归档一步完成)
WITH moved AS (
  DELETE FROM orders
  WHERE status = 'settled'
  RETURNING id, order_no, user_email, amount, status, created_at
)
INSERT INTO orders_archive (order_no, user_email, amount, status, created_at)
SELECT order_no, user_email, amount, status, created_at FROM moved
RETURNING order_no, amount;
text
 order_no | amount
----------+---------
 ORD-4001 |  161.10
 ORD-4002 |  774.00
(2 rows)

INSERT 0 2
sql
-- 步骤 8:核对搬移结果 —— 业务表已空,归档表有数据
SELECT (SELECT count(*) FROM orders)          AS live_orders,
       (SELECT count(*) FROM orders_archive)  AS archived_orders,
       (SELECT sum(amount) FROM orders_archive) AS archived_amount;
text
 live_orders | archived_orders | archived_amount
-------------+-----------------+-----------------
           0 |               2 |          935.10
(1 row)
sql
-- 步骤 9:CTE 中一次完成搬移与统计
WITH moved AS (
  DELETE FROM orders_archive WHERE status = 'settled'
  RETURNING order_no, amount
)
SELECT count(*) AS purged_rows, coalesce(sum(amount), 0) AS purged_amount FROM moved;
text
 purged_rows | purged_amount
-------------+---------------
           2 |        935.10
(1 row)

DELETE 2
sql
-- 步骤 10:UPSERT + RETURNING,区分"新插入"与"被更新"
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-4001', 'alice@example.com', 199.00)
ON CONFLICT (order_no) DO UPDATE
SET amount = orders.amount + EXCLUDED.amount, status = 'reopened'
RETURNING order_no, status, amount, (xmax = 0) AS is_insert;
text
 order_no |  status  | amount  | is_insert
----------+----------+---------+-----------
 ORD-4001 | reopened |  360.10 | f
(1 row)

INSERT 0 1
-- is_insert = f 说明走了冲突更新分支;订单已被重新激活并累加金额

上面的流程覆盖了 RETURNING 的主要形态:拿回生成值、OLD/NEW 对比、条件校验、多表关联返回、删除取回、CTE 原子搬移与统计。

注意事项 ​

  • RETURNING 只返回被实际修改的行。 条件没命中的行不会出现,因此"返回 0 行"是判断条件是否写对的最直接信号。

  • MERGE 的 RETURNING * 会包含源表与目标表两套列。 大量列重名会让结果难以使用,务必用表别名限定。

sql
-- 不推荐:源与目标列混在一起
MERGE INTO inventory t USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
RETURNING *;

-- 推荐:明确只要目标表的列
MERGE INTO inventory t USING staged_inventory s ON t.sku = s.sku
WHEN MATCHED THEN UPDATE SET qty = s.qty
RETURNING t.*;
  • INSERT 的 old.* 通常是 NULL,DELETE 的 new.* 通常是 NULL。 这是语义使然,不要误以为写错了。

  • COPY 不支持 RETURNING。 需要回执就得用 INSERT ... RETURNING,或导入后再统一查询。

  • CTE 里的写语句一定执行。 即使外层 SELECT 从未引用它,DELETE / UPDATE 也已经发生。

sql
-- DELETE 会真实执行,哪怕外层只数了 orders
WITH moved AS (
  DELETE FROM orders WHERE status = 'expired' RETURNING *
)
SELECT count(*) FROM orders;
  • RETURNING 的输出不参与行锁之外的一致性保证。 它反映的是本语句的效果,不是"数据库当前状态"——同一语句执行期间,其他事务可能已在别处发生了变更。

  • CTE 版本是:WITH ... AS (INSERT/UPDATE/DELETE ... RETURNING ...) 才能作为数据源被引用;普通 INSERT 的结果无法直接再被查询。

  • RETURNING 会带来网络开销。 批量更新百万行时把全部列带回来会显著增加传输与内存占用,此时应只返回必要列,或干脆用影响行数。

  • 同一语句里 new.列 与裸列名等价,但仅限目标表。 若列名在源表中也存在(MERGE / UPDATE ... FROM),不加限定会报歧义错误。

  • RETURNING 的顺序不保证与 VALUES 顺序一致。 虽然实践中通常按插入顺序返回,但不要把它当作合约,需要顺序就显式 ORDER BY(RETURNING 之后不能再写 ORDER BY,得包一层 CTE 或子查询)。

sql
-- 需要有序结果时,包一层外层查询
WITH ins AS (
  INSERT INTO orders (order_no, user_email, amount)
  VALUES ('ORD-5001', 'a@example.com', 10),
         ('ORD-5002', 'b@example.com', 20)
  RETURNING id, order_no
)
SELECT * FROM ins ORDER BY order_no;

最佳实践 ​

  1. 写操作一律带 RETURNING。 它是"这次写入到底发生了什么"的唯一可信回执,能省掉一次查询并消除竞态。
sql
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-6001', 'alice@example.com', 199.00)
RETURNING id, order_no, status, created_at;
  1. 把返回内容当业务断言。 期望 1 行却返回 0 行,说明前置状态不符,应当走异常分支而不是静默继续。
sql
UPDATE inventory SET qty = qty - 5 WHERE sku = 'SKU-1001' AND qty >= 5
RETURNING sku, qty;
-- 应用层检查:无返回行 → 抛"库存不足",触发回滚
  1. 为输出列显式起别名。 应用代码依赖"字段名"而不是"第几列",别名让契约稳定。
sql
UPDATE orders SET status = 'paid' WHERE order_no = 'ORD-6001'
RETURNING order_no, status AS new_status, updated_at AS changed_at;
  1. 需要审计或差额,就用 OLD / NEW(PostgreSQL 18+)。 一次拿到改前改后,避免"再查一次旧值"的错误做法。
sql
UPDATE products SET price = round(price * 1.10, 2)
WHERE price <= 99.99
RETURNING name, old.price AS old_price, new.price AS new_price,
          new.price - old.price AS price_change;
  1. 用 CTE + RETURNING 把"删除 + 归档"做成原子操作。 避免"删了但归档失败"或"归档了但删除失败"的中间态。
sql
WITH moved AS (
  DELETE FROM orders WHERE status = 'expired'
  RETURNING order_no, user_email, amount, status, created_at
)
INSERT INTO orders_archive (order_no, user_email, amount, status, created_at)
SELECT order_no, user_email, amount, status, created_at FROM moved;
  1. UPDATE ... FROM / MERGE 的 RETURNING 一律用表别名限定。 避免歧义报错,也让返回的列来源一目了然。
sql
UPDATE orders o SET status = 'settled'
FROM payments p
WHERE o.order_no = p.order_no AND p.status = 'success'
RETURNING o.order_no, o.status, p.pay_no, p.amount;
  1. MERGE 用 merge_action() 统计增删改分布。 同步任务最需要知道的正是这三类各有多少。
sql
WITH changed AS (
  MERGE INTO inventory t USING staged_inventory s ON t.sku = s.sku
  WHEN MATCHED THEN UPDATE SET qty = s.qty
  WHEN NOT MATCHED THEN INSERT (sku, qty) VALUES (s.sku, s.qty)
  RETURNING merge_action() AS action, t.sku
)
SELECT action, count(*) AS cnt FROM changed GROUP BY action ORDER BY action;
  1. 批量写入控制返回列的数量。 百万行级别只需返回主键或聚合值,不要把整行带回应用。
sql
-- 大批量只需确认写入数量与主键范围
UPDATE orders SET status = 'expired' WHERE status = 'pending'
RETURNING id;
  1. 需要稳定顺序时用外层查询包裹。 RETURNING 子句本身不提供排序能力。
sql
WITH ins AS (
  INSERT INTO orders (order_no, user_email, amount)
  SELECT 'ORD-' || g, 'batch@example.com', g * 10 FROM generate_series(1, 5) AS g
  RETURNING id, order_no, amount
)
SELECT count(*) AS inserted, sum(amount) AS total, min(order_no) AS first_no FROM ins;

下一节

继续学习:基础查询(SELECT)

参考链接 ​