PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

36.5. SQL 语言函数 #

36.5.1. SQL 函数的参数
36.5.2. 基本类型上的 SQL 函数
36.5.3. 复合类型上的 SQL 函数
36.5.4. 带输出参数的 SQL 函数
36.5.5. 带输出参数的 SQL 存储过程
36.5.6. 变长参数的 SQL 函数
36.5.7. 带默认参数值的 SQL 函数
36.5.8. 作为表数据源的 SQL 函数
36.5.9. 返回集合的 SQL 函数
36.5.10. SQL Functions Returning TABLE
36.5.11. 多态泛型 SQL 函数
36.5.12. 带排序规则的 SQL 函数

SQL 函数执行由 SQL 语句组成的任意列表,并返回列表中最后一个查询的执行结果。在简单的非集合返回(non-set)场景下,将返回最后一个查询结果的第一行数据(请注意,除非显式指定 ORDER BY 子句,否则多行结果中的“第一行”顺序是不确定的)。如果最后一个查询没有返回任何行,则函数返回空值(NULL)。

或者,可以通过将函数的返回类型声明为 SETOF sometype,或等价声明为 RETURNS TABLE(columns),将 SQL 函数声明为返回集合(即多行数据)。在此情况下,将返回最后一个查询结果的所有行。后文将详细介绍具体细节。

SQL 函数的函数体必须是由分号分隔的 SQL 语句列表。最后一条语句末尾的分号是可选的。除非函数显式声明为返回 void,否则最后一条语句必须是 SELECT 查询,或是带有 RETURNING 子句的 INSERT、UPDATE、DELETE 或 MERGE 命令。

SQL 语言中的任何命令集合都可以打包在一起定义为一个函数。除 SELECT 查询外,这些命令还可以包含数据修改语句(INSERT、UPDATE、DELETE 和 MERGE)以及其他 SQL 命令(但不能在 SQL 函数中使用事务控制命令如 COMMIT、SAVEPOINT,以及某些实用工具命令如 VACUUM)。不过,最后一条命令必须是 SELECT 或带有 RETURNING 子句,以返回声明为函数返回类型的数据。另外,如果您希望定义一个执行某些操作但无需返回有价值结果的 SQL 函数,可以将其声明为返回 void。例如,以下函数从 emp 表中删除具有负薪资的记录行:

CREATE FUNCTION clean_emp() RETURNS void AS '
    DELETE FROM emp
        WHERE salary < 0;
' LANGUAGE SQL;

SELECT clean_emp();

 clean_emp
-----------

(1 row)

您也可以将其编写为存储过程,从而避免返回类型的困扰。例如:

CREATE PROCEDURE clean_emp() AS '
    DELETE FROM emp
        WHERE salary < 0;
' LANGUAGE SQL;

CALL clean_emp();

在此类简单场景下,返回 void 的函数与存储过程之间的差异主要是代码风格上的选择。然而,存储过程提供了诸如事务控制等函数所不具备的高级功能。此外,存储过程符合 SQL 标准,而返回 void 则是 PostgreSQL 的扩展语法。

