CREATE FUNCTION 基础
CREATE FUNCTION 是把一段 SQL 或过程化代码登记进系统目录、让它可以像内置函数一样被调用的语句。本节从语句本身出发:讲清完整语法骨架、LANGUAGE 子句的四种取值、美元引用为什么优于单引号、RETURN 与 RETURN QUERY 的分工,并把 CREATE OR REPLACE 那条最容易被忽视的限制——改返回类型或参数名必须 DROP 重建——单独强调。学完之后,你能独立写出可上线、可维护、权限受控的自定义函数。
与「内置函数」的分工
前面几个阶段的章节都在讲 PostgreSQL 自带的函数(字符串、日期、数组、JSON、序列、UUID);从本节起进入自定义函数(User-Defined Function)。内置函数由 C 语言实现在数据库二进制里,自定义函数则是你写进系统目录 pg_proc 的一段可执行代码。两者的调用方式完全一致,但生命周期、性能特征、权限模型完全不同——这正是本节要建立的心智模型。
前置知识
阅读本节前,建议先了解:UUID 生成函数
基础概念
函数(Function)是 PostgreSQL 中被调用的可执行单元。调用点可以出现在 SELECT 列表、WHERE、ORDER BY、CHECK 约束、列默认值、生成列、触发器以及索引表达式中。它在系统目录里由名称 + 输入参数类型序列共同标识,这称为签名(Signature)。
| 概念 | 术语 | 说明 |
|---|---|---|
| 函数 | Function | 登记在 pg_proc 中、可被 SQL 语句调用的可执行单元 |
| 函数头 | Function Header | 名称、参数、RETURNS、LANGUAGE、易变性、安全属性等声明部分 |
| 函数体 | Function Body | AS 后面那段字符串,含义由语言处理器决定 |
| 签名 | Signature | 名称 + 输入参数类型序列,是函数在 pg_proc 中的唯一键 |
| 重载 | Overloading | 同名但参数类型序列不同的多个函数共存 |
| 语言处理器 | Language Handler | 解释函数体的模块:sql / plpgsql / c / internal |
| 美元引用 | Dollar Quoting | 用 $tag$ ... $tag$ 包裹字面量,内部单引号无需转义 |
| 内联 | Inlining | 优化器把 sql 函数的函数体展开进调用它的查询 |
| 易变性 | Volatility | IMMUTABLE / STABLE / VOLATILE,决定优化器能做的假设 |
| 安全上下文 | Security Context | SECURITY INVOKER(默认)与 SECURITY DEFINER |
函数由两部分组成,理解这个划分是理解后续所有细节的前提:
| 组成部分 | 内容 | 例子 |
|---|---|---|
| 头(Header) | 名称、参数、返回类型、语言、易变性、安全属性 | CREATE FUNCTION f(i int) RETURNS int LANGUAGE sql |
| 体(Body) | 真正执行的代码,写在美元引用里 | AS $$ SELECT i + 1 $$ |
LANGUAGE 声明函数体用什么语言处理器(Language Handler)解释。PostgreSQL 内置了四种,用户最常用前两种:
| 语言 | 函数体是什么 | 能否内联 | 有无控制流 | 适用场景 |
|---|---|---|---|---|
sql | 一条或多条 SQL 语句(末条决定返回值) | 能 | 无 | 简单映射、查询封装、性能敏感路径 |
plpgsql | 语句序列(BEGIN ... END) | 不能 | 有 | 业务逻辑、多步写入、异常处理 |
c | .so 动态库 + 链接符号 | 不能 | 有 | 极致性能、系统集成(需编译) |
internal | 只能用内核已有的函数名 | — | — | 数据库内部使用,普通用户无法新建 |
先记住这条:语言选择决定了函数能不能被优化器「看穿」,而这一点的性能影响常常是数量级。sql 与 plpgsql 的完整对比见 SQL 函数 vs PL/pgSQL 函数。
语法与用法
CREATE [ OR REPLACE ] FUNCTION 名称 ( [ 参数模式 ] [ 参数名 ] 参数类型 [ DEFAULT 默认值 ] [, ...] )
[ RETURNS 返回类型 | RETURNS TABLE ( 列名 列类型 [, ...] ) ]
LANGUAGE 语言名
[ IMMUTABLE | STABLE | VOLATILE ] -- 易变性,默认 VOLATILE
[ STRICT | CALLED ON NULL INPUT ] -- NULL 入参策略,默认 CALLED ON NULL INPUT
[ SECURITY INVOKER | SECURITY DEFINER ] -- 安全上下文,默认 INVOKER
[ PARALLEL { UNSAFE | RESTRICTED | SAFE } ] -- 并行安全性,默认 UNSAFE
[ COST 执行代价 ] [ ROWS 预估行数 ] -- 规划器提示
[ SET 配置参数 { TO | = } 值 ] [ ... ]
[ AS 函数体 ]
-- PostgreSQL 14+ 的 SQL 标准函数体写法(语言默认 sql,且必须放最后)
CREATE FUNCTION 名称 ( 参数... ) RETURNS 类型 LANGUAGE sql RETURN 表达式;最小可运行示例——同一个「加一」逻辑的两种语言实现:
-- 1) LANGUAGE sql:函数体是一条 SELECT 表达式
CREATE FUNCTION demo_add_one(i integer)
RETURNS integer
LANGUAGE sql
IMMUTABLE
AS $$
SELECT i + 1;
$$;-- 2) LANGUAGE plpgsql:函数体是语句序列,必须显式 RETURN
CREATE FUNCTION demo_grade_of(score integer)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
IF score >= 90 THEN
RETURN 'A';
ELSIF score >= 60 THEN
RETURN 'PASS';
ELSE
RETURN 'FAIL';
END IF;
END;
$$;SELECT demo_add_one(41) AS plus_one,
demo_grade_of(95) AS grade_a,
demo_grade_of(30) AS grade_fail; plus_one | grade_a | grade_fail
----------+---------+------------
42 | A | FAIL
(1 row)详细说明
函数头与函数体:为什么 AS 后面只是一段字符串
CREATE FUNCTION 在创建时只做两件事:解析函数头(名称、参数、返回类型、属性),把 AS 后面的内容原样存成 pg_proc.prosrc。函数体的语法通常不在创建时校验——plpgsql 的函数体到第一次执行才编译。
-- 这段代码创建时不会报错,语法错误藏在字符串里
CREATE FUNCTION demo_broken() RETURNS integer
LANGUAGE plpgsql
AS $$
BEGIN
RETURN 1 +; -- 故意的语法错误
END;
$$;CREATE FUNCTIONSELECT demo_broken();ERROR: syntax error at or near ";"
LINE 3: RETURN 1 +;
^结论:写完函数立即调用一次,不要等应用上线才发现拼写错误。这也是「创建脚本末尾附带一个 SELECT f() 自检」这条工程习惯的由来。
LANGUAGE 子句:sql / plpgsql / c / internal
-- 查看数据库里已安装的语言处理器(plpgsql 默认安装,其他语言需装扩展)
SELECT lanname, lanpltrusted AS trusted, lanplcallfoid::regproc AS handler
FROM pg_language
ORDER BY lanname; lanname | trusted | handler
-----------+---------+----------------
c | f | -
internal | f | -
plpgsql | t | plpgsql_call_handler
sql | t | -
(4 rows)几个容易忽略的事实:
sql与c/internal不是「过程化语言(PL)」,它们内置在内核,不出现在\dx扩展列表中。plpgsql是默认安装的扩展,但它仍然是扩展,用\dx能看到:
-- psql 元命令:查看已安装扩展
\dx plpgsql List of installed extensions
Name | Version | Schema | Description
----------+---------+--------+------------------------------------
plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language
(1 row)PL/pgSQL 本身的 MAJOR / MINOR 版本号与 PostgreSQL 主版本绑定,plpgsql.extra_warnings、plpgsql.check_asserts 等 GUC 参数可以直接在会话里设置:
-- 打开「可疑赋值」与「常量条件」告警,帮助发现 plpgsql 里的低级错误
SET plpgsql.extra_warnings = 'all';c与internal不能用于生产环境的普通业务逻辑。c需要编译动态库并具备服务器文件系统访问能力;internal只允许注册内核已有的符号,且仅超级用户能创建。- 不写
LANGUAGE时默认是sql,但那是 PostgreSQL 14+ 的 SQL 标准函数体写法(RETURN 表达式)才有的默认;用AS $$ ... $$形式的传统函数必须显式声明。
-- PostgreSQL 14+:SQL 标准函数体,语言默认 sql,写法更紧凑
CREATE FUNCTION demo_add_one_v2(i integer) RETURNS integer
RETURN i + 1;-- PostgreSQL 14+:过程也可以用 BEGIN ATOMIC(函数不支持)
CREATE PROCEDURE demo_cleanup()
LANGUAGE sql
BEGIN ATOMIC
DELETE FROM demo.orders WHERE status = 'cancelled';
END;美元引用(Dollar Quoting):为什么几乎是强制用法
函数体是字符串常量。如果用普通单引号包裹,函数体内每一个单引号都要写成两个,反斜杠也要转义:
-- 不推荐:单引号包裹,内部引号翻倍,几乎无法维护
CREATE FUNCTION demo_bad() RETURNS text LANGUAGE sql
AS 'SELECT ''hello''::text || '' world''::text';-- 推荐:美元引用,内部引号原样写
CREATE FUNCTION demo_good() RETURNS text LANGUAGE sql
AS $$ SELECT 'hello'::text || ' world'::text AS greeting $$;
SELECT demo_good(); demo_good
----------------
hello world
(1 row)嵌套时给外层换一个标签。当函数体内部本身包含 $$(比如要在函数里再 EXECUTE 一段 CREATE FUNCTION),内层就用不同的标签:
CREATE FUNCTION demo_outer() RETURNS text
LANGUAGE plpgsql
AS $outer$
DECLARE
v_ddl text;
BEGIN
v_ddl := $inner$
SELECT '生成于内层'::text;
$inner$;
RETURN v_ddl;
END;
$outer$;标签的语法要求只有三条:以 $ 开头、以 $ 结尾、中间是合法标识符字符。常用 $body$、$fn$、$q$。
内外标签必须不同
如果内外都用 $$,解析器会在内层的第一个 $$ 处提前闭合字符串,随后报出一串看似无关的语法错误。只要函数体里出现 $$,外层就必须换标签。
AS 后面也可以写两个字符串(AS 'obj_file', 'link_symbol'),那是 C 语言函数的形态,普通业务函数用不到。
函数体基本结构:sql 与 plpgsql 的分野
| 维度 | LANGUAGE sql | LANGUAGE plpgsql |
|---|---|---|
| 体结构 | 一条或多条 SQL 语句,分号分隔 | DECLARE ... BEGIN ... END; 块 |
| 返回值来源 | 最后一条 SELECT / INSERT ... RETURNING / UPDATE ... RETURNING | 显式 RETURN / RETURN QUERY / RETURN NEXT |
| 变量 | 没有(只能用 $1、$2 或参数名) | 有 DECLARE 段 |
| 控制流 | 没有 | IF / CASE / LOOP / FOR / WHILE |
| 异常处理 | 没有 | BEGIN ... EXCEPTION ... END |
| 动态 SQL | 没有 | EXECUTE / RETURN QUERY EXECUTE |
| 能否内联 | 满足条件即可 | 永远不能 |
LANGUAGE sql 函数体里不能写 BEGIN ... END——那是 plpgsql(及 PostgreSQL 14+ 的 BEGIN ATOMIC)的语法:
-- 错误!sql 语言里没有 BEGIN ... END 这个概念
CREATE FUNCTION demo_wrong() RETURNS integer
LANGUAGE sql
AS $$
BEGIN
SELECT 1;
END;
$$;ERROR: syntax error at or near "BEGIN"
LINE 3: BEGIN
^sql 语言也不能有多条「各自独立」的语句影响返回值,但体里可以有界于 WITH 的条件逻辑;需要真正流程控制时请改用 plpgsql:
-- sql 语言里的条件逻辑只能靠 CASE / COALESCE / NULLIF
CREATE FUNCTION demo_grade_sql(score integer) RETURNS text
LANGUAGE sql
IMMUTABLE
AS $$
SELECT CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 60 THEN 'PASS'
ELSE 'FAIL'
END;
$$;RETURNS 与 RETURN:声明与返回是两件事
RETURNS(带 S)属于函数头,声明返回类型;RETURN(不带 S)属于函数体,产生返回值。这是初学者最常混淆的一对关键字。
| 写法 | 所属位置 | 作用 |
|---|---|---|
RETURNS integer | 函数头 | 声明:本函数返回一个整数 |
RETURN 42; | plpgsql 函数体 | 执行:返回 42 并结束函数 |
RETURN; | plpgsql void 函数体 | 提前结束,无返回值 |
RETURN QUERY SELECT ... | plpgsql 函数体 | 把查询结果整体追加进结果集 |
RETURNS 的常见形态:
| 写法 | 含义 | 调用方式 |
|---|---|---|
RETURNS void | 不返回值,只为副作用 | SELECT f(); |
RETURNS integer | 单个标量 | SELECT f(); |
RETURNS integer[] | 数组 | SELECT f(); / unnest(f()) |
RETURNS SETOF integer | 多行单列 | SELECT * FROM f(); |
RETURNS SETOF users | 多行,行结构取自表 users | SELECT * FROM f(); |
RETURNS TABLE(a int, b text) | 多行多列 | SELECT * FROM f(); |
RETURNS record | 结构未定型,需 OUT 或调用处给列定义 | SELECT * FROM f() AS t(...) |
RETURNS refcursor | 返回游标名 | 事务内配合 FETCH |
完整的返回类型清单、OUT 参数与 TABLE 的等价关系、RETURN NEXT / RETURN QUERY EXECUTE 的用法,全部放在 函数参数与返回类型 一节展开。
RETURN vs RETURN QUERY
RETURN 返回一个值(或一整行);RETURN QUERY 把一条查询的全部结果追加到函数的结果集,用于 RETURNS SETOF / RETURNS TABLE / RETURNS void。
-- RETURN:一次调用只产出一个值,函数随即结束
CREATE FUNCTION demo_first_even(n integer) RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
RETURN n + (n % 2); -- 找到后立即结束
END;
$$;-- RETURN QUERY:把整条查询的结果集作为函数结果
CREATE FUNCTION demo_evens(n integer) RETURNS SETOF integer
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
RETURN QUERY
SELECT g FROM generate_series(2, n, 2) AS g;
END;
$$;
SELECT array_agg(x) AS evens FROM demo_evens(10) AS x; evens
------------------
{2,4,6,8,10}
(1 row)RETURN QUERY 与 RETURN NEXT 的区别是「一次追加一整集」还是「一行一行追加」:
| 写法 | 追加单位 | 典型场景 |
|---|---|---|
RETURN QUERY SELECT ... | 一条查询的全部行 | 结果就是一条查询 |
RETURN NEXT 表达式 | 一行(循环内反复调用) | 需要边算边吐、逐行加工 |
RETURN QUERY EXECUTE '动态 SQL' | 动态查询的全部行 | 表名/列名由参数决定 |
-- RETURN NEXT:循环里逐行追加
CREATE FUNCTION demo_fib(n integer) RETURNS SETOF bigint
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
a bigint := 0;
b bigint := 1;
t bigint;
i integer;
BEGIN
FOR i IN 1..n LOOP
RETURN NEXT a; -- 每次追加一行
t := a + b;
a := b;
b := t;
END LOOP;
END;
$$;
SELECT array_agg(x) AS fib10 FROM demo_fib(10) AS x; fib10
---------------------------
{0,1,1,2,3,5,8,13,21,34}
(1 row)LANGUAGE sql 里没有 RETURN
sql 函数的返回值来自最后一条语句:SELECT 或带 RETURNING 的 INSERT / UPDATE / DELETE / MERGE。写 RETURN 直接语法错误。需要 RETURN / RETURN QUERY 就必须用 plpgsql。
CREATE OR REPLACE 的三条限制(本节最重要的实践要点)
OR REPLACE 看起来很顺手,但它只允许改「函数体 + 辅助属性」,不允许改「标识函数身份的三要素」:
| 想改的东西 | CREATE OR REPLACE 允许吗 | 后果 |
|---|---|---|
函数体(AS $$ ... $$) | ✅ 允许 | 正常替换 |
易变性(IMMUTABLE ↔ VOLATILE) | ✅ 允许 | 正常替换 |
SECURITY INVOKER ↔ DEFINER | ✅ 允许 | 正常替换 |
SET search_path | ✅ 允许 | 正常替换 |
COST / ROWS / PARALLEL | ✅ 允许 | 正常替换 |
返回类型(RETURNS) | ❌ 不允许 | 报错 cannot change return type of existing function |
参数名(argname) | ❌ 不允许 | 报错 cannot change name of input parameter |
| 参数类型序列 | ❌ 不算替换 | 等于新建另一个函数,旧的仍在 |
-- 创建 v1
CREATE FUNCTION demo_release(p_items integer) RETURNS integer
LANGUAGE sql IMMUTABLE
AS $$ SELECT p_items * 2 $$;-- 错误!想改返回类型为 text,OR REPLACE 直接拒绝
CREATE OR REPLACE FUNCTION demo_release(p_items integer) RETURNS text
LANGUAGE sql IMMUTABLE
AS $$ SELECT 'ok'::text $$;ERROR: cannot change return type of existing function
HINT: Use DROP FUNCTION demo_release(integer) first.-- 错误!想改参数名也会被拒绝
CREATE OR REPLACE FUNCTION demo_release(p_count integer) RETURNS integer
LANGUAGE sql IMMUTABLE
AS $$ SELECT p_count * 2 $$;ERROR: cannot change name of input parameter "p_items"
HINT: Use DROP FUNCTION first.改返回类型 / 参数名必须 DROP 重建
CREATE OR REPLACE 不能改返回类型,也不能改参数名。这不是限制的疏漏,而是刻意的:数据库里可能有视图、生成列、表达式索引、触发器、CHECK 约束依赖这个函数,改掉返回结构会让它们全部失效。
因此,当你要改的是返回类型或参数名时,正确的流程是:
DROP FUNCTION IF EXISTS demo_release(integer);
CREATE FUNCTION demo_release(p_count integer) RETURNS text
LANGUAGE sql IMMUTABLE
AS $$ SELECT p_count::text $$;DROP 遇到依赖对象会立刻报错,从而强制你逐个检查依赖。反之,用 OR REPLACE 悄悄改掉行为的函数,可能让表达式索引里固化着旧算法的值,造成查询结果永久错乱。
同一个「改参数类型」的动作,OR REPLACE 不会报错,但结果更糟——它悄悄留下两个函数:
CREATE OR REPLACE FUNCTION demo_release(p_items bigint) RETURNS integer
LANGUAGE sql IMMUTABLE
AS $$ SELECT p_items::integer * 3 $$;
-- 现在库里有两个 demo_release,重载解析会在调用时决定用哪个
\df demo_release List of functions
Schema | Name | Result data type | Argument data types
--------+---------------+------------------+---------------------
public | demo_release | integer | p_items bigint
public | demo_release | integer | p_items integer
(2 rows)参数类型属于签名,所以改它 = 新建函数。重载带来的连锁问题见 函数重载与可变参数。
查看函数:\df / \sf / pg_proc
-- psql 元命令
\df demo_* -- 列出匹配名字的函数及其参数、返回类型
\df+ demo_release -- 追加易变性、安全属性、语言、所有者、注释
\sf demo_release -- 打印函数源码(source)-- 目录视图:函数在 pg_proc 中的关键字段
SELECT p.oid,
p.proname AS name,
pg_get_function_arguments(p.oid) AS arguments,
pg_get_function_result(p.oid) AS result,
l.lanname AS language,
p.provolatile AS volatility, -- i / s / v
p.prosecdef AS security_definer,
p.proisstrict AS strict,
p.proowner::regrole AS owner
FROM pg_proc AS p
JOIN pg_language AS l ON l.oid = p.prolang
WHERE p.proname = 'demo_release'; oid | name | arguments | result | language | volatility | security_definer | strict | owner
------+--------------+--------------------------+---------+----------+------------+------------------+--------+-------
16523| demo_release | p_items integer | integer | sql | i | f | f | ch
16524| demo_release | p_items bigint | integer | sql | i | f | f | ch
(2 rows)provolatile 的取值:i = IMMUTABLE、s = STABLE、v = VOLATILE。不写易变性就是 v,这是性能问题的常见源头,详见 IMMUTABLE / STABLE / VOLATILE。
导出可重建的 DDL,做版本管理时用这个而不是手抄:
SELECT pg_get_functiondef('demo_release(integer)'::regprocedure) AS definition;CREATE OR REPLACE FUNCTION public.demo_release(p_items integer)
RETURNS integer
LANGUAGE sql
IMMUTABLE
AS $function$
SELECT p_items * 2
$function$注意它不包含 COMMENT ON、ALTER ... OWNER TO、GRANT / REVOKE,这些要单独导出。
删除函数:DROP FUNCTION 必须带参数类型
函数靠签名标识,删除时也必须给签名:
-- 错误!同名函数有多个时无法确定删哪个
DROP FUNCTION demo_release;ERROR: function name "demo_release" is not unique
HINT: Specify the argument list to select the function unambiguously.-- 正确:带参数类型(重载场景下这是唯一可行写法)
DROP FUNCTION demo_release(integer);
-- 用 regprocedure 转换可避免类型别名带来的歧义
DROP FUNCTION 'demo_release(bigint)'::regprocedure;DROP FUNCTION IF EXISTS f(参数类型); 是可重复执行脚本的标准开头;若确认要连带删除依赖对象,才使用 CASCADE——但生产环境请先看清楚它要级联删掉什么。
函数(Function)与存储过程(Procedure)的区别
PostgreSQL 11 才引入存储过程,两者在调用方式和事务能力上差异明显:
| 维度 | 函数(CREATE FUNCTION) | 存储过程(CREATE PROCEDURE) |
|---|---|---|
| 调用方式 | SELECT f(...) / 出现在表达式中 | CALL p(...),不能出现在表达式中 |
| 返回值 | 必须有 RETURNS(可为 void),可返回集合 | 没有返回值,只能靠 INOUT 参数带回 |
| 事务控制 | 不能 COMMIT / ROLLBACK(在调用者事务内) | 可以 COMMIT / ROLLBACK / SET TRANSACTION |
能否用于 FROM / 索引 / 生成列 | 能 | 不能 |
| 能否内联 | LANGUAGE sql 可被内联 | 不适用 |
| 适合场景 | 查询封装、表达式、表函数 | 批量作业、分批提交、DDL 编排 |
-- 过程:分批删除,每批提交,避免一个巨事务锁住整张表
CREATE PROCEDURE demo_purge_orders(p_batch integer DEFAULT 10000)
LANGUAGE plpgsql
AS $$
DECLARE
v_deleted integer;
BEGIN
LOOP
DELETE FROM demo.orders
WHERE id IN (
SELECT id FROM demo.orders WHERE status = 'cancelled' LIMIT p_batch
);
GET DIAGNOSTICS v_deleted = ROW_COUNT;
EXIT WHEN v_deleted = 0;
COMMIT; -- 只有过程能这样写
END LOOP;
END;
$$;
CALL demo_purge_orders(100);判断标准:需要在查询里用(SELECT 列表、WHERE、表函数)就用函数;需要跨事务边界做批量操作才用过程。业务代码里 90% 的场景是函数。
执行权限:GRANT EXECUTE 与默认的 PUBLIC
新函数创建后,EXECUTE 权限默认授予 PUBLIC——任何能连上该库的角色都能调用它。对只读辅助函数无害,但对会写数据、或 SECURITY DEFINER 的函数是隐患。
-- 先收回默认权限,再按需授权
REVOKE ALL ON FUNCTION demo_add_one(integer) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION demo_add_one(integer) TO app_role;更彻底的是从源头改默认权限,让以后新建的函数不再自动开放:
-- 当前角色在 public schema 下新建的函数,默认不再对 PUBLIC 开放
ALTER DEFAULT PRIVILEGES IN SCHEMA public
REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;查看现有权限分配:
SELECT p.proname,
pg_get_function_identity_arguments(p.oid) AS args,
p.proacl
FROM pg_proc AS p
WHERE p.proname = 'demo_add_one'; proname | args | proacl
--------------+---------+-----------------------------------
demo_add_one | integer | {=X/ch,ch=X/ch,app_role=X/ch}
(1 row)proacl 里 =X/ch 表示「PUBLIC 有执行权限、授予者是 ch」;REVOKE ... FROM PUBLIC 之后这一段会消失。CREATE 函数还需要参数类型与返回类型的 USAGE 权限,这是容易忽略的隐藏前提。
实战示例
下面从零建一个演示 schema、四组函数,逐一验证行为、权限与内联效果,最后清理。
第一步:建 schema 与表,造数据
CREATE SCHEMA IF NOT EXISTS demo;
SET search_path = demo, public;
DROP TABLE IF EXISTS demo.orders;
CREATE TABLE demo.orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no varchar(32) NOT NULL UNIQUE,
customer text NOT NULL,
amount numeric(12, 2) NOT NULL,
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO demo.orders (order_no, customer, amount, status) VALUES
('SO2026001', 'alice', 120.00, 'paid'),
('SO2026002', 'bob', 80.50, 'pending'),
('SO2026003', 'carol', 300.00, 'paid'),
('SO2026004', 'dave', 10.00, 'cancelled');CREATE SCHEMA
SET
DROP TABLE
CREATE TABLE
INSERT 0 4第二步:写一个 LANGUAGE sql 的表达式封装(含税计算)
CREATE FUNCTION demo.with_tax(p_amount numeric, p_rate numeric DEFAULT 0.06)
RETURNS numeric
LANGUAGE sql
IMMUTABLE
AS $$
SELECT round(p_amount * (1 + p_rate), 2);
$$;
SELECT demo.with_tax(100.00) AS default_rate,
demo.with_tax(100.00, 0.13) AS custom_rate,
demo.with_tax(p_rate => 0.13, p_amount => 100.00) AS named_args; default_rate | custom_rate | named_args
--------------+-------------+------------
106.00 | 113.00 | 113.00
(1 row)第三步:写一个返回行集的 plpgsql 函数
CREATE FUNCTION demo.order_summary(p_status text)
RETURNS TABLE(order_count bigint, total_amount numeric)
LANGUAGE plpgsql
STABLE
AS $$
BEGIN
RETURN QUERY
SELECT count(*), COALESCE(sum(o.amount), 0)
FROM demo.orders AS o
WHERE o.status = p_status;
END;
$$;
SELECT * FROM demo.order_summary('paid'); order_count | total_amount
-------------+--------------
2 | 420.00
(1 row)第四步:验证 sql 函数被内联(计划里看不到函数调用)
EXPLAIN (COSTS OFF)
SELECT * FROM demo.orders WHERE amount > demo.with_tax(50.00); QUERY PLAN
---------------------------------------
Seq Scan on orders
Filter: (amount > 53.00)
(2 rows)with_tax(50.00) 是 IMMUTABLE 的 sql 函数且参数是常量,优化器在计划期直接常量折叠成 53.00,函数调用整个消失。把同一个函数改成 plpgsql 就会退化成不可折叠的黑盒调用——原因见 SQL 函数 vs PL/pgSQL 函数。
第五步:查看函数元数据与源码
SELECT p.proname,
pg_get_function_result(p.oid) AS result,
l.lanname,
p.provolatile AS vol,
pg_get_function_identity_arguments(p.oid) AS args
FROM pg_proc AS p
JOIN pg_language AS l ON l.oid = p.prolang
WHERE p.pronamespace = 'demo'::regnamespace
ORDER BY p.proname; proname | result | language | vol | args
---------------+------------------------------------+----------+-----+-----------------
order_summary | TABLE(order_count bigint, total_amount numeric) | plpgsql | s | p_status text
with_tax | numeric | sql | i | p_amount numeric, p_rate numeric
(2 rows)第六步:收紧权限
-- 撤销默认的 PUBLIC 执行权限,只授权给应用角色
REVOKE ALL ON FUNCTION demo.with_tax(numeric, numeric) FROM PUBLIC;
SELECT proname, proacl
FROM pg_proc
WHERE proname = 'with_tax' AND pronamespace = 'demo'::regnamespace; proname | proacl
-----------+----------------
with_tax | {ch=X/ch}
(1 row)第七步:改函数的正确姿势(DROP + CREATE)
-- 想把返回类型从 numeric 改成 text:OR REPLACE 会报错,必须重建
DROP FUNCTION IF EXISTS demo.with_tax(numeric, numeric);
CREATE FUNCTION demo.with_tax_label(p_amount numeric, p_rate numeric DEFAULT 0.06)
RETURNS text
LANGUAGE sql
IMMUTABLE
AS $$
SELECT to_char(round(p_amount * (1 + p_rate), 2), 'FM999999990.00') || ' CNY';
$$;
SELECT demo.with_tax_label(100.00) AS label; label
--------------
106.00 CNY
(1 row)第八步:清理
DROP FUNCTION demo.with_tax_label(numeric, numeric);
DROP FUNCTION demo.order_summary(text);
DROP FUNCTION demo_add_one(integer);
DROP FUNCTION demo_grade_of(integer);
DROP FUNCTION demo_first_even(integer);
DROP FUNCTION demo_evens(integer);
DROP FUNCTION demo_fib(integer);
DROP FUNCTION demo_grade_sql(integer);
DROP FUNCTION demo_bad();
DROP FUNCTION demo_good();
DROP FUNCTION demo_outer();
DROP PROCEDURE demo_purge_orders(integer);
DROP SCHEMA demo CASCADE;DROP FUNCTION
...
DROP SCHEMA注意事项
OR REPLACE不能改返回类型、不能改参数名。改这两样只能DROP重建,报错分别是cannot change return type of existing function与cannot change name of input parameter。OR REPLACE改参数类型 ≠ 替换。参数类型属于签名,改了等于新建函数,旧的仍留在库里,形成难以察觉的重载。sql-- 错误!以为在「升级」函数,其实造出了第二个签名 CREATE OR REPLACE FUNCTION demo_release(p_items bigint) RETURNS integer LANGUAGE sql AS $$ SELECT p_items::integer $$;DROP FUNCTION不带签名在多签名时会报is not unique,务必写全参数类型,或用'f(integer)'::regprocedure。不写易变性就是
VOLATILE。这是「忘了标」导致性能劣化的根源:优化器无法常量折叠、无法下推索引条件。AS $$ ... $$只是一段字符串,plpgsql 的函数体到第一次执行才编译。创建成功不代表能用,务必立即调用一次。LANGUAGE sql的函数体里不能写BEGIN ... END、不能写RETURN。前者是 plpgsql 语法,后者的替代品是「最后一条语句即返回值」。新函数的
EXECUTE默认开放给PUBLIC。会写数据的函数、SECURITY DEFINER函数必须先REVOKE再按需GRANT。SECURITY DEFINER默认不固定search_path,是明确的提权风险。未加SET search_path前不要在生产使用,细节见 SECURITY DEFINER / INVOKER。参数名与列名同名会导致歧义。
WHERE id = id在 plpgsql 里会报column reference is ambiguous,解法见 函数参数与返回类型。函数名大小写:不加引号时
Add_One与add_one是同一个函数;加双引号能区分大小写,但会带来「到处都要加引号」的维护负担,不推荐。sql函数体里不能执行事务控制命令。COMMIT/ROLLBACK/SAVEPOINT/VACUUM都会报错,这类操作必须用过程(CALL)。不要把简单表达式封装成函数。
f(x) = x * 1.06直接写在查询里,省掉一次函数解析与调用开销。CREATE FUNCTION需要语言、参数类型、返回类型的USAGE权限,只给CREATE权限是不够的。改名只能用
ALTER FUNCTION ... RENAME TO;ALTER FUNCTION还能改易变性、安全属性、SET子句、所有者和注释,但同样不能改参数类型与返回类型。
最佳实践
默认选
LANGUAGE sql。能写成单条SELECT就不要用 plpgsql,内联带来的性能差距常是数量级。sqlCREATE FUNCTION app_calc_tax(p_amount numeric) RETURNS numeric LANGUAGE sql IMMUTABLE RETURN round(p_amount * 1.06, 2); -- PostgreSQL 14+ 的紧凑写法显式声明易变性。纯计算的标
IMMUTABLE;读表但语句内稳定的标STABLE;会写库或含random()/nextval()的标VOLATILE。永远不要依赖默认值。函数体一律用美元引用,并在体内含
$$时给外层换标签:sqlAS $body$ ... $body$;函数名加业务前缀,避开内置函数与扩展函数:
app_、demo_、shop_。参数统一加
p_前缀,从根上避免与列名撞车。把函数定义纳入版本管理。用
pg_get_functiondef()导出真身,脚本以DROP FUNCTION IF EXISTS ...;开头保证可重复执行。新函数创建后立即调用一次自检,尤其 plpgsql 函数:
sqlSELECT app_calc_tax(100.00); -- 语法错误在这一刻暴露改函数遵守「先
DROP再CREATE」,让依赖对象报错暴露;只有在「函数体微调、签名与返回类型完全不变」时才用OR REPLACE。权限从源头收紧,一次性解决所有新建函数的默认开放问题:
sqlALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;生产环境一律写
COMMENT ON FUNCTION,说明参数含义、副作用与幂等性:sqlCOMMENT ON FUNCTION app_calc_tax(numeric) IS '按 6% 税率计算含税金额;纯函数,可安全用于生成列';只在需要跨事务边界时才用过程,其余场景用函数,保持可组合性。
需要「查询 + 副作用」时优先
INSERT ... RETURNING形式的sql函数,一次往返完成写入与回传。
下一节
继续学习:函数参数与返回类型