Skip to content

函数结构(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 SectionDECLARE 与 BEGIN 之间,逐条声明变量、常量、游标
语句段Statement SectionBEGIN 与 END 之间,真正被执行的语句序列
嵌套块Nested Block写在语句段里、可无限层嵌套的内层块
遮蔽Shadowing内层同名声明覆盖外层,外层需用 外层标签.名字 访问
函数头Function HeaderCREATE FUNCTION 到 AS 之间的全部声明
函数体Function BodyAS 后面那段字符串,包含块结构
参数模式Argument ModeIN / OUT / INOUT / VARIADIC
易变性VolatilityIMMUTABLE / STABLE / VOLATILE
严格性StrictnessSTRICT(等价 RETURNS NULL ON NULL INPUT)与默认的 CALLED ON NULL INPUT

块的三条硬性规则 ​

这三条规则是新手写 plpgsql 时最常踩的坑,先集中记住:

规则正确写法错误写法说明
BEGIN 后不能跟分号BEGINBEGIN;带分号后变成「空语句 + 语法错误」
块内的 END 后必须跟分号END;END内层块没有分号会连带污染外层解析
函数体最外层的 END 可以不带分号END 或 END;—官方明确说明最后那个 END 不要求分号

用一张图表示块与分号的对应关系:

text
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
易变性可选,默认 VOLATILEIMMUTABLE
严格性可选,默认 CALLED ON NULL INPUTSTRICT
安全上下文可选,默认 SECURITY INVOKERSECURITY DEFINER
并行安全性可选,默认 PARALLEL UNSAFEPARALLEL SAFE
规划器提示可选COST 100 / ROWS 1000
会话参数可选SET search_path = pg_catalog, pg_temp

语法与用法 ​

块结构的语法原型:

text
[ <<label>> ]
[ DECLARE
    声明 ... ]
BEGIN
    语句 ...
END [ label ];

CREATE FUNCTION 中块的位置:

text
CREATE [ OR REPLACE ] FUNCTION 名称 ( [ 参数模式 ] [ 参数名 ] 参数类型 [ DEFAULT 默认值 ] [, ...] )
  [ RETURNS 返回类型 | RETURNS TABLE ( 列名 列类型 [, ...] ) ]
  LANGUAGE plpgsql
  [ 属性子句 ... ]
AS $$
  [ <<label>> ]
  [ DECLARE
      声明 ... ]
  BEGIN
      语句 ...
  END [ label ];
$$;

最小可运行示例——演示声明段与语句段的分工:

sql
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;
$$;
sql
SELECT demo_block_demo(10) AS result;
text
 result
--------
     15
(1 row)

带标签与嵌套块的示例:

sql
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;
$$;
sql
SELECT demo_nested_block();
text
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。

可以在会话级别打开遮蔽告警,提前发现这类潜在错误:

sql
SET plpgsql.extra_warnings = 'shadowed_variables';
检查项作用
shadowed_variables内层声明遮蔽外层时告警
strict_multi_assignmentSELECT INTO 的列数与变量数不匹配时告警
too_many_rowsSELECT INTO 返回多行(非 STRICT)时告警

生命周期上,内层块每次进入都会重新求值默认值,内层变量在 END 之后不可见也不再存在:

text
外层块进入
  |-- 外层声明: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 参数组成」接管。

sql
-- 一个 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;
text
 sum_result
------------
          7
(1 row)
sql
-- 多个 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);
text
 o_sum | o_diff
-------+--------
    13 |      7
(1 row)

INOUT 参数既要在调用时传值,也会出现在结果行里:

sql
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;
text
 updated
---------
     123
(1 row)

VARIADIC 参数把调用时多余的实际参数收集成一个数组。它必须是参数列表中的最后一个,且类型必须声明成数组类型。

sql
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;
text
 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
sql
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);
text
  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 → 丧失优化机会
sql
-- 推荐:纯计算标 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 就没意义」的纯函数
sql
-- 推荐:参数为 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;
text
 strict_returns_null
---------------------
 t
(1 row)

安全上下文(Security Context)决定函数以谁的权限运行:

属性运行身份用途风险
SECURITY INVOKER(默认)调用者普通函数无
SECURITY DEFINER函数所有者让无权限用户间接做受限操作若 search_path 未收紧,可被劫持提权
sql
-- 推荐: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
sql
-- 错误!返回类型不能通过 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;
$$;
text
ERROR:  cannot change return type of existing function
HINT:  Use DROP FUNCTION app_upper(text) first.

正确做法是先删除、再创建:

sql
-- 推荐:先 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 / ROWSALTER 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 必须给出参数类型序列,不能只给名字,也不能带参数名:

sql
-- 错误!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;

查看与导出函数定义的三种手段:

sql
-- 1) psql 元命令:列出函数签名
\df demo_*
text
                             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)
sql
-- 2) psql 元命令:显示函数定义体
\sf demo_block_demo
text
CREATE 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$
sql
-- 3) 函数式接口:导出完整定义,便于版本管理
SELECT pg_get_functiondef('demo_block_demo(integer)'::regprocedure);

实战示例 ​

示例一:用 OUT 参数实现「一次调用返回多项统计」 ​

需求:给定商品分类,一次返回商品数、最高价、平均价,避免三次往返。

sql
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');
text
 o_count | o_max | o_avg
---------+-------+-------
       3 | 59.50 | 41.47
(1 row)

示例二:带 VARIADIC 的灵活聚合函数 ​

需求:计算任意个整数的最大值,且支持「传入一个数组」两种调用方式。

sql
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;
text
 list_form | array_form
-----------+------------
        19 |         19
(1 row)

示例三:验证块的生命周期与默认值求值时机 ​

需求:证明每次进入块都会重新求值默认值——这是块结构中最容易被误解的一点。

sql
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;
text
                stamps
----------------------------------------
 10:22:31.104 10:22:31.126 10:22:31.148
(1 row)

三次时间戳各不相同,说明内层块的 DECLARE 在每轮进入时都执行了。与之对比,外层块的 v_lines 只在函数开始执行时求值一次。

示例四:函数属性的实际影响 ​

需求:对比 IMMUTABLE STRICT 与默认 VOLATILE 在 EXPLAIN 下的查询计划差异。

sql
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;
sql
-- 推荐: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  ');
text
                  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 重建,权限与注释会一并丢失,需要手工补回。

sql
-- 先记录原有所有者、权限与注释,便于重建后还原
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;
text
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 ";"42601BEGIN; 或 END IF 缺分号
cannot change return type of existing function42P13用 OR REPLACE 改返回类型
cannot change name of input parameter42P13用 OR REPLACE 改输入参数名
function "f" does not exist42883参数类型序列不匹配
function name "f" is not unique42725重载导致歧义,需写成 f(...) 或强制转换实参
VARIADIC parameter must be the last input parameter42P13VARIADIC 放在了中间
column "x" does not exist42703块的作用域不对,变量在内层块外不可见

最佳实践 ​

  • 函数体一律用美元引用,体内含 $$ 时外层换命名标签 $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 的陷阱。

参考链接 ​