Skip to content

函数重载与可变参数 ​

PostgreSQL 允许同名函数共存,只要参数类型不同——这叫函数重载(Function Overloading)。它带来灵活性,也带来「调用时到底命中哪个」的解析难题。本节讲清重载解析规则、VARIADIC 变长参数、以及 CREATE OR REPLACE 在重载场景下的陷阱。

与「函数参数与返回类型」的分工

函数参数与返回类型 讲过单个函数的参数模式与返回形态;本节讲多个同名函数之间如何共存与解析,以及 VARIADIC 这一个特殊参数模式的细节。

前置知识

阅读本节前,建议先了解:函数参数与返回类型、SQL 函数 vs PL/pgSQL 函数

基础概念 ​

函数签名(Signature)是「函数名 + IN/INOUT 参数类型列表」。PostgreSQL 的立场是:函数名不必唯一,签名必须唯一。

概念英文说明
函数重载Function Overloading同名、参数类型不同的一组函数
签名Signature函数名 + 参数类型列表(OUT 不计入)
重载解析Overload Resolution调用时决定命中哪个重载的过程
精确匹配Exact Match参数类型完全相同,优先级最高
隐式转换Implicit Cast类型可自动转换(如 int → bigint)
歧义Ambiguous多个候选同样匹配,无法决断,报错
变长参数VARIADIC最后一位接收不定个数的实参,聚成数组
首选类型Preferred Type同类别内有多候选时的默认倾向(如 float8)

参数名与默认值不是签名的一部分:

变化是否改变签名后果
改参数名否与原函数冲突(already exists)
改参数默认值否与原函数冲突
改参数类型是变成另一个函数,旧的仍存在
改返回类型否(但影响替换)OR REPLACE 会报 cannot change return type
改易变性否可以 OR REPLACE 直接改

语法与用法 ​

sql
-- 三个同名重载,靠参数类型区分
CREATE FUNCTION describe(v integer) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT 'integer:' || v $$;

CREATE FUNCTION describe(v text) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT 'text:' || v $$;

CREATE FUNCTION describe(v numeric) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT 'numeric:' || v $$;
sql
SELECT describe(42)         AS int_call,
       describe('hello')    AS text_call,
       describe(3.14)       AS numeric_call;
text
   int_call   | text_call | numeric_call
--------------+-----------+--------------
 integer:42   | text:hello| numeric:3.14
(3 rows)

VARIADIC 最小示例:

sql
CREATE FUNCTION join_all(VARIADIC parts text[]) RETURNS text
LANGUAGE sql IMMUTABLE
AS $$ SELECT array_to_string(parts, '-') $$;

-- 展开传参
SELECT join_all('a', 'b', 'c') AS expanded;
-- 传数组
SELECT join_all(VARIADIC ARRAY['a', 'b', 'c']) AS as_array;
text
 expanded
----------
 a-b-c
(1 row)
text
 as_array
----------
 a-b-c
(1 row)

详细说明 ​

重载解析的三层优先级 ​

调用 f(x) 时,PostgreSQL 按下面的顺序找候选:

  1. 精确匹配:x 的类型与参数类型完全一致 → 直接胜出。
  2. 隐式转换:没有精确匹配,但 x 的类型能隐式转换到某个候选的参数类型 → 在这些候选中做选择。
  3. 歧义报错:多个候选处于同一优先级且无法决断 → function is not unique。
sql
CREATE FUNCTION pick(v integer) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT 'integer' $$;

CREATE FUNCTION pick(v bigint) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT 'bigint' $$;

-- 精确匹配:integer 命中 pick(integer)
SELECT pick(1) AS exact_int;
text
 exact_int
-----------
 integer
(1 row)
sql
-- 精确匹配:bigint 命中 pick(bigint)
SELECT pick(1::bigint) AS exact_bigint;
text
 exact_bigint
-------------
 bigint
(1 row)

精确匹配优先于隐式转换,所以即便 integer 能隐式转 bigint,也仍走 pick(integer)。