CREATE FUNCTION 命令的语法要求将函数体书写为字符串常量。通常最便捷的做法是使用美元符界定符(dollar quoting,参见 Section 4.1.2.4)。如果选择使用常规单引号字符串常量语法,则必须在函数体内对单引号(')和反斜杠(\)进行双重转义(假设启用了转义字符串语法,参见 Section 4.1.2.1)。

36.5.1. SQL 函数的参数 #

在 SQL 函数的函数体内,可以使用名称或位置编号来引用传入的参数。下文将分别展示这两种方法的具体示例。

若使用名称引用,需在声明函数参数时为其指定名称,然后在函数体中直接使用该名称即可。如果参数名称与函数内部当前 SQL 命令中的任何列名相同,列名将优先解析。若要显式指定参数以覆盖该行为,请使用函数名限定参数名,即 function_name.argument_name(如果该形式仍与带限定词的列名冲突,同样列名优先。您可以通过在 SQL 命令中为该表指定不同的别名来消除歧义)。

在传统的数字编号引用方式中,使用 $n 语法引用参数:$1 代表第一个输入参数,$2 代表第二个,依此类推。无论特定参数在声明时是否指定了名称,该方式均可正常工作。

如果参数是复合类型(composite type),则可以使用点号语法(例如 argname.fieldname 或 $1.fieldname)访问参数的内部属性。同样,在按名称引用时,可能需要使用函数名对参数名进行限定以消除歧义。

SQL 函数参数只能用作数据值,而不能用作标识符。因此例如以下用法是合法的:

INSERT INTO mytable VALUES ($1);

但以下用法则无法工作:

INSERT INTO $1 VALUES (42);

Note

使用名称引用 SQL 函数参数的功能自 PostgreSQL 9.2 起引入。若要在旧版本服务器中使用函数,必须使用 $n 位置记法。

36.5.2. 基本类型上的 SQL 函数 #

最简单的 SQL 函数不带参数,仅返回基本数据类型,例如 integer:

CREATE FUNCTION one() RETURNS integer AS $$
    SELECT 1 AS result;
$$ LANGUAGE SQL;

-- Alternative syntax for string literal:
CREATE FUNCTION one() RETURNS integer AS '
    SELECT 1 AS result;
' LANGUAGE SQL;

SELECT one();

 one
-----
   1

请注意,我们在函数体内部为函数的计算结果定义了一个列别名(名称为 result),但该列别名在函数外部并不可见。因此,外部调用返回的结果列标签为 one(即函数名)而非 result。

定义接受基本类型作为参数的 SQL 函数同样十分简单:

CREATE FUNCTION add_em(x integer, y integer) RETURNS integer AS $$
    SELECT x + y;
$$ LANGUAGE SQL;

SELECT add_em(1, 2) AS answer;

 answer
--------
      3

或者,我们可以省略参数名称,直接使用位置编号引用参数:

CREATE FUNCTION add_em(integer, integer) RETURNS integer AS $$
    SELECT $1 + $2;
$$ LANGUAGE SQL;

SELECT add_em(1, 2) AS answer;

 answer
--------
      3

下面是一个更具实用价值的函数示例,可用于对银行账户进行扣款:

CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS numeric AS $$
    UPDATE bank
        SET balance = balance - debit
        WHERE accountno = tf1.accountno;
    SELECT 1;
$$ LANGUAGE SQL;

用户可以按如下方式执行该函数,将账户 17 扣减 100.00:

SELECT tf1(17, 100.0);

在此示例中,我们将第一个参数命名为 accountno,但这与 bank 表中的列名相同。在 UPDATE 命令内部,accountno 将优先指代列 bank.accountno,因此必须使用 tf1.accountno 限定以指代函数参数。当然,通过为参数选择不同的名称即可完全避免此歧义。

在实际业务中,我们通常希望函数返回更有意义的结果而非固定常量 1,因此更合理的定义如下:

CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS numeric AS $$
    UPDATE bank
        SET balance = balance - debit
        WHERE accountno = tf1.accountno;
    SELECT balance FROM bank WHERE accountno = tf1.accountno;
$$ LANGUAGE SQL;

该函数在调整余额后返回最新的账户余额。通过使用 RETURNING 子句,可以在单条命令中实现相同的效果:

CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS numeric AS $$
    UPDATE bank
        SET balance = balance - debit
        WHERE accountno = tf1.accountno
    RETURNING balance;
$$ LANGUAGE SQL;

如果 SQL 函数中的最后一条 SELECT 或 RETURNING 子句返回的类型与函数声明的返回类型不完全一致,只要存在隐式或赋值类型转换,PostgreSQL 就会自动将结果值转换为目标类型。否则必须编写显式类型转换。例如,假设我们希望前面的 add_em 函数返回 float8 类型,只需编写:

CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
    SELECT $1 + $2;
$$ LANGUAGE SQL;

因为 integer 求和结果可以被隐式转换为 float8(有关类型转换的更多信息,请参阅 Chapter 10 或 CREATE CAST)。

36.5.3. 复合类型上的 SQL 函数 #

编写接受复合类型参数的函数时,不仅需要指定要访问哪个参数,还需指定该参数的具体属性(字段)。例如,假设 emp 是一张包含员工数据的表,因此它也是该表每行数据对应的复合类型名称。以下函数 double_salary 计算某员工薪水翻倍后的数值:

CREATE TABLE emp (
    name        text,
    salary      numeric,
    age         integer,
    cubicle     point
);

INSERT INTO emp VALUES ('Bill', 4200, 45, '(2,1)');

CREATE FUNCTION double_salary(emp) RETURNS numeric AS $$
    SELECT $1.salary * 2 AS salary;
$$ LANGUAGE SQL;

SELECT name, double_salary(emp.*) AS dream
    FROM emp
    WHERE emp.cubicle ~= point '(2,1)';

 name | dream
------+-------
 Bill |  8400

请注意使用 $1.salary 语法来选取参数行数据中的一个字段。还要注意外层调用的 SELECT 命令如何使用 table_name.* 选取整行作为复合值。也可以仅用表名指代表行,如下所示:

SELECT name, double_salary(emp) AS dream
    FROM emp
    WHERE emp.cubicle ~= point '(2,1)';

但这种用法容易造成混淆,因此已不推荐使用(有关表行复合值的这两种表示方式的详细信息,请参阅 Section 8.16.5)。

有时动态构造复合参数值非常方便。这可以通过 ROW 构造器来实现。例如,我们可以调整传递给函数的数据:

SELECT name, double_salary(ROW(name, salary*1.1, age, cubicle)) AS dream
    FROM emp;

同样可以构建返回复合类型的函数。以下是返回单个 emp 行的函数示例:

CREATE FUNCTION new_emp() RETURNS emp AS $$
    SELECT text 'None' AS name,
        1000.0 AS salary,
        25 AS age,
        point '(2,2)' AS cubicle;
$$ LANGUAGE SQL;

在此示例中,我们为每个属性指定了常量值,但完全可以使用任何计算表达式来替换这些常量。

在定义此类函数时请注意两个关键要点:

  • 查询语句中目标列表(select list)的顺序必须与列在复合类型中出现的顺序完全一致(如上所示对列命名对系统而言是无关紧要的)。

  • 必须确保每个表达式的类型能够转换为复合类型中对应列的类型。否则将产生如下错误:

    
    ERROR:  return type mismatch in function declared to return emp
    DETAIL:  Final statement returns text instead of point at column 4.
    
    

    与基本类型情况相同,系统不会自动插入显式类型转换,仅支持隐式或赋值类型转换。

定义相同函数的另一种方式是:

CREATE FUNCTION new_emp() RETURNS emp AS $$
    SELECT ROW('None', 1000.0, 25, '(2,2)')::emp;
$$ LANGUAGE SQL;

这里我们编写了一个仅返回正确复合类型的单列的 SELECT 语句。在此场景下它并没有本质优势,但在某些特定场景下是非常实用的替代方案——例如,如果我们需要通过调用另一个返回目标复合值的函数来计算结果。另一个例子是,如果我们编写的函数返回复合类型上的域(domain over composite)而非纯复合类型,则必须编写为返回单列的形式,因为无法对整行结果执行强制域转换。

我们可以直接在值表达式中调用此函数:

SELECT new_emp();

         new_emp
--------------------------
 (None,1000.0,25,"(2,2)")

或者将其作为表函数进行调用:

SELECT * FROM new_emp();

 name | salary | age | cubicle
------+--------+-----+---------
 None | 1000.0 |  25 | (2,2)

第二种调用方式在 Section 36.5.8 中有更详尽的说明。

使用返回复合类型的函数时,您可能仅需要其结果中的某个特定字段(属性)。可以使用如下语法:

SELECT (new_emp()).name;

 name
------
 None

必须添加额外的圆括号以防止解析器产生歧义。如果省略圆括号,将报错如下:

SELECT new_emp().name;
ERROR:  syntax error at or near "."
LINE 1: SELECT new_emp().name;
                        ^

另一种提取属性的选择是使用函数式记法:

SELECT name(new_emp());

 name
------
 None

正如 Section 8.16.5 中所解释的,字段属性记法与函数式记法是完全等价的。

使用返回复合类型函数的另一种方式是将结果传递给接受对应行类型作为输入的其他函数:

CREATE FUNCTION getname(emp) RETURNS text AS $$
    SELECT $1.name;
$$ LANGUAGE SQL;

SELECT getname(new_emp());
 getname
---------
 None
(1 row)

36.5.4. 带输出参数的 SQL 函数 #

描述函数结果的另一种方式是使用输出参数(output parameters),如下例所示:

CREATE FUNCTION add_em (IN x int, IN y int, OUT sum int)
AS 'SELECT x + y'
LANGUAGE SQL;

SELECT add_em(3,7);
 add_em
--------
     10
(1 row)

这与 Section 36.5.2 中展示的 add_em 版本在本质上没有区别。输出参数的真正价值在于,它提供了一种便捷的方式来定义返回多列的函数。例如:

CREATE FUNCTION sum_n_product (x int, y int, OUT sum int, OUT product int)
AS 'SELECT x + y, x * y'
LANGUAGE SQL;

 SELECT * FROM sum_n_product(11,42);
 sum | product
-----+---------
  53 |     462
(1 row)

这里的本质变化是我们为函数的结果创建了一个匿名的复合类型。上述示例与以下写法的最终效果完全一致:

CREATE TYPE sum_prod AS (sum int, product int);

CREATE FUNCTION sum_n_product (int, int) RETURNS sum_prod
AS 'SELECT $1 + $2, $1 * $2'
LANGUAGE SQL;

但无需单独定义具名复合类型通常更加便捷。请注意,赋予输出参数的名称并非装饰,它们直接决定了匿名复合类型的列名(如果省略输出参数的名称,系统会自动选择一个列名)。

请注意,从 SQL 中调用此类函数时,调用参数列表中不包含输出参数。这是因为 PostgreSQL 仅依据输入参数来确定函数的调用签名。这也意味着在删除或引用函数时,仅输入参数具有实际意义。我们可以通过以下任意一种方式删除上述函数:

DROP FUNCTION sum_n_product (x int, y int, OUT sum int, OUT product int);
DROP FUNCTION sum_n_product (int, int);

参数可以标记为 IN(默认值)、OUT、INOUT 或 VARIADIC。INOUT 参数既作为输入参数(属于调用参数列表的一部分),又作为输出参数(属于返回记录类型的一部分)。VARIADIC 可变参数也是输入参数,但如后文所述具有特殊处理机制。

36.5.5. 带输出参数的 SQL 存储过程 #

存储过程同样支持输出参数,但其工作机制与函数略有不同。在 CALL 命令中,输出参数必须包含在实参列表中。例如,前面银行账户扣款例程可以改写为如下存储过程:

CREATE PROCEDURE tp1 (accountno integer, debit numeric, OUT new_balance numeric) AS $$
    UPDATE bank
        SET balance = balance - debit
        WHERE accountno = tp1.accountno
    RETURNING balance;
$$ LANGUAGE SQL;

调用该存储过程时,必须传入一个与 OUT 参数相匹配的参数占位符,习惯上书写为 NULL:

CALL tp1(17, 100.0, NULL);

若传入其他表达式,则该表达式必须能够隐式转换为该参数的声明类型(与输入参数规则一致)。但请注意,该表达式本身并不会被求值。

从 PL/pgSQL 中调用存储过程时,不能写 NULL,而必须提供一个变量用于接收存储过程的输出结果。详见 Section 41.6.3。

36.5.6. 变长参数的 SQL 函数 #

只要所有“可选”实参属于相同的数据类型,SQL 函数即可声明为接受可变数量的参数。这些可选实参将以数组形式传递给函数。声明该函数时需将最后一个形参标记为 VARIADIC,并且该参数必须声明为数组类型。例如:

CREATE FUNCTION mleast(VARIADIC arr numeric[]) RETURNS numeric AS $$
    SELECT min($1[i]) FROM generate_subscripts($1, 1) g(i);
$$ LANGUAGE SQL;

SELECT mleast(10, -1, 5, 4.4);
 mleast
--------
     -1
(1 row)

实际上,位于或超出 VARIADIC 形参位置的所有实参都会被打包成一维数组,效果相当于执行了:

SELECT mleast(ARRAY[10, -1, 5, 4.4]);    -- 直接这样写无法执行

但您不能直接按上述方式编写调用——或者说这样写无法匹配当前函数定义。因为标记为 VARIADIC 的形参匹配的是其元素类型的一个或多个实例,而非数组类型本身。

有时将已构建好的数组直接传递给可变参数函数非常有用,尤其是当一个可变参数函数希望将其数组参数透传给另一个函数时。此外,在允许不受信任用户创建对象的模式中调用可变参数函数时,这也是唯一安全的方式(详见 Section 10.3)。可以在调用时显式指定 VARIADIC 关键字:

SELECT mleast(VARIADIC ARRAY[10, -1, 5, 4.4]);

这样可以阻止将函数的变长参数展开为其元素类型,从而使数组实参值能够正常匹配。VARIADIC 仅能附加在函数调用的最后一个实参上。

在调用中指定 VARIADIC 也是向变长参数函数传递空数组的唯一方式,例如:

SELECT mleast(VARIADIC ARRAY[]::numeric[]);

仅书写 SELECT mleast() 无法正常工作,因为变长参数必须至少匹配一个实参(如果您希望允许无参调用,可以额外定义一个同名为 mleast 且不带参数的函数)。

从变长参数生成的数组元素参数被视为没有独立名称。这意味着不能使用命名参数记法(参见 Section 4.3)调用变长函数,除非显式指定了 VARIADIC。例如以下写法可以工作:

SELECT mleast(VARIADIC arr => ARRAY[10, -1, 5, 4.4]);

但以下写法均无法工作:

SELECT mleast(arr => 10);
SELECT mleast(arr => ARRAY[10, -1, 5, 4.4]);

36.5.7. 带默认参数值的 SQL 函数 #

函数可以为部分或全部输入参数声明默认值。当调用函数时提供的实参数量不足时,将自动代入默认值。由于实参只能从实参列表的尾部开始省略,因此位于具有默认值的参数之后的所有形参也必须具有默认值(虽然使用具名参数记法理论上可以放宽此限制,但系统仍强制执行该规则以确保位置参数记法行为合理)。无论是否使用默认值,在存在互不信任用户的数据库中调用函数时都需要格外注意安全防范(参见 Section 10.3)。

例如:

CREATE FUNCTION foo(a int, b int DEFAULT 2, c int DEFAULT 3)
RETURNS int
LANGUAGE SQL
AS $$
    SELECT $1 + $2 + $3;
$$;

SELECT foo(10, 20, 30);
 foo
-----
  60
(1 row)

SELECT foo(10, 20);
 foo
-----
  33
(1 row)

SELECT foo(10);
 foo
-----
  15
(1 row)

SELECT foo();  -- 由于第一个参数没有默认值,此调用失败
ERROR:  function foo() does not exist

等号 = 也可以替代关键字 DEFAULT。

36.5.8. 作为表数据源的 SQL 函数 #

所有 SQL 函数均可用于查询语句的 FROM 子句中,但这对于返回复合类型的函数尤为有用。若函数定义为返回基本类型,则表函数产生单列表;若函数定义为返回复合类型,表函数会为复合类型的每个属性生成一列。

示例如下:

CREATE TABLE foo (fooid int, foosubid int, fooname text);
INSERT INTO foo VALUES (1, 1, 'Joe');
INSERT INTO foo VALUES (1, 2, 'Ed');
INSERT INTO foo VALUES (2, 1, 'Mary');

CREATE FUNCTION getfoo(int) RETURNS foo AS $$
    SELECT * FROM foo WHERE fooid = $1;
$$ LANGUAGE SQL;

SELECT *, upper(fooname) FROM getfoo(1) AS t1;

 fooid | foosubid | fooname | upper
-------+----------+---------+-------
     1 |        1 | Joe     | JOE
(1 row)

如示例所示,我们可以像操作普通表的列一样操作函数返回结果的各个列。

请注意,该函数仅返回了一行数据。这是因为我们没有使用 SETOF。下一节将对此进行说明。

36.5.9. 返回集合的 SQL 函数 #

当一个 SQL 函数被声明为返回 SETOF sometype 时,函数的最终查询语句将执行完毕,其输出的每一行都会作为结果集的一个元素返回。

该特性通常在 FROM 子句中调用函数时使用。在这种情况下,函数返回的每一行都将转换为查询所见表的一行。例如,假设表 foo 具有上述相同的内容,我们执行:

CREATE FUNCTION getfoo(int) RETURNS SETOF foo AS $$
    SELECT * FROM foo WHERE fooid = $1;
$$ LANGUAGE SQL;

SELECT * FROM getfoo(1) AS t1;

我们将得到:

 fooid | foosubid | fooname
-------+----------+---------
     1 |        1 | Joe
     1 |        2 | Ed
(2 rows)

也可以通过输出参数定义各列来返回多行数据,如下所示:

CREATE TABLE tab (y int, z int);
INSERT INTO tab VALUES (1, 2), (3, 4), (5, 6), (7, 8);

CREATE FUNCTION sum_n_product_with_tab (x int, OUT sum int, OUT product int)
RETURNS SETOF record
AS $$
    SELECT $1 + tab.y, $1 * tab.y FROM tab;
$$ LANGUAGE SQL;

SELECT * FROM sum_n_product_with_tab(10);
 sum | product
-----+---------
  11 |      10
  13 |      30
  15 |      50
  17 |      70
(4 rows)

此处的关键点在于必须书写 RETURNS SETOF record,以指明函数返回多行而非单行记录。如果只有一个输出参数,则直接书写该参数的类型而非 record。

通过多次调用返回集合的函数来构造查询结果是非常实用的,其中每次调用的参数来自表或子查询的连续行。推荐的做法是使用 LATERAL 关键字,如 Section 7.2.1.5 中所述。以下示例使用集合返回函数枚举树形结构的元素:

SELECT * FROM nodes;
   name    | parent
-----------+--------
 Top       |
 Child1    | Top
 Child2    | Top
 Child3    | Top
 SubChild1 | Child1
 SubChild2 | Child1
(6 rows)

CREATE FUNCTION listchildren(text) RETURNS SETOF text AS $$
    SELECT name FROM nodes WHERE parent = $1
$$ LANGUAGE SQL STABLE;

SELECT * FROM listchildren('Top');
 listchildren
--------------
 Child1
 Child2
 Child3
(3 rows)

SELECT name, child FROM nodes, LATERAL listchildren(name) AS child;
  name  |   child
--------+-----------
 Top    | Child1
 Top    | Child2
 Top    | Child3
 Child1 | SubChild1
 Child1 | SubChild2
(5 rows)

虽然本例中实现的效果也可以通过简单连接达成,但在更复杂的计算中,将部分工作拆解封装到函数中会极为便利。

集合返回函数也可以直接在查询的选择列表(select list)中调用。对于查询自身生成的每一行,系统都会调用一次该集合返回函数,并为该函数结果集的每个元素生成一个输出行。前面的示例也可以改用如下查询完成:

SELECT listchildren('Top');
 listchildren
--------------
 Child1
 Child2
 Child3
(3 rows)

SELECT name, listchildren(name) FROM nodes;
  name  | listchildren
--------+--------------
 Top    | Child1
 Top    | Child2
 Top    | Child3
 Child1 | SubChild1
 Child1 | SubChild2
(5 rows)

在最后一条 SELECT 中,请注意对于 Child2、Child3 等未出现输出行。这是因为 listchildren 针对这些实参返回了空集,因而没有生成任何结果行。这与使用 LATERAL 语法对函数结果执行内连接的表现完全一致。

PostgreSQL 处理位于选择列表中的集合返回函数的行为,与将其书写在 LATERAL FROM 子句项中的表现几乎完全相同。例如:

SELECT x, generate_series(1,5) AS g FROM tab;

几乎等价于:

SELECT x, g FROM tab, LATERAL generate_series(1,5) AS g;

二者唯一的区别在于,在当前具体示例中,由于 g 对 tab 没有实际的横向依赖,规划器可以选择将 g 置于嵌套循环连接的外侧,从而产生不同的行输出顺序。选择列表中的集合返回函数始终被视为位于与 FROM 子句其余部分进行嵌套循环连接的内侧进行求值,因此在考虑 FROM 子句的下一行之前,该函数会完整执行完毕。

如果查询的选择列表中包含多个集合返回函数,其行为类似于将这些函数放入单个 LATERAL ROWS FROM( ... ) 子句中。对于底层查询的每一行,首先使用每个函数的第一个结果生成一行输出,然后使用第二个结果生成下一行输出,依此类推。如果某些集合返回函数产生的输出少于其他函数,缺失的数据将以 NULL 值填充,从而使单个底层行发出的总行数与产生最多输出的集合返回函数的行数一致。因此,各集合返回函数会“步调一致”(lockstep)地并行运行直至全部耗尽,随后再继续处理下一个底层行。

集合返回函数可以在选择列表中嵌套调用,尽管这在 FROM 子句项中是不允许的。在此类情况下,每个嵌套层级都被单独处理,如同一个独立的 LATERAL ROWS FROM( ... ) 项。例如在:

SELECT srf1(srf2(x), srf3(y)), srf4(srf5(z)) FROM tab;

中,对于 tab 的每一行,集合返回函数 srf2、srf3 和 srf5 会首先步调一致地同步运行,随后 srf1 和 srf4 会针对底层函数生成的每一行再次同步执行。

集合返回函数不能在条件求值结构(例如 CASE 或 COALESCE)内部使用。例如考虑:

SELECT x, CASE WHEN x > 0 THEN generate_series(1, 5) ELSE 0 END FROM tab;

直观看来,这似乎应该对满足 x > 0 的输入行重复输出 5 次,而对不满足的仅输出 1 次;但实际上,由于 generate_series(1, 5) 会在 CASE 表达式求值之前在隐式 LATERAL FROM 项中运行,它会导致对每个输入行都重复 5 次。为了避免混淆,此类用法将在语法解析期直接报错。

Note

如果函数的最后一条命令是带有 RETURNING 的 INSERT、UPDATE、DELETE 或 MERGE,即使该函数未声明为 SETOF,或者外层调用查询未获取所有结果行,该数据修改命令也会始终执行完毕。RETURNING 子句产生的任何多余行都会被静默丢弃,但指示的表数据修改仍然会完整发生(并在从函数返回之前全部完成)。

Note

在 PostgreSQL 10 之前,在同一个选择列表中放置多个集合返回函数的行为并不十分直观,除非它们产生的行数完全一致;否则,最终输出的行数是各函数产生行数的最小公倍数(LCM)。此外,嵌套集合返回函数的行为也与上述机制不同。过去还允许在 CASE 内部进行条件执行,这使得情况更加复杂。编写需要兼容旧版本 PostgreSQL 的查询时,建议使用 LATERAL 语法,这能在各版本间提供一致的结果。若现有查询依赖集合返回函数的条件求值,可以通过将条件分支移入自定义集合返回函数来修复,例如:

SELECT x, CASE WHEN y > 0 THEN generate_series(1, z) ELSE 5 END FROM tab;

可以改写为:

CREATE FUNCTION case_generate_series(cond bool, start int, fin int, els int)
  RETURNS SETOF int AS $$
BEGIN
  IF cond THEN
    RETURN QUERY SELECT generate_series(start, fin);
  ELSE
    RETURN QUERY SELECT els;
  END IF;
END$$ LANGUAGE plpgsql;

SELECT x, case_generate_series(y > 0, 1, z, 5) FROM tab;

这种改写形式在所有 PostgreSQL 版本中都具有相同的确定性行为。

36.5.10. SQL Functions Returning TABLE #

声明函数返回集合的另一种方式是使用 RETURNS TABLE(columns) 语法。这等价于使用一个或多个 OUT 参数并将函数标记为返回 SETOF record(或在仅有一个输出参数时返回 SETOF 该类型)。此记法已在较新的 SQL 标准中进行了规范,因此相比使用 SETOF 具备更好的跨数据库可移植性。

例如,前面的求和与求乘积示例也可以通过此方式实现:

CREATE FUNCTION sum_n_product_with_tab (x int)
RETURNS TABLE(sum int, product int) AS $$
    SELECT $1 + tab.y, $1 * tab.y FROM tab;
$$ LANGUAGE SQL;

使用 RETURNS TABLE 记法时,不允许同时使用显式的 OUT 或 INOUT 形参——所有输出列都必须包含在 TABLE 列定义列表中。

36.5.11. 多态泛型 SQL 函数 #

SQL 函数可以声明为接受并返回 Section 36.2.5 中所述的多态类型。以下是一个多态函数 make_array,它将两个任意相同类型的元素组装成一个数组:

CREATE FUNCTION make_array(anyelement, anyelement) RETURNS anyarray AS $$
    SELECT ARRAY[$1, $2];
$$ LANGUAGE SQL;

SELECT make_array(1, 2) AS intarray, make_array('a'::text, 'b') AS textarray;
 intarray | textarray
----------+-----------
 {1,2}    | {a,b}
(1 row)

请注意使用显式类型转换 'a'::text 来指定该实参属于 text 类型。如果实参只是普通字符串字面量,则这是必须的,否则它会被视为 unknown 类型,而 unknown 的数组不是合法类型。若不进行类型转换,将收到如下错误:

ERROR:  could not determine polymorphic type because input has type unknown

按照上述方式声明的 make_array,您必须提供两个数据类型完全相同的实参;系统不会尝试进行类型协调。因此例如以下调用将无法执行:

SELECT make_array(1, 2.5) AS numericarray;
ERROR:  function make_array(integer, numeric) does not exist

另一种解决方案是使用“通用兼容”(common)多态类型族,它允许系统自动尝试推导合适的公共类型:

CREATE FUNCTION make_array2(anycompatible, anycompatible)
RETURNS anycompatiblearray AS $$
    SELECT ARRAY[$1, $2];
$$ LANGUAGE SQL;

SELECT make_array2(1, 2.5) AS numericarray;
 numericarray
--------------
 {1,2.5}
(1 row)

由于当所有输入均为未知类型时,通用类型解析规则默认选择 text 类型,因此以下写法也能正常工作:

SELECT make_array2('a', 'b') AS textarray;
 textarray
-----------
 {a,b}
(1 row)

允许函数拥有多态参数而返回固定类型,但反之则不允许。例如:

CREATE FUNCTION is_greater(anyelement, anyelement) RETURNS boolean AS $$
    SELECT $1 > $2;
$$ LANGUAGE SQL;

SELECT is_greater(1, 2);
 is_greater
------------
 f
(1 row)

CREATE FUNCTION invalid_func() RETURNS anyelement AS $$
    SELECT 1;
$$ LANGUAGE SQL;
ERROR:  cannot determine result data type
DETAIL:  A result of type anyelement requires at least one input of type anyelement, anyarray, anynonarray, anyenum, or anyrange.

多态机制同样适用于包含输出参数的函数。例如:

CREATE FUNCTION dup (f1 anyelement, OUT f2 anyelement, OUT f3 anyarray)
AS 'select $1, array[$1,$1]' LANGUAGE SQL;

SELECT * FROM dup(22);
 f2 |   f3
----+---------
 22 | {22,22}
(1 row)

多态特性也可与可变参数函数结合使用。例如:

CREATE FUNCTION anyleast (VARIADIC anyarray) RETURNS anyelement AS $$
    SELECT min($1[i]) FROM generate_subscripts($1, 1) g(i);
$$ LANGUAGE SQL;

SELECT anyleast(10, -1, 5, 4);
 anyleast
----------
       -1
(1 row)

SELECT anyleast('abc'::text, 'def');
 anyleast
----------
 abc
(1 row)

CREATE FUNCTION concat_values(text, VARIADIC anyarray) RETURNS text AS $$
    SELECT array_to_string($2, $1);
$$ LANGUAGE SQL;

SELECT concat_values('|', 1, 4, 2);
 concat_values
---------------
 1|4|2
(1 row)

36.5.12. 带排序规则的 SQL 函数 #

当 SQL 函数具有一个或多个支持排序规则(collatable)的数据类型形参时,系统会根据传递给实参的排序规则为每次函数调用确定一个排序规则(详见 Section 23.2)。如果成功识别出唯一的排序规则(即各实参之间不存在隐式排序规则冲突),则所有支持排序规则的参数都会隐式赋予该排序规则。这将直接影响函数内部对排序规则敏感的操作行为。例如使用前述的 anyleast 函数,以下调用的结果:

SELECT anyleast('abc'::text, 'ABC');

将取决于数据库的默认排序规则。在 C 区域设置下结果为 ABC,但在许多其他区域设置下结果可能为 abc。可以通过为任一实参添加 COLLATE 子句来强制指定排序规则,例如:

SELECT anyleast('abc'::text, 'ABC' COLLATE "C");

另外,如果您希望函数始终使用特定的排序规则执行而忽略外部调用环境,可以在函数体定义中按需插入 COLLATE 子句。以下版本的 anyleast 将始终使用 en_US 区域设置比较字符串:

CREATE FUNCTION anyleast (VARIADIC anyarray) RETURNS anyelement AS $$
    SELECT min($1[i] COLLATE "en_US") FROM generate_subscripts($1, 1) g(i);
$$ LANGUAGE SQL;

但请注意,若将该函数应用于不支持排序规则的数据类型,系统将抛出错误。

如果在实际参数中无法确定公共排序规则,SQL 函数会将其参数视为具有其数据类型的默认排序规则(通常为数据库的默认排序规则,但对于域类型参数可能有所不同)。

支持排序规则的参数行为可以被视为一种受限的多态形式,仅适用于文本类数据类型。