现在的数据库系统,业务逻辑经常写在服务端——用procedure和function来搞。用PostgreSQL的时候,得分清楚function和procedure的区别(特别是11+版本加了procedure之后),还有它们跟事务是怎么配合的。
下面我给你讲讲PostgreSQL 17里,事务机制、嵌套调用、procedure/function里怎么部分回滚,都是官方文档和现在的限制。
核心概念:function vs procedure
function(CREATE FUNCTION)——永远都在一个外部事务里跑;function里不能用显式事务命令(BEGIN、COMMIT、ROLLBACK、SAVEPOINT)。
- 所有改动只会在外部事务级别commit或者rollback。
- 想在function里“部分回滚”,可以用
BEGIN ... EXCEPTION ... END,但不能在function里commit。
procedure(CREATE PROCEDURE)——就是为了让你能在服务端直接控制事务(比如分阶段commit、回滚啥的)。
- 在procedure(PL/pgSQL)里可以用
COMMIT、ROLLBACK、SAVEPOINT、RELEASE SAVEPOINT。 - 注意:在PL/pgSQL的procedure里不能用
ROLLBACK TO SAVEPOINT(会报语法错)。 - procedure只能用SQL命令
CALL ...来调,不能用SELECT或者在别的function里调。
怎么在一个procedure/function里调另一个?
function可以直接像普通函数那样调别的function:
-- 例子:计算折扣的function
CREATE OR REPLACE FUNCTION calculate_discount(order_total NUMERIC)
RETURNS NUMERIC AS $$
BEGIN
IF order_total >= 100 THEN
RETURN order_total * 0.1;
ELSE
RETURN 0;
END IF;
END;
$$ LANGUAGE plpgsql;
-- 订单处理function会调另一个function
CREATE OR REPLACE FUNCTION process_order(order_id INT, order_total NUMERIC)
RETURNS VOID AS $$
DECLARE
discount NUMERIC;
BEGIN
discount := calculate_discount(order_total);
RAISE NOTICE '折扣: %', discount;
INSERT INTO orders_log (order_id, order_total, discount)
VALUES (order_id, order_total, discount);
END;
$$ LANGUAGE plpgsql;
所有操作都在同一个外部事务里!任何function里出错,所有改动都会回滚。
procedure调用和嵌套事务
procedure可以在别的procedure里用CALL ...调(PostgreSQL 17支持CALL proc1() -> CALL proc2()这种调用栈),但事务规则还是一样:
- 事务命令(
COMMIT、ROLLBACK、SAVEPOINT、RELEASE SAVEPOINT)只能在最外层procedure里用。 - 如果procedure里有事务控制,结果你在已经有显式事务的情况下(比如客户端不开autocommit)调它,
COMMIT/SAVEPOINT会报错。
procedure不能在function或者匿名块(DO ...)里调。只能用CALL单独调。
带事务控制的procedure例子
-- 分批commit的procedure(只在autocommit连接下有效)
CREATE PROCEDURE process_batch_orders()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT order_id, order_total FROM incoming_orders LOOP
BEGIN
-- 每批数据单独保存
INSERT INTO orders (order_id, total) VALUES (rec.order_id, rec.order_total);
EXCEPTION WHEN OTHERS THEN
INSERT INTO order_errors(order_id, err_text) VALUES (rec.order_id, SQLERRM);
END;
COMMIT;
END LOOP;
END;
$$;
-- 调用procedure
CALL process_batch_orders();
每次COMMIT后会自动开新事务。
PL/pgSQL里的部分回滚(类似savepoint的效果)
PL/pgSQL(无论function还是procedure)不支持ROLLBACK TO SAVEPOINT命令。
想回滚部分代码,只能用BEGIN ... EXCEPTION ... END块:
BEGIN
-- 一些操作
BEGIN
-- 可能出错的操作
EXCEPTION WHEN OTHERS THEN
-- 这个块里的改动会回滚
RAISE NOTICE '块内回滚!';
END;
END;
在procedure里也可以用SAVEPOINT和RELEASE SAVEPOINT,但不能用ROLLBACK TO SAVEPOINT。它们主要用来分阶段,真正控制还是得靠异常处理。
限制和最佳实践
- function——只能做原子操作:要么全成,要么全回滚。出错就全撤。
- procedure——只能用CALL调:只能用单独的SQL命令,不能在SELECT/function里调。嵌套事务可以,但得严格按PL/pgSQL的限制来。
- 部分回滚——只能用EXCEPTION:官方推荐和支持的部分回滚方式(类似SAVEPOINT)。
- 嵌套procedure只有用CALL才能控制事务:否则会报错。
逻辑和事务交互的常见问题
我能在function里搞“嵌套”事务吗?
不能。所有操作都在一个事务里。想部分回滚——只能用EXCEPTION块。
我能在function或匿名块里用COMMIT/ROLLBACK吗?
不能,会报语法错。用procedure吧。
能不能在function里调procedure?
不能,只能用CALL命令调。function/SELECT里不行。
procedure里能不能ROLLBACK TO SAVEPOINT?
不能!PL/pgSQL里不让用。用EXCEPTION块吧。
GO TO FULL VERSION