歧义:同一优先级有多个候选 ​

当没有精确匹配、且有多个等价的隐式转换路径时,解析失败:

sql
CREATE FUNCTION amb(v numeric) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT 'numeric' $$;

CREATE FUNCTION amb(v real) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT 'real' $$;

-- 错误!integer 到 numeric 与 real 都能隐式转换,无法决断
SELECT amb(1);
text
ERROR:  function amb(integer) is not unique
LINE 1: SELECT amb(1);
               ^
HINT:  Could not choose a best candidate function. You might need to add explicit type casts.

解法是显式加类型转换:

sql
SELECT amb(1::numeric) AS as_numeric, amb(1::real) AS as_real;
text
 as_numeric | as_real
------------+---------
 numeric    | real
(2 rows)

缩小重载面是最有效的预防

与其记住解析规则,不如减少重载数量。同一个名字只重载 2~3 个「语义一致、类型明显不同」的版本(如 jsonb 版与 text 版),比堆十几个数值类型重载要好维护得多。

优选类型(Preferred Type) ​

当候选处于同一类型类别内时,PostgreSQL 用「优选类型」打破平局:

类型类别优选类型
数值float8(double precision)
字符串text
日期时间timestamptz
位串varbit
布尔boolean
sql
CREATE FUNCTION pref(v real) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT 'real' $$;

CREATE FUNCTION pref(v double precision) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT 'double precision' $$;

-- 无精确匹配时,数值类别优选 float8
SELECT pref(1::smallint) AS chosen;
text
      chosen
------------------
 double precision
(1 row)

参数名与默认值不参与签名 ​

sql
-- 第一次
CREATE FUNCTION named(a integer) RETURNS integer
LANGUAGE sql IMMUTABLE AS $$ SELECT a $$;
sql
-- 错误!参数名不同不影响签名,仍视为同一个函数
CREATE FUNCTION named(b integer) RETURNS integer
LANGUAGE sql IMMUTABLE AS $$ SELECT b $$;
text
ERROR:  function "named" already exists with same argument types

这意味着:给已有函数加参数名、改参数名,都必须用 CREATE OR REPLACE 或先 DROP。

CREATE OR REPLACE 在重载下的陷阱 ​

改参数类型 = 新建函数,旧的会留下:

sql
-- 原本存在
CREATE FUNCTION calc(a integer) RETURNS integer
LANGUAGE sql IMMUTABLE AS $$ SELECT a * 2 $$;

-- 想"升级",实际新建了另一个函数
CREATE OR REPLACE FUNCTION calc(a bigint) RETURNS bigint
LANGUAGE sql IMMUTABLE AS $$ SELECT a * 3 $$;
sql
\df calc
text
                        List of functions
 Schema | Name | Result data type | Argument data types |  Type
--------+------+------------------+---------------------+--------
 public | calc | integer          | a integer           | func
 public | calc | bigint           | a bigint            | func
(2 rows)

两个函数共存。更糟的是调用方 calc(5) 会精确命中 integer 版(字面量 5 是 integer),得到 10 而不是你以为的 15。

这是重载场景下最常见的生产事故

「我把函数逻辑改了,怎么没生效?」——十有八九是改了参数类型导致新建了重载,而老调用点仍然命中老版本。改函数前先 \df 函数名 看清现有签名,改完再 \df 核对,不要只看 CREATE FUNCTION 成功就以为替换完成。

排查重载 ​

sql
-- psql:按名字列出所有重载
\df calc
\df+ calc

-- 目录表:按参数类型查
SELECT oid,
       proname,
       proargtypes::regtype[] AS arg_types,
       prorettype::regtype    AS return_type
FROM pg_proc
WHERE proname = 'calc'
ORDER BY oid;
text
  oid  | proname | arg_types | return_type
-------+---------+-----------+-------------
 16901 | calc    | {integer} | integer
 16902 | calc    | {bigint}  | bigint
(2 rows)

