PostgreSQL 15 的 CREATE PROCEDURE 支持事务控制,但过程体内禁止使用 BEGIN/COMMIT/ROLLBACK;所有操作默认隶属外部事务,仅可通过 SAVEPOINT 实现局部回滚,不可脱离外层事务独立提交或回滚。

存储过程里不能用 BEGIN/COMMIT 直接控制事务
PostgreSQL 15 的 CREATE PROCEDURE 本身支持事务控制,但你不能在过程体里写 BEGIN、COMMIT 或 ROLLBACK —— 这些语句在 PL/pgSQL 过程中是语法错误。过程调用默认运行在外部事务上下文中,它的所有操作天然属于该事务,除非你显式使用保存点(SAVEPOINT)做局部回滚。
常见错误现象:ERROR: SAVEPOINT is not allowed in a procedure(如果你用的是旧版本或误配了函数类型),或者更隐蔽的:过程执行完,外部事务一提交,所有变更才生效,根本无法“中途回滚”某一步。
- 必须用
CREATE PROCEDURE,不是CREATE FUNCTION;后者不支持事务控制语句,且强制返回值 - 过程内部只能用
SAVEPOINT+ROLLBACK TO SAVEPOINT实现局部回退,不能脱离外层事务 - 如果过程被
CALL在一个已开启的事务中,它没有权限“结束”那个事务
用 SAVEPOINT 做可回退的中间状态
当你需要在过程内某步失败时只撤销那部分操作(比如先插入主表,再批量插入子表,子表失败不影响主表),就得靠 SAVEPOINT。它不是独立事务,而是外层事务里的标记点。
示例场景:创建订单并关联多条明细,明细插入出错时只丢弃明细,保留订单头:
CREATE OR REPLACE PROCEDURE create_order_with_items(
p_customer_id INT,
p_items TEXT[] -- 格式如 '{"item1,100","item2,200"}'
)
LANGUAGE plpgsql
AS $$
DECLARE
v_order_id INT;
BEGIN
INSERT INTO orders (customer_id) VALUES (p_customer_id) RETURNING id INTO v_order_id;
<pre class='brush:php;toolbar:false;'>-- 设立保存点,后续操作可局部回滚
SAVEPOINT items_insert;
FOREACH item IN ARRAY p_items LOOP
INSERT INTO order_items (order_id, product, amount)
VALUES (v_order_id, split_part(item, ',', 1), split_part(item, ',', 2)::NUMERIC);
END LOOP;EXCEPTION WHEN OTHERS THEN -- 只回滚到保存点,订单头仍保留 ROLLBACK TO SAVEPOINT items_insert; RAISE NOTICE 'Failed to insert some items, order % kept', v_order_id; END; $$;
-
SAVEPOINT名字要唯一,避免嵌套冲突;同一过程内重复定义会覆盖前一个 -
ROLLBACK TO SAVEPOINT不会终止过程执行,后续语句仍可继续(比如记录日志、返回状态) - 不能对同一个保存点多次
ROLLBACK TO,否则报错invalid savepoint
PROCEDURE 和 FUNCTION 的事务行为差异
很多人混淆这两者,导致事务逻辑失控。关键区别不在语法糖,而在 PostgreSQL 内部事务模型约束:
PostgreSQL 18.4 官方 Ubuntu 安装包现已发布,这是目前最新的稳定版本。推荐通过官方 APT 仓库安装:先执行 sudo apt update 更新索引,再运行 sudo apt install postgresql-18 即可完成部署。新版本引入了异步 I/O 子系统,在顺序扫描与 VACUUM 场景下性能提升显著,同时支持 UUID v7 原生生成函数与虚拟生成列。
-
CREATE FUNCTION:运行在只读查询上下文延伸中,禁止任何事务控制语句,连SAVEPOINT都不支持;所有修改都绑定到调用它的事务 -
CREATE PROCEDURE:明确设计为执行事务性操作,允许SAVEPOINT、RAISE、GET DIAGNOSTICS等,但依然不能COMMIT/ROLLBACK - 函数可用于
SELECT,过程只能用CALL;混用会导致语法错误或静默失败
典型踩坑:把本该是 PROCEDURE 的批量更新逻辑写成 FUNCTION,然后发现异常后数据全丢了——因为没保存点机制,也没法局部回滚,只能等外层事务统一处理。
外部事务未开启时 CALL 的行为
如果直接 CALL my_proc() 而没手动 BEGIN,PostgreSQL 会为这次调用自动开启一个隐式事务,并在过程成功返回后自动 COMMIT。这看起来像“过程自己管事务”,其实是外壳在兜底。
但这个隐式事务不可控:你无法在调用前后加其他 SQL,也不能和别的操作合并进同一个事务块。一旦过程内部出错,整个隐式事务回滚,包括所有已执行语句。
- 生产环境强烈建议始终显式包裹:
BEGIN; CALL ...; COMMIT;或BEGIN; CALL ...; ROLLBACK; - 测试时容易忽略这点,导致单测通过、集成时失败——因为测试常单独跑过程,而真实业务必然嵌入更大事务流
- 监控
pg_stat_activity可看到state = 'idle in transaction'表示有未结束的显式事务,这是泄漏信号
最易被忽略的是:过程内部的 RAISE EXCEPTION 不会自动触发外层事务回滚,它只是抛出错误;是否回滚,完全取决于调用方有没有 EXCEPTION 块或事务边界设置。

















