函数结构(DECLARE / BEGIN / END)
一个 PL/pgSQL 函数由函数头与函数体两部分构成:函数头声明名称、参数、返回类型与属性,函数体则是 [ <<label>> ] [ DECLARE ... ] BEGIN ... END 这样一层套一层的块。本节把这个结构彻底拆开:块的语法与三条硬性规则、嵌套块与变量遮蔽、参数模式、各种 RETURNS 形式、函数属性、OR REPLACE 的限制,以及如何查看与维护已建函数。
前置知识
阅读本节前,建议先了解:PL/pgSQL 概述
基础概念
块(Block)是 PL/pgSQL 中唯一的结构单位。DECLARE、BEGIN、END 三个关键字把一个块切成三段,加上可选的块标签(Label),就构成了完整的语法。
| 概念 | 术语 | 说明 |
|---|---|---|
| 块标签 | Block Label | <<label>> 形式,给块命名,用于限定变量与跳转 |
| 声明段 | Declaration Section | DECLARE 与 BEGIN 之间,逐条声明变量、常量、游标 |
| 语句段 | Statement Section | BEGIN 与 END 之间,真正被执行的语句序列 |
| 嵌套块 | Nested Block | 写在语句段里、可无限层嵌套的内层块 |
| 遮蔽 | Shadowing | 内层同名声明覆盖外层,外层需用 外层标签.名字 访问 |
| 函数头 | Function Header | CREATE FUNCTION 到 AS 之间的全部声明 |
| 函数体 | Function Body | AS 后面那段字符串,包含块结构 |
| 参数模式 | Argument Mode | IN / OUT / INOUT / VARIADIC |
| 易变性 | Volatility | IMMUTABLE / STABLE / VOLATILE |
| 严格性 | Strictness | STRICT(等价 RETURNS NULL ON NULL INPUT)与默认的 CALLED ON NULL INPUT |
块的三条硬性规则
这三条规则是新手写 plpgsql 时最常踩的坑,先集中记住:
| 规则 | 正确写法 | 错误写法 | 说明 |
|---|---|---|---|
BEGIN 后不能跟分号 | BEGIN | BEGIN; | 带分号后变成「空语句 + 语法错误」 |
块内的 END 后必须跟分号 | END; | END | 内层块没有分号会连带污染外层解析 |
函数体最外层的 END 可以不带分号 | END 或 END; | — | 官方明确说明最后那个 END 不要求分号 |
用一张图表示块与分号的对应关系:
CREATE FUNCTION f() RETURNS int LANGUAGE plpgsql AS $$
DECLARE <- 声明段开始
v_a int := 1; <- 声明也要分号
BEGIN <- 不能写成 BEGIN;
v_a := v_a + 1; <- 语句要分号
DECLARE <- 内层块
v_b int := 2;
BEGIN
v_a := v_a + v_b;
END; <- 内层 END 必须分号
RETURN v_a;
END <- 函数体最后的 END,分号可有可无
$$;函数头的组成
| 组成部分 | 是否必需 | 例子 |
|---|---|---|
| 名称 | 必需 | app_calc_tax |
| 参数列表 | 必需(可为空) | (p_amount numeric, p_rate numeric DEFAULT 0.06) |
| 返回声明 | 函数必需,过程禁止 | RETURNS numeric / RETURNS TABLE(...) |
| 语言 | 必需(sql 标准体除外) | LANGUAGE plpgsql |
| 易变性 | 可选,默认 VOLATILE | IMMUTABLE |
| 严格性 | 可选,默认 CALLED ON NULL INPUT | STRICT |
| 安全上下文 | 可选,默认 SECURITY INVOKER | SECURITY DEFINER |
| 并行安全性 | 可选,默认 PARALLEL UNSAFE | PARALLEL SAFE |
| 规划器提示 | 可选 | COST 100 / ROWS 1000 |
| 会话参数 | 可选 | SET search_path = pg_catalog, pg_temp |
语法与用法
块结构的语法原型:
[ <<label>> ]
[ DECLARE
声明 ... ]
BEGIN
语句 ...
END [ label ];CREATE FUNCTION 中块的位置:
CREATE [ OR REPLACE ] FUNCTION 名称 ( [ 参数模式 ] [ 参数名 ] 参数类型 [ DEFAULT 默认值 ] [, ...] )
[ RETURNS 返回类型 | RETURNS TABLE ( 列名 列类型 [, ...] ) ]
LANGUAGE plpgsql
[ 属性子句 ... ]
AS $$
[ <<label>> ]
[ DECLARE
声明 ... ]
BEGIN
语句 ...
END [ label ];
$$;最小可运行示例——演示声明段与语句段的分工:
CREATE OR REPLACE FUNCTION demo_block_demo(p_base integer)
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_total integer := 0; -- 声明段:进入块时初始化一次
v_step constant integer := 5; -- 常量:赋值后不可修改
BEGIN
v_total := p_base + v_step; -- 语句段:赋值用 :=
RETURN v_total;
END;
$$;SELECT demo_block_demo(10) AS result; result
--------
15
(1 row)带标签与嵌套块的示例:
CREATE OR REPLACE FUNCTION demo_nested_block()
RETURNS text
LANGUAGE plpgsql
AS $$
<< outer_block >>
DECLARE
v_qty integer := 30;
BEGIN
-- 内层块遮蔽了外层同名变量
DECLARE
v_qty integer := 80;
BEGIN
RAISE NOTICE '内层 v_qty = %', v_qty; -- 80
RAISE NOTICE '外层 v_qty = %', outer_block.v_qty; -- 30
END;
RAISE NOTICE '回到外层 v_qty = %', v_qty; -- 30
RETURN 'done';
END;
$$;SELECT demo_nested_block();NOTICE: 内层 v_qty = 80
NOTICE: 外层 v_qty = 30
NOTICE: 回到外层 v_qty = 30
demo_nested_block
-------------------
done
(1 row)详细说明
块标签:<<label>>
标签写在块的最前面,可以放在 DECLARE 之前,也可以放在 BEGIN 之前(没有声明段时)。
标签的两个实际用途:
| 用途 | 写法 | 场景 |
|---|---|---|
| 限定变量名 | outer_block.v_qty | 内层声明了同名变量,需要访问外层那个 |
| 限定跳转目标 | EXIT outer_loop; / CONTINUE outer_loop; | 从内层循环直接跳出多层循环 |
函数体最外层还有一个隐式标签,就是函数名本身。 也就是说,在函数内部可以用 函数名.变量名 访问最外层的变量或参数,这在变量名被内层遮蔽时非常有用。例如函数 demo_fn(p_id integer) 内部若又声明了同名的局部变量,可用 demo_fn.p_id 拿到参数。
嵌套块与变量遮蔽
内层块可以访问外层块声明的所有变量和参数;内层若声明了同名变量,就产生了遮蔽(Shadowing)——内层范围内该名字指向内层变量。上面 demo_nested_block 的输出已经展示了这一点:内层打印 80,用 outer_block.v_qty 限定后拿到外层的 30,内层块退出后再读又是 30。
可以在会话级别打开遮蔽告警,提前发现这类潜在错误:
SET plpgsql.extra_warnings = 'shadowed_variables';| 检查项 | 作用 |
|---|---|
shadowed_variables | 内层声明遮蔽外层时告警 |
strict_multi_assignment | SELECT INTO 的列数与变量数不匹配时告警 |
too_many_rows | SELECT INTO 返回多行(非 STRICT)时告警 |
生命周期上,内层块每次进入都会重新求值默认值,内层变量在 END 之后不可见也不再存在:
外层块进入
|-- 外层声明:v_msg := 'outer'
|-- 外层语句
| |
| |-- 内层块进入 —— 内层声明 v_msg := 'inner'(新的变量槽位)
| |-- 内层语句(v_msg 指向内层)
| |-- 内层块退出 —— 内层变量销毁
| |
|-- 外层语句(v_msg 重新指回外层)
|-- 外层块退出参数模式:IN / OUT / INOUT / VARIADIC
CREATE FUNCTION 的参数可以带模式关键字。不带时默认是 IN。
| 模式 | 含义 | 能否有默认值 | 对返回值的影响 |
|---|---|---|---|
IN | 只输入(默认) | 可以 | 无 |
OUT | 只输出 | 可以 | 构成结果行,多个 OUT 参数等价于 RETURNS TABLE |
INOUT | 既输入又输出 | 可以 | 构成结果行 |
VARIADIC | 可变参数,必须是最后一个 | 不可以 | 作为普通输入参数处理 |
OUT 参数的关键性质:函数调用时不需要为它传值,且它一旦出现,RETURNS 的语义就被「结果行由 OUT 参数组成」接管。
-- 一个 OUT 参数:函数返回该参数的类型
CREATE OR REPLACE FUNCTION demo_out_one(p_a integer, p_b integer, OUT o_sum integer)
LANGUAGE plpgsql
AS $$
BEGIN
o_sum := p_a + p_b; -- 给 OUT 参数赋值即为设置返回值
END;
$$;
SELECT demo_out_one(3, 4) AS sum_result; sum_result
------------
7
(1 row)-- 多个 OUT 参数:结果的列名就是参数名
CREATE OR REPLACE FUNCTION demo_out_many(p_a integer, p_b integer,
OUT o_sum integer, OUT o_diff integer)
LANGUAGE plpgsql
AS $$
BEGIN
o_sum := p_a + p_b;
o_diff := p_a - p_b;
END;
$$;
SELECT * FROM demo_out_many(10, 3); o_sum | o_diff
-------+--------
13 | 7
(1 row)INOUT 参数既要在调用时传值,也会出现在结果行里:
CREATE OR REPLACE FUNCTION demo_inout(p_val INOUT integer, p_add integer)
LANGUAGE plpgsql
AS $$
BEGIN
p_val := p_val + p_add; -- 读旧值、写新值
END;
$$;
SELECT demo_inout(100, 23) AS updated; updated
---------
123
(1 row)VARIADIC 参数把调用时多余的实际参数收集成一个数组。它必须是参数列表中的最后一个,且类型必须声明成数组类型。
CREATE OR REPLACE FUNCTION demo_variadic(VARIADIC p_nums integer[])
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_total integer := 0;
v_i integer;
BEGIN
-- 数组下标从 1 开始
FOR v_i IN 1 .. array_length(p_nums, 1) LOOP
v_total := v_total + p_nums[v_i];
END LOOP;
RETURN v_total;
END;
$$;
-- 两种调用方式等价
SELECT demo_variadic(1, 2, 3, 4) AS by_list,
demo_variadic(VARIADIC ARRAY[1,2,3,4]) AS by_array; by_list | by_array
---------+----------
10 | 10
(1 row)VARIADIC 不能与 OUT 混用在同一函数上,也不能给默认值。另外,若要「原样传一个数组」而不是把数组拆成多个参数,调用时必须写 VARIADIC 关键字,否则数组会被当作单个元素。
RETURNS 的各种形式
| 返回声明 | 返回内容 | 典型用法 |
|---|---|---|
RETURNS void | 无值 | 只想产生副作用(写库、发通知) |
RETURNS 类型 | 单个标量或复合值 | 计算、转换、查找单个值 |
RETURNS 表名 | 该表的整行类型 | 返回表的一行 |
RETURNS SETOF 类型 | 零到多行 | 返回多行单列或多行整行 |
RETURNS TABLE(列 类型, ...) | 零到多行、多列 | 等价于多个 OUT 参数 |
RETURNS record | 单行的匿名记录 | 列结构在调用时才确定 |
RETURNS SETOF record | 多行的匿名记录 | 调用时必须附列定义清单 |
对应的返回方式也分几种:
| 返回方式 | 适用返回声明 | 说明 |
|---|---|---|
RETURN 表达式; | 非集合 | 立即结束函数并返回 |
RETURN; | void、过程 | 不做任何事地结束 |
RETURN NEXT 表达式; | SETOF | 追加一行,函数继续执行 |
RETURN QUERY 查询; | SETOF | 把查询结果整体追加为多行 |
RETURN QUERY EXECUTE 字符串; | SETOF | 动态 SQL 版的 RETURN QUERY |
CREATE OR REPLACE FUNCTION demo_setof_lines(p_count integer)
RETURNS SETOF text
LANGUAGE plpgsql
AS $$
DECLARE
i integer;
BEGIN
FOR i IN 1 .. p_count LOOP
RETURN NEXT 'line ' || i; -- 每轮追加一行
END LOOP;
END;
$$;
SELECT * FROM demo_setof_lines(3); demo_setof_lines
-------------------
line 1
line 2
line 3
(3 rows)RETURNS TABLE(列 类型, ...) 与「多个 OUT 参数」在语义上完全等价,只是写法不同:前者的列名写在 RETURNS TABLE 里,后者的列名就是参数名。两者的函数体都只需给这些名字赋值,然后 RETURN; 结束。
函数属性详解
易变性(Volatility)决定优化器对函数的信任程度,是属性里对性能影响最大的一项:
| 属性 | 优化器可以做的假设 | 何时使用 | 反例 |
|---|---|---|---|
IMMUTABLE | 相同参数永远返回相同结果,不读数据库 | 纯计算、upper()、数学运算 | 读了表却标 IMMUTABLE → 结果被错误缓存 |
STABLE | 单条语句内多次调用结果一致,可读库 | 查询类函数、now() 语义 | 内部写库却标 STABLE |
VOLATILE(默认) | 无任何假设,每次调用都执行 | 会写库、含 random() / nextval() | 纯函数标 VOLATILE → 丧失优化机会 |
-- 推荐:纯计算标 IMMUTABLE,可参与索引表达式与常量折叠
CREATE OR REPLACE FUNCTION app_tax(p_amount numeric)
RETURNS numeric
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
RETURN round(p_amount * 1.06, 2);
END;
$$;严格性(Strictness)决定入参为 NULL 时的行为:
| 属性 | 行为 | 适用场景 |
|---|---|---|
CALLED ON NULL INPUT(默认) | 即使参数为 NULL 也照常执行 | 需要在函数内自行处理 NULL |
STRICT(等价 RETURNS NULL ON NULL INPUT) | 任一参数为 NULL 时直接返回 NULL,不执行函数体 | 「有 NULL 就没意义」的纯函数 |
-- 推荐:参数为 NULL 时无意义的函数用 STRICT,省去手工判空
CREATE OR REPLACE FUNCTION app_upper(p_text text)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE STRICT
AS $$
BEGIN
RETURN upper(p_text);
END;
$$;
SELECT app_upper(NULL) IS NULL AS strict_returns_null; strict_returns_null
---------------------
t
(1 row)安全上下文(Security Context)决定函数以谁的权限运行:
| 属性 | 运行身份 | 用途 | 风险 |
|---|---|---|---|
SECURITY INVOKER(默认) | 调用者 | 普通函数 | 无 |
SECURITY DEFINER | 函数所有者 | 让无权限用户间接做受限操作 | 若 search_path 未收紧,可被劫持提权 |
-- 推荐:SECURITY DEFINER 必须同时固定 search_path
CREATE OR REPLACE FUNCTION app_get_secret_row(p_id bigint)
RETURNS text
LANGUAGE plpgsql
STABLE
SECURITY DEFINER
SET search_path = pg_catalog, pg_temp
AS $$
DECLARE
v_result text;
BEGIN
SELECT msg INTO v_result FROM public.demo_batch_log WHERE id = p_id;
RETURN v_result;
END;
$$;并行安全性(Parallel Safety)决定函数能否出现在并行查询中:
| 取值 | 含义 | 例子 |
|---|---|---|
PARALLEL UNSAFE(默认) | 禁止并行执行 | 会写库、修改会话状态 |
PARALLEL RESTRICTED | 只能在并行领导进程中执行 | 访问临时表、游标、setseed() |
PARALLEL SAFE | 可安全并行 | 纯计算函数 |
规划器提示只影响成本估算,不影响正确性:
| 属性 | 默认值 | 说明 |
|---|---|---|
COST 执行代价 | 非 C 语言函数为 100,C 函数为 1 | 每行调用一次的相对代价 |
ROWS 预估行数 | 1000 | 集合返回函数的预估返回行数,仅对 SETOF 有意义 |
SUPPORT 支撑函数 | 无 | 为规划器提供额外优化的内部函数 |
CREATE OR REPLACE 的限制
OR REPLACE 听起来像「可以随便改」,但它有明确的边界:
能否用 OR REPLACE 改 | 说明 |
|---|---|
| 函数体 | 可以 |
| 易变性、严格性、安全属性 | 可以 |
COST / ROWS / PARALLEL | 可以 |
SET 子句 | 可以(会整体替换,不是追加) |
OUT 参数的名字 | 可以 |
| 参数类型序列(签名) | 不可以,改了就是另一个函数 |
| 返回类型 | 不可以,会报错 cannot change return type of existing function |
| 函数名 | 不可以,用 ALTER FUNCTION ... RENAME TO |
-- 错误!返回类型不能通过 OR REPLACE 修改
CREATE OR REPLACE FUNCTION app_upper(p_text text)
RETURNS varchar -- 原来是 text
LANGUAGE plpgsql
IMMUTABLE STRICT
AS $$
BEGIN
RETURN upper(p_text);
END;
$$;ERROR: cannot change return type of existing function
HINT: Use DROP FUNCTION app_upper(text) first.正确做法是先删除、再创建:
-- 推荐:先 DROP 再 CREATE(注意 DROP 必须给出参数类型)
DROP FUNCTION IF EXISTS app_upper(text);
CREATE FUNCTION app_upper(p_text text)
RETURNS varchar
LANGUAGE plpgsql IMMUTABLE STRICT
AS $$
BEGIN
RETURN upper(p_text);
END;
$$;维护函数:ALTER / DROP / \sf
ALTER FUNCTION 能改的东西:
| 变更项 | 语句 |
|---|---|
| 改名 | ALTER FUNCTION f(int) RENAME TO g; |
| 改所有者 | ALTER FUNCTION f(int) OWNER TO app_owner; |
| 改模式(schema) | ALTER FUNCTION f(int) SET SCHEMA app; |
| 改易变性 | ALTER FUNCTION f(int) IMMUTABLE; |
| 改严格性 | ALTER FUNCTION f(int) STRICT; |
| 改安全上下文 | ALTER FUNCTION f(int) SECURITY DEFINER; |
| 改并行性 | ALTER FUNCTION f(int) PARALLEL SAFE; |
改 COST / ROWS | ALTER FUNCTION f(int) COST 500; |
| 设置会话参数 | ALTER FUNCTION f(int) SET search_path = pg_catalog, pg_temp; |
| 重置会话参数 | ALTER FUNCTION f(int) RESET search_path; |
DROP FUNCTION 必须给出参数类型序列,不能只给名字,也不能带参数名:
-- 错误!DROP 不接受参数名
DROP FUNCTION app_upper(p_text text);
-- 错误!同名函数有重载时,只给名字会报歧义
DROP FUNCTION app_upper;
-- 推荐:给完整签名
DROP FUNCTION IF EXISTS app_upper(text);
-- 有依赖对象时用 CASCADE 级联删除(谨慎)
DROP FUNCTION IF EXISTS app_upper(text) CASCADE;查看与导出函数定义的三种手段:
-- 1) psql 元命令:列出函数签名
\df demo_* List of functions
Schema | Name | Result data type | Argument data types | Type
--------+---------------------+------------------+---------------------+------
public | demo_block_demo | integer | p_base integer | func
public | demo_nested_block | text | | func
public | demo_out_many | record | p_a integer, ... | func
(3 rows)-- 2) psql 元命令:显示函数定义体
\sf demo_block_demoCREATE OR REPLACE FUNCTION public.demo_block_demo(p_base integer)
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS $function$
DECLARE
v_total integer := 0;
v_step constant integer := 5;
BEGIN
v_total := p_base + v_step;
RETURN v_total;
END;
$function$-- 3) 函数式接口:导出完整定义,便于版本管理
SELECT pg_get_functiondef('demo_block_demo(integer)'::regprocedure);实战示例
示例一:用 OUT 参数实现「一次调用返回多项统计」
需求:给定商品分类,一次返回商品数、最高价、平均价,避免三次往返。
CREATE TABLE IF NOT EXISTS demo_products(
id bigserial PRIMARY KEY,
category text,
price numeric(10,2)
);
INSERT INTO demo_products(category, price) VALUES
('book', 39.90), ('book', 59.50), ('book', 25.00),
('pen', 3.50), ('pen', 8.00);
CREATE OR REPLACE FUNCTION app_category_stats(
p_category text,
OUT o_count bigint,
OUT o_max numeric,
OUT o_avg numeric
)
LANGUAGE plpgsql
STABLE
AS $$
BEGIN
SELECT count(*), max(price), round(avg(price), 2)
INTO o_count, o_max, o_avg
FROM demo_products
WHERE category = p_category;
END;
$$;
SELECT * FROM app_category_stats('book'); o_count | o_max | o_avg
---------+-------+-------
3 | 59.50 | 41.47
(1 row)示例二:带 VARIADIC 的灵活聚合函数
需求:计算任意个整数的最大值,且支持「传入一个数组」两种调用方式。
CREATE OR REPLACE FUNCTION app_max_of(VARIADIC p_nums integer[])
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE STRICT
AS $$
DECLARE
v_max integer;
v_i integer;
BEGIN
v_max := p_nums[1];
FOR v_i IN 2 .. array_length(p_nums, 1) LOOP
IF p_nums[v_i] > v_max THEN
v_max := p_nums[v_i];
END IF;
END LOOP;
RETURN v_max;
END;
$$;
SELECT app_max_of(4, 19, 7) AS list_form,
app_max_of(VARIADIC ARRAY[4, 19, 7]) AS array_form; list_form | array_form
-----------+------------
19 | 19
(1 row)示例三:验证块的生命周期与默认值求值时机
需求:证明每次进入块都会重新求值默认值——这是块结构中最容易被误解的一点。
CREATE OR REPLACE FUNCTION demo_default_timing(p_rounds integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_lines text := '';
i integer;
BEGIN
FOR i IN 1 .. p_rounds LOOP
DECLARE
v_stamp timestamptz := clock_timestamp(); -- 每轮进入内层块都重新求值
BEGIN
v_lines := v_lines || to_char(v_stamp, 'HH24:MI:SS.MS') || ' ';
PERFORM pg_sleep(0.02); -- 制造时间差
END;
END LOOP;
RETURN v_lines;
END;
$$;
SELECT demo_default_timing(3) AS stamps; stamps
----------------------------------------
10:22:31.104 10:22:31.126 10:22:31.148
(1 row)三次时间戳各不相同,说明内层块的 DECLARE 在每轮进入时都执行了。与之对比,外层块的 v_lines 只在函数开始执行时求值一次。
示例四:函数属性的实际影响
需求:对比 IMMUTABLE STRICT 与默认 VOLATILE 在 EXPLAIN 下的查询计划差异。
CREATE TABLE IF NOT EXISTS demo_attr_t(
id bigserial PRIMARY KEY,
code text
);
INSERT INTO demo_attr_t(code) SELECT 'C' || i FROM generate_series(1, 5000) AS i;
ANALYZE demo_attr_t;-- 推荐:IMMUTABLE 让常量折叠生效
CREATE OR REPLACE FUNCTION app_norm_code(p_code text)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE STRICT
AS $$
BEGIN
RETURN upper(trim(p_code));
END;
$$;
EXPLAIN (COSTS OFF)
SELECT * FROM demo_attr_t WHERE code = app_norm_code(' c123 '); QUERY PLAN
----------------------------------------------
Index Scan using demo_attr_t_pkey on demo_attr_t
Index Cond: (code = 'C123'::text)
(2 rows)可以看到,IMMUTABLE 让 app_norm_code(' c123 ') 在规划期就被折叠成一个常量 'C123',从而使索引条件可用。若函数是默认的 VOLATILE,这种折叠不会发生。
示例五:给变更返回类型的函数补齐权限与注释
需求:把一个返回 text 的函数改成返回 varchar。由于必须 DROP 重建,权限与注释会一并丢失,需要手工补回。
-- 先记录原有所有者、权限与注释,便于重建后还原
SELECT pg_get_userbyid(p.proowner) AS owner,
d.description AS comment
FROM pg_proc p
LEFT JOIN pg_description d ON d.objoid = p.oid AND d.objsubid = 0
WHERE p.proname = 'app_upper';
-- 推荐:DROP + CREATE,再补回注释与授权(否则重建后 EXECUTE 权限会回到默认状态)
DROP FUNCTION IF EXISTS app_upper(text);
CREATE FUNCTION app_upper(p_text text)
RETURNS varchar
LANGUAGE plpgsql IMMUTABLE STRICT
AS $$
BEGIN
RETURN upper(p_text);
END;
$$;
COMMENT ON FUNCTION app_upper(text) IS '转大写;STRICT,输入 NULL 返回 NULL';
GRANT EXECUTE ON FUNCTION app_upper(text) TO PUBLIC;DROP FUNCTION
CREATE FUNCTION
COMMENT
GRANT注意事项
BEGIN之后不能写分号。这是 plpgsql 与 SQL 脚本最直观的区别,BEGIN;会立刻报syntax error at or near ";"。块内
END必须带分号,函数体最后的END才可以不带。DECLARE必须写在BEGIN之前。想在语句中间声明变量,只能新开一个内层块,把DECLARE放进内层。VARIADIC只能出现在参数列表的最后,且类型必须是数组;调用时若要传整个数组,必须带VARIADIC关键字。OUT参数会改变返回值语义。一旦函数有了OUT参数,就不能再写RETURNS 某类型(唯一例外是RETURNS SETOF与OUT的组合使用需谨慎,官方限制较多)。STRICT与SECURITY DEFINER是可以叠加的,但STRICT的判断发生在权限检查之后——不要用它来规避 NULL 引起的越权。IMMUTABLE标错会造成数据错误,而不只是性能问题:它可能被用于索引表达式,一旦函数依赖会话或数据,索引就会与真实值不一致。ALTER FUNCTION不能改参数类型或返回类型,这两项只能DROP重建。DROP FUNCTION必须给出参数类型,且要留意依赖对象会被CASCADE一并删除。
常见错误一览:
| 错误现象 | SQLSTATE | 常见原因 |
|---|---|---|
syntax error at or near ";" | 42601 | BEGIN; 或 END IF 缺分号 |
cannot change return type of existing function | 42P13 | 用 OR REPLACE 改返回类型 |
cannot change name of input parameter | 42P13 | 用 OR REPLACE 改输入参数名 |
function "f" does not exist | 42883 | 参数类型序列不匹配 |
function name "f" is not unique | 42725 | 重载导致歧义,需写成 f(...) 或强制转换实参 |
VARIADIC parameter must be the last input parameter | 42P13 | VARIADIC 放在了中间 |
column "x" does not exist | 42703 | 块的作用域不对,变量在内层块外不可见 |
最佳实践
函数体一律用美元引用,体内含
$$时外层换命名标签$func$/$body$。严格区分声明段与语句段:所有变量、常量、游标放
DECLARE;不要在BEGIN之后临时「声明」变量。sql-- 不推荐:想中途声明变量而强行嵌套,可读性差 BEGIN ... DECLARE v_x int := 1; BEGIN ... END; END; -- 推荐:一次性声明在顶部,语义更清晰 DECLARE v_x int := 1; BEGIN ... END;命名约定挡住遮蔽问题:参数
p_前缀、局部变量v_前缀、常量c_前缀、游标cur_前缀。只在需要跨多层跳转或明确限定变量时才用块标签,并为标签取有意义的名称(如
retry_block、item_loop)。显式声明易变性,绝不依赖默认
VOLATILE:函数行为 声明 纯计算 IMMUTABLE读表、语句内稳定 STABLE写库、有副作用 VOLATILE(显式写出来)「任一参数为 NULL 则无意义」时用
STRICT,能省掉一层判空代码,也让优化器知道可跳过调用。SECURITY DEFINER必须配SET search_path = pg_catalog, pg_temp,并把函数放在固定 schema 下,避免被同名对象劫持。优先用
OUT参数或RETURNS TABLE一次返回多项结果,而不是让客户端连发多条SELECT。sql-- 推荐:一次往返拿到多个统计量 SELECT * FROM app_category_stats('book'); -- 不推荐:三次往返 SELECT count(*) FROM demo_products WHERE category = 'book'; SELECT max(price) FROM demo_products WHERE category = 'book'; SELECT avg(price) FROM demo_products WHERE category = 'book';改函数遵守「先
DROP再CREATE」,只有「函数体微调、签名与返回类型完全不变」时才用OR REPLACE。把
pg_get_functiondef()的输出纳入版本管理,并在迁移脚本里用DROP FUNCTION IF EXISTS 名(签名);保证幂等。每次新建函数后立即调用一次自检,把体内语法错误挡在部署之前。
打开
plpgsql.extra_warnings,让遮蔽、多列赋值不匹配、SELECT INTO多行这类隐性错误提前暴露。
下一节
下一节我们学习:变量声明与赋值,掌握 DECLARE 的完整语法、%TYPE / %ROWTYPE 的用法与 SELECT INTO 的陷阱。
参考链接
- PostgreSQL 18 官方文档 - PL/pgSQL 结构
- PostgreSQL 18 官方文档 - PL/pgSQL 声明
- PostgreSQL 18 官方文档 - CREATE FUNCTION
- PostgreSQL 18 官方文档 - ALTER FUNCTION
- PostgreSQL 18 官方文档 - DROP FUNCTION
- PostgreSQL 18 官方文档 - CREATE PROCEDURE
- PostgreSQL 18 官方文档 - 函数易变性分类
- PostgreSQL 18 官方文档 - pg_proc 系统目录
- PostgreSQL 18 官方文档 - psql 元命令(\df / \sf)