proargtypes 是 oidvector 类型,::regtype[] 转换后可直接读出类型名。若已知参数类型想反查:

sql
SELECT 'calc(integer)'::regprocedure AS resolved,
       'calc(bigint)'::regprocedure  AS resolved_bigint;

regprocedure 转换会在类型无法唯一确定时直接报错,非常适合做「是否存在」的检查。

VARIADIC 的规则 ​

text
CREATE FUNCTION f(a integer, VARIADIC rest text[]) RETURNS ...

四条硬规则:

规则说明
必须放最后否则报 VARIADIC parameter must be the last input parameter
类型必须是数组否则报 VARIADIC parameter must be an array type
只能有一个否则报 cannot have multiple VARIADIC parameters
实参可直接展开f(1, 'a', 'b'),或整体传数组 f(1, VARIADIC ARRAY['a','b'])
sql
CREATE FUNCTION tally(p_label text, VARIADIC p_nums numeric[])
RETURNS text
LANGUAGE sql IMMUTABLE
AS $$
  SELECT p_label || ':' || COALESCE(sum(n), 0)
  FROM unnest(p_nums) AS n;
$$;

SELECT tally('sum', 1, 2, 3.5) AS expanded;
text
 expanded
----------
 sum:6.5
(1 row)

不写 VARIADIC 直接传数组会报错——这是最常见的误用:

sql
-- 错误!数组被当成一个 numeric 实参,与 numeric[] 形参不匹配
SELECT tally('sum', ARRAY[1, 2, 3]);
text
ERROR:  function tally(unknown, numeric[]) does not exist
LINE 1: SELECT tally('sum', ARRAY[1, 2, 3]);
               ^
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.

正确写法是加 VARIADIC 关键字:

sql
SELECT tally('sum', VARIADIC ARRAY[1, 2, 3]) AS as_array;
text
 as_array
----------
 sum:6
(1 row)

空参数边界 ​

VARIADIC 参数可以传零个实参,此时收到空数组:

sql
SELECT tally('empty') AS no_args;
text
 no_args
---------
 empty:0
(1 row)

注意与「传一个 NULL」的区别:

sql
-- 传一个 NULL 元素:数组是 {NULL},长度 1
SELECT tally('null') AS null_arg;
text
  null_arg
-----------
 null:0
(1 row)

sum(NULL) 是 NULL,经 COALESCE 变 0。但 array_length 会是 1 而不是 NULL:

sql
SELECT array_length(VARIADIC ARRAY[]::numeric[], 1) IS NULL AS empty_len_is_null;
text
 empty_len_is_null
-------------------
 t
(1 row)

空数组的 array_length 返回 NULL 而不是 0,这是数组的通用行为,遍历前要做 COALESCE。

遍历 VARIADIC 参数 ​

plpgsql 里两种遍历方式:

sql
-- 方式一:FOREACH 遍历元素
CREATE FUNCTION count_words_foreach(VARIADIC p_words text[])
RETURNS integer
LANGUAGE plpgsql IMMUTABLE
AS $$
DECLARE
  w     text;
  total integer := 0;
BEGIN
  FOREACH w IN ARRAY p_words LOOP
    total := total + length(w);
  END LOOP;
  RETURN total;
END;
$$;

-- 方式二:unnest + 聚合(更 SQL 化)
CREATE FUNCTION count_words_sql(VARIADIC p_words text[])
RETURNS integer
LANGUAGE sql IMMUTABLE
AS $$
  SELECT COALESCE(sum(length(w)), 0)::integer FROM unnest(p_words) AS w;
$$;

SELECT count_words_foreach('ab', 'cde') AS foreach_result,
       count_words_sql('ab', 'cde')     AS sql_result;
text
 foreach_result | sql_result
----------------+------------
              5 |          5
(1 row)

FOREACH 与 FOR ... IN 1..array_length(...) 的区别:FOREACH 会自动跳过 NULL 元素并正确处理多维数组,优先用 FOREACH。

