导读:本期聚焦于唐振业创作的《PostgreSQL存储过程和函数有什么区别?入门应该先学哪个?》,敬请观看详情。在 PostgreSQL 里,存储过程和函数是两种不同的服务端例程,但不少从其他数据库迁移过来的开发者会习惯性混用。函数用 CREATE FUNCTION 定义,必须有返回类型,可以嵌入 SELECT、WHERE 等 SQL 表达式,适合做计算、转换和结果集封装;过程用 CREATE PROCEDURE 定义,从 PostgreSQL 11 起能够执行 COMMIT 和 ROLLBACK,只能通过 CALL 调用,适合需要分批提交的批量数据变更。本文从两者差异讲起,覆盖 PL/pgSQL 基本结构、变量声明、参数模式、返回类型、IF 与循环控制、异常捕获以及动态 SQL 的写法,并给出可运行示例。最后补充 SECURITY DEFINER、调试手段和性能标记等实用要点,帮助你从零开始写出安全、可维护的 PostgreSQL 服务端程序。

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

PostgreSQL存储过程和函数有什么区别?入门应该先学哪个?

一、函数和过程的本质区别

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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0926/61918.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。