PostgreSQL 中的服务端例程分为函数(FUNCTION)和过程(PROCEDURE)两类,但两者并不能简单画等号。很多从其他数据库迁移过来的开发者会把所有封装在数据库里的逻辑统称为存储过程,这种叫法在 PostgreSQL 里容易引起误解。函数侧重于计算和返回结果,过程侧重于执行一连串数据变更并可以控制事务边界。理解这个差异后,再学习 PL/pgSQL 的基础语法、控制结构和异常处理,就能写出可维护的数据库端程序。

一、函数和过程的本质区别
PostgreSQL 从很早的版本就支持函数,而真正意义上的过程直到 PostgreSQL 11 才引入。函数使用 CREATE FUNCTION 创建,必须声明返回类型,可以返回标量、一行记录、一个表集合,也可以什么都不返回但还是要写上 RETURNS void。函数可以嵌入到 SQL 表达式中,比如放在 SELECT 列表、WHERE 条件甚至索引表达式里。函数内部不能直接执行 COMMIT 或 ROLLBACK,因为函数运行在调用它的外层事务中。
过程使用 CREATE PROCEDURE 创建,没有返回类型,只能通过 CALL 语句调用。过程最大的特点是可以在内部进行事务控制,例如完成一批更新后提交,或者遇到某种条件时回滚。这使过程特别适合需要分批处理大量数据的场景,而函数更适合做计算、格式化和查询结果封装。
-- 函数:根据价格和税率计算税后价 CREATE FUNCTION get_taxed_price(price numeric, rate numeric DEFAULT 0.13) RETURNS numeric LANGUAGE sql AS $$ SELECT price * (1 + rate); $$; -- 过程:批量更新并提交 CREATE PROCEDURE mark_expired_accounts() LANGUAGE plpgsql AS $$ BEGIN UPDATE accounts SET status = 'expired' WHERE last_login < now() - interval '1 year'; COMMIT; END; $$;
上面的例子中,get_taxed_price 可以用 SELECT get_taxed_price(100) 调用,而 mark_expired_accounts 必须通过 CALL mark_expired_accounts(); 执行。如果把过程放进 SELECT 里,PostgreSQL 会直接报错。选择时记住一个简单判断:如果逻辑需要返回结果给查询使用,就写函数;如果逻辑的核心是修改数据并且希望在执行过程中提交或回滚,就写过程。
二、PL/pgSQL 块结构与参数变量
PL/pgSQL 是 PostgreSQL 最常用的服务端编程语言,它基于 SQL 并增加了变量、条件、循环、异常处理等能力。一个 PL/pgSQL 代码块通常由 DECLARE、BEGIN、EXCEPTION 三部分组成,外层用美元符号引用 $$ 包住,避免单引号转义问题。变量声明放在 DECLARE 中,支持默认值和 CONSTANT 常量。
DO $$ DECLARE total_amount numeric := 0; customer_name varchar(100) := 'guest'; BEGIN RAISE NOTICE 'total is %', total_amount; END; $$;
函数和过程都支持参数,参数模式包括 IN、OUT、INOUT 和 VARIADIC。OUT 参数可以让函数返回多个字段,调用时相当于返回一行记录。INOUT 参数则既能传入也能传出。使用 %TYPE 可以让变量自动继承表字段类型,使用 %ROWTYPE 可以声明整行变量,避免表结构变动时维护大量类型定义。
CREATE FUNCTION split_name(full_name text, OUT first_name text, OUT last_name text)
LANGUAGE plpgsql
AS $$
BEGIN
first_name := split_part(full_name, ' ', 1);
last_name := split_part(full_name, ' ', 2);
END;
$$;
SELECT * FROM split_name('Ada Lovelace');
函数还可以返回表集合,常见做法是声明 RETURNS TABLE 并用 RETURN QUERY 返回查询结果。这种方式适合封装复杂查询,让应用层拿到结构清晰的结果集。对于过程来说,参数模式没有返回类型的问题,但过程仍然可以使用 INOUT 参数向调用方传回少量状态信息,只是不能在 SQL 表达式中使用。
三、控制流程、动态 SQL 与异常处理
PL/pgSQL 的条件判断使用 IF THEN ELSIF ELSE END IF,也可以用 CASE 表达式进行更紧凑的分支。循环结构包括 LOOP 配合 EXIT WHEN、WHILE 循环,以及适合遍历查询结果的 FOR IN SELECT。实际业务里常见的是遍历一批记录逐条处理,但要注意如果单条 SQL 能完成,就不要用循环,因为逐行处理在数据库端的开销远高于集合操作。
CREATE FUNCTION grade_score(score numeric)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
grade text;
BEGIN
IF score >= 90 THEN
grade := 'A';
ELSIF score >= 80 THEN
grade := 'B';
ELSIF score >= 70 THEN
grade := 'C';
ELSE
grade := 'D';
END IF;
RETURN grade;
END;
$$;
动态 SQL 通过 EXECUTE 执行,适合表名、列名或条件结构需要运行时确定的场景。拼接动态 SQL 时不能直接把参数用字符串连起来,否则会带来 SQL 注入风险。format 函数的 %I 用于标识符安全引用,%L 用于字面量安全引用,参数化查询则用 USING 传入值。应优先使用 format 与 USING 的组合。
CREATE FUNCTION count_rows_in_table(table_name text)
RETURNS bigint
LANGUAGE plpgsql
AS $$
DECLARE
result bigint;
BEGIN
EXECUTE format('SELECT count(*) FROM %I', table_name)
INTO result;
RETURN result;
END;
$$;
异常处理使用 EXCEPTION WHEN 子句,可以捕获 unique_violation、foreign_key_violation、check_violation 等常见错误。还可以通过 GET STACKED DIAGNOSTICS 获取更详细的错误信息。需要注意的是,PL/pgSQL 里的异常处理块会形成一个子事务,进入该块时会有额外开销,所以不要用异常处理代替正常的业务判断。
CREATE PROCEDURE safe_insert_user(user_email text)
LANGUAGE plpgsql
AS $$
BEGIN
BEGIN
INSERT INTO users(email) VALUES(user_email);
EXCEPTION
WHEN unique_violation THEN
RAISE NOTICE 'duplicate email: %', user_email;
END;
END;
$$;
四、权限、调试与性能优化
创建函数或过程时,默认使用调用者的权限执行,也就是 SECURITY INVOKER。如果希望封装一些对表的受限访问,可以声明 SECURITY DEFINER,让逻辑以创建者权限运行。但 SECURITY DEFINER 必须配合 search_path 设置,否则攻击者可能通过修改 search_path 劫持函数内部引用的对象。下面这个函数固定了 search_path,可以降低这类风险。
CREATE FUNCTION public.get_active_users() RETURNS SETOF users LANGUAGE sql SECURITY DEFINER SET search_path = public AS $$ SELECT * FROM users WHERE active = true; $$;
调试 PL/pgSQL 并不像普通编程语言那样有丰富的断点工具。最常用的手段是在过程中临时加入 RAISE NOTICE 输出中间变量,配合 EXPLAIN ANALYZE 观察函数执行计划和真实耗时。也可以开启 auto_explain 模块记录包含函数调用的慢查询,对定位性能瓶颈很有帮助。
性能方面,函数和过程默认被标记为 VOLATILE,意味着每次调用都可能返回不同结果,优化器不能缓存或内联。如果函数逻辑确定且与底层表数据无关,可以标记为 IMMUTABLE;如果同一事务内结果稳定,可以标记为 STABLE。对于纯 SQL 函数,加上这些标记后,PostgreSQL 有可能将其内联到外层查询,从而减少调用开销。过程因为是用于数据修改,一般不适合这类标记。另一个性能误区是在 PL/pgSQL 循环里反复执行单条 SQL,应当尽量改成基于集合的 UPDATE ... FROM 或 INSERT ... SELECT。
五、入门选择与编写清单
刚开始接触 PostgreSQL 时,不需要强行区分自己写的是不是“存储过程”。可以先从函数入手,因为函数更通用,能被查询、视图和触发器复用。等遇到需要分批提交的批量任务时,再改用过程。写任何服务端例程前,建议先明确三件事:输入参数是否允许为空、返回值是标量还是集合、逻辑是否需要事务控制。这三点会直接影响签名设计和错误处理方式。
编写过程中应保持一个习惯:能用一个 SQL 语句完成的逻辑,不要用 PL/pgSQL 循环;能用参数化动态 SQL 的地方,不要拼接用户输入;能给函数标上 STABLE 或 IMMUTABLE 的地方,不要保留默认 VOLATILE。这样写出来的数据库程序不仅好调试,也更安全、更容易被优化器接受。
PostgreSQL存储过程PL/pgSQLPostgreSQL函数修改时间:2026-09-26 00:31:06