array_position 适合「找第几个」:

sql
CREATE FUNCTION index_of(p_needle text, VARIADIC p_haystack text[])
RETURNS integer
LANGUAGE sql IMMUTABLE
AS $$ SELECT array_position(p_haystack, p_needle) $$;

SELECT index_of('b', 'a', 'b', 'c') AS pos;
text
 pos
-----
   2
(1 row)

内置的 VARIADIC 函数 ​

不少内置函数就是 VARIADIC,理解这一点能解释它们的边界行为:

函数签名形态说明
format(fmt text, VARIADIC args any[])变长格式化字符串
concat(VARIADIC any)变长(伪类型 any)拼接任意值,NULL 当空串
concat_ws(sep text, VARIADIC any)变长带分隔符拼接,会跳过 NULL
coalesce(VARIADIC any)变长取第一个非 NULL
greatest(VARIADIC any) / least(VARIADIC any)变长最大 / 最小
jsonb_build_array(VARIADIC any)变长构造 JSON 数组
jsonb_build_object(VARIADIC any)变长构造 JSON 对象(参数成对)
num_nonnulls(VARIADIC any)变长非 NULL 参数个数
sql
SELECT concat('a', NULL, 'b')              AS concat_keeps,
       concat_ws('-', 'a', NULL, 'b')      AS concat_ws_skips,
       coalesce(NULL, NULL, 'x')           AS coalesce_pick,
       greatest(1, 9, 5)                   AS greatest_pick,
       num_nonnulls(1, NULL, 3)            AS nonnull_count;
text
 concat_keeps | concat_ws_skips | coalesce_pick | greatest_pick | nonnull_count
--------------+-----------------+---------------+---------------+---------------
 ab           | a-b             | x             |             9 |             2
(1 row)

注意 concat 与 concat_ws 的差异:前者把 NULL 当空串从而保留拼接位置,后者跳过 NULL 从而不产生多余分隔符。这是 VARIADIC 语义下最容易搞混的一对。

jsonb_build_object 的 VARIADIC 要求参数成对:

sql
SELECT jsonb_build_object('a', 1, 'b', 2) AS obj;
text
            obj
----------------------------
 {"a": 1, "b": 2}
(1 row)
sql
-- 错误!参数个数为奇数,无法配成键值对
SELECT jsonb_build_object('a', 1, 'b');
text
ERROR:  argument list must have even number of elements
HINT:  The arguments of jsonb_build_object() must consist of alternating keys and values.

重载 vs 命名不同的函数 ​

工程上该选哪个?对照如下:

维度用重载(同名)用不同名字
调用方书写统一名字,简洁名字要区分,冗长
类型解析风险有(歧义、误命中)无
CREATE OR REPLACE 风险高(改类型即新建)低
文档与搜索需按签名检索按名字即可
语义一致性强制保持同名同义无约束
排查难度\df 要看多行一目了然
适用场景同一操作支持多种输入类型不同语义的操作

推荐:只在「同一语义、输入类型明显不同」时重载,且数量控制在少数几个。语义不同的一律用不同名字。

DROP FUNCTION 必须写全签名 ​

重载存在时,删函数必须给参数类型:

sql
-- 错误!calc 有两个重载
DROP FUNCTION calc;
text
ERROR:  function name "calc" is not unique
HINT:  Specify the argument list to select the function unambiguously.
sql
-- 正确
DROP FUNCTION calc(integer);
DROP FUNCTION calc(bigint);

批量删除同名重载,可用目录表生成脚本:

sql
SELECT format('DROP FUNCTION %s;', p.oid::regprocedure) AS drop_stmt
FROM pg_proc AS p
JOIN pg_namespace AS n ON n.oid = p.pronamespace
WHERE p.proname = 'calc' AND n.nspname = 'public';
text
              drop_stmt
--------------------------------------
 DROP FUNCTION public.calc(integer);
 DROP FUNCTION public.calc(bigint);
(2 rows)

重载与 search_path 的跨 schema 风险 ​

函数解析会遍历整个 search_path。不同 schema 下的同名函数会互相干扰:

sql
-- public 下有 app_safe(text)
-- 攻击者在自己 schema 建 app_safe(text)
-- 若该 schema 在 search_path 中更靠前,调用会命中攻击者的版本

这不只影响安全性(见 SECURITY DEFINER / INVOKER),也影响正确性:search_path 变化会导致同一段 SQL 调用到不同实现。

sql
-- 检查当前 search_path
SHOW search_path;
text
      search_path
-----------------------
 "$user", public
(1 row)

生产函数里建议用全限定名调用关键函数:

sql
SELECT public.calc(5);

实战示例 ​

完整的重载与 VARIADIC 演练:建表、建重载函数、验证解析、排查、清理。

建表与造数据 ​

sql
CREATE SCHEMA IF NOT EXISTS demo;
SET search_path = demo, public;

CREATE TABLE demo.orders (
  id          bigserial PRIMARY KEY,
  customer    text           NOT NULL,
  amount      numeric(12, 2) NOT NULL,
  status      text           NOT NULL DEFAULT 'pending',
  tags        text[]         NOT NULL DEFAULT '{}',
  created_at  timestamptz    NOT NULL DEFAULT now()
);

INSERT INTO demo.orders (customer, amount, status, tags) VALUES
  ('alice', 120.00, 'paid',      ARRAY['vip', 'wholesale']),
  ('alice',  30.00, 'pending',   ARRAY['vip']),
  ('bob',    80.50, 'pending',   ARRAY['retail']),
  ('carol', 300.00, 'paid',      ARRAY['vip', 'priority']),
  ('carol',  50.00, 'paid',      ARRAY[]::text[]),
  ('dave',   10.00, 'cancelled', ARRAY['retail']);

建一组重载 ​

sql
-- 按客户名汇总
CREATE FUNCTION demo.total(key text)
RETURNS numeric
LANGUAGE sql STABLE
AS $$ SELECT COALESCE(sum(amount), 0) FROM demo.orders WHERE customer = key $$;

-- 按订单 id 取金额
CREATE FUNCTION demo.total(key bigint)
RETURNS numeric
LANGUAGE sql STABLE
AS $$ SELECT COALESCE(sum(amount), 0) FROM demo.orders WHERE id = key $$;

-- 按状态汇总
CREATE FUNCTION demo.total(key demo_orders_status_dummy) RETURNS numeric
LANGUAGE sql STABLE
AS $$ SELECT 0::numeric $$;

前三行演示「同名不同签名」。实际使用中只保留前两个:

sql
SELECT demo.total('alice') AS by_name,
       demo.total(1::bigint) AS by_id;
text
 by_name | by_id
---------+--------
  150.00 | 120.00
(1 row)

验证精确匹配优先 ​

sql
-- 字面量 1 是 integer,没有 integer 版本,会尝试隐式转换
-- 此时 bigint 是唯一候选,成功
SELECT demo.total(1) AS literal_call;
text
 literal_call
--------------
       120.00
(1 row)

查看最终命中了哪个签名:

sql
SELECT 'demo.total(integer)'::regprocedure AS resolved_signature;
text
     resolved_signature
-----------------------------
 demo.total(bigint)
(1 row)

regprocedure 的转换过程就等同于重载解析,因此它能告诉你会命中哪个。

演示歧义 ​

sql
CREATE FUNCTION demo.category(v numeric) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT 'numeric' $$;

CREATE FUNCTION demo.category(v real) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT 'real' $$;

-- 错误!integer → numeric 与 integer → real 都能隐式转换
SELECT demo.category(1);
text
ERROR:  function demo.category(integer) is not unique
HINT:  Could not choose a best candidate function. You might need to add explicit type casts.

显式转换后正常:

sql
SELECT demo.category(1::numeric) AS as_numeric,
       demo.category(1::real)    AS as_real;
text
 as_numeric | as_real
------------+---------
 numeric    | real
(2 rows)

演示「改类型 = 新建函数」 ​

sql
CREATE OR REPLACE FUNCTION demo.total(key bigint) RETURNS numeric
LANGUAGE sql STABLE
AS $$ SELECT COALESCE(sum(amount), 1) FROM demo.orders WHERE id = key $$;

上面的语句改的是函数体(签名不变),属于真正的替换。若改成 key int:

sql
CREATE OR REPLACE FUNCTION demo.total(key int) RETURNS numeric
LANGUAGE sql STABLE
AS $$ SELECT COALESCE(sum(amount), 2) FROM demo.orders WHERE id = key $$;

\df demo.total
text
                            List of functions
 Schema | Name  | Result data type | Argument data types | Type
--------+-------+------------------+---------------------+------
 demo   | total | numeric          | key bigint          | func
 demo   | total | numeric          | key int             | func
 demo   | total | numeric          | key text            | func
(3 rows)

现在有三个 total。这正是要警惕的场景。

VARIADIC 实战:按多个标签筛选 ​

sql
CREATE FUNCTION demo.orders_with_all_tags(VARIADIC p_tags text[])
RETURNS TABLE(id bigint, customer text, tags text[])
LANGUAGE sql STABLE
AS $$
  SELECT o.id, o.customer, o.tags
  FROM demo.orders AS o
  WHERE o.tags @> p_tags
  ORDER BY o.id;
$$;

-- 展开传参:要求同时带 vip 与 wholesale
SELECT * FROM demo.orders_with_all_tags('vip', 'wholesale');
text
 id | customer |        tags
----+----------+---------------------
  1 | alice    | {vip,wholesale}
(1 row)
sql
-- 传数组:要求同时带 vip 与 priority
SELECT * FROM demo.orders_with_all_tags(VARIADIC ARRAY['vip', 'priority']);
text
 id | customer |    tags
----+----------+---------------
  4 | carol    | {vip,priority}
(1 row)
sql
-- 零参数:空数组,@> 空数组恒真,返回全部
SELECT count(*) AS all_rows FROM demo.orders_with_all_tags();
text
 all_rows
----------
        6
(1 row)

VARIADIC + plpgsql 遍历 ​

sql
CREATE FUNCTION demo.tag_report(VARIADIC p_tags text[])
RETURNS TABLE(tag text, order_count bigint)
LANGUAGE plpgsql STABLE
AS $$
DECLARE
  t text;
BEGIN
  FOREACH t IN ARRAY p_tags LOOP
    RETURN QUERY
    SELECT t, count(*)
    FROM demo.orders AS o
    WHERE t = ANY (o.tags);
  END LOOP;
END;
$$;

SELECT * FROM demo.tag_report('vip', 'retail', 'nonexistent');
text
     tag     | order_count
-------------+-------------
 vip         |           3
 retail      |           2
 nonexistent |           0
(3 rows)

排查与清理 ​

sql
-- 查出某个名字下的全部重载及其定义入口
SELECT p.oid::regprocedure AS signature,
       p.provolatile      AS volatility,
       p.prorettype::regtype AS returns
FROM pg_proc AS p
JOIN pg_namespace AS n ON n.oid = p.pronamespace
WHERE p.proname = 'total' AND n.nspname = 'demo'
ORDER BY p.oid;
text
      signature       | volatility | returns
----------------------+------------+---------
 demo.total(bigint)   | s          | numeric
 demo.total(integer)  | s          | numeric
 demo.total(text)     | s          | numeric
(3 rows)
sql
DROP FUNCTION demo.tag_report(text[]);
DROP FUNCTION demo.orders_with_all_tags(text[]);
DROP FUNCTION demo.category(numeric);
DROP FUNCTION demo.category(real);
DROP FUNCTION demo.total(bigint);
DROP FUNCTION demo.total(integer);
DROP FUNCTION demo.total(text);
DROP SCHEMA demo CASCADE;

注意事项 ​

  • 参数名与默认值不构成签名。改名字、改默认值都会与既有函数冲突,必须 CREATE OR REPLACE 或先 DROP。

  • 改参数类型等于新建函数。旧重载会保留,调用方可能仍命中旧版,这是最常见的「改了不生效」原因。

  • 精确匹配优先于隐式转换。pick(1) 永远命中 pick(integer),不会命中 pick(bigint)。

  • 同优先级多候选报 is not unique。用显式 ::类型 消解,不要指望 PostgreSQL「猜」。

  • VARIADIC 必须是最后一个参数、类型必须是数组、只能有一个,三条硬规则违反任一条都报错。

  • 传数组给 VARIADIC 必须加 VARIADIC 关键字,否则数组被当成单个实参,报 function does not exist。

  • VARIADIC 可以零实参,此时收到空数组;array_length 对空数组返回 NULL 而不是 0。

  • concat 与 concat_ws 对 NULL 的处理不同:前者当空串(保留位置),后者跳过(不产生额外分隔符)。

  • jsonb_build_object 参数必须成对,奇数个报 argument list must have even number of elements。

  • DROP FUNCTION 必须带签名,重载存在时不写签名报 is not unique。

  • search_path 变化会导致同名函数解析到不同实现。跨 schema 场景用全限定名调用。

  • OUT 参数不进签名。这决定了 f(IN a int, OUT b int) 与 f(IN a int) RETURNS int 无法共存。

  • 重载过多会显著增加解析开销与排查难度。同一名字下的重载建议不超过 3~5 个。

  • \df 默认只显示部分信息,用 \df+ 看易变性、所有者、描述;用 \df schema.* 限定范围。

  • 重载无法重载「仅返回类型不同」。f() RETURNS int 与 f() RETURNS text 是两个签名完全相同的函数,不能共存。

最佳实践 ​

  • 只在语义完全相同时重载。「按名汇总」与「按 id 汇总」如果语义不同,就用 total_by_name / total_by_id 两个名字。

  • 重载数量控制在 2~3 个,避免解析歧义与维护负担。

  • 改函数前先 \df 函数名 看清现有签名,改完再核对。把这一步写进上线检查单。

  • 给关键函数调用加显式类型转换,尤其是在 SQL 脚本与迁移文件中,避免依赖隐式解析:

    sql
    SELECT app_total(1::bigint);
  • 用 regprocedure 做签名固化测试:

    sql
    SELECT 'app_total(integer)'::regprocedure IS NOT NULL AS exists;
  • VARIADIC 函数的调用一律显式写 VARIADIC 当传的是数组变量时,避免「忘了写关键字」的报错。

  • VARIADIC 函数体优先用 unnest + 聚合,比 FOREACH 循环更 SQL 化、通常也更快:

    sql
    SELECT sum(length(w)) FROM unnest(p_words) AS w;
  • VARIADIC 参数给命名前缀(p_tags、p_nums),避免与列名、变量名撞车。

  • 导出函数清单作为基线,纳入版本管理,便于发现意外新增的重载:

    sql
    SELECT p.oid::regprocedure
    FROM pg_proc AS p
    JOIN pg_namespace AS n ON n.oid = p.pronamespace
    WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
    ORDER BY 1;
  • 删除重载用脚本生成,不要手写,减少漏删:

    sql
    SELECT format('DROP FUNCTION %s;', p.oid::regprocedure)
    FROM pg_proc AS p
    JOIN pg_namespace AS n ON n.oid = p.pronamespace
    WHERE p.proname = '目标名' AND n.nspname = '目标schema';
  • 搜索路径明确的函数用全限定名调用,尤其在扩展、触发器、SECURITY DEFINER 场景。

  • 为每个重载写 COMMENT,注明与其他重载的区别,\df+ 时能直接看到。

下一节

继续学习:IMMUTABLE / STABLE / VOLATILE

参考链接 ​