33.4. 查询语言(SQL)函数

SQL 函数执行一个任意 SQL 查询的列表,返回列表里最后一个查询的结果。 它必须是一条 SELECT。在比较简单的情况下(非集合的情况), 返回最后一条查询结果的第一行。(请记住多行结果的"第一行"是不明确的,除非你用 ORDER BY 对结果排序。) 如果最后一个查询碰巧不返回行,那么返回 NULL 值。

另外,一个 SQL 函数可以声明为返回一个集合,方法是把该函数的返回类型声明为 SETOF sometype。 这个时候最后一条查询结果的所有行都会被返回。更多的细节在下面讲。

SQL 函数的函数体应该是一个用分号分隔的一条或多条 SQL 语句的列表。 请注意,因为 CREATE FUNCTION 命令的语法要求函数体要封闭在单引号里面, 所以在函数体中使用的单引号(')必须逃逸, 方法是写两个单引号(') 或者在需要逃逸的单引号之前放一个反斜杠(\')。

SQL 函数的参数在查询里可以用 $n 语法引用: $1 指第一个参数,$2 指第二个参数,以此类推。 如果参数是复合类型,那么可以用点表示法, 也就是说,$1.name,访问参数里的字段。

33.4.1. 基本类型上的 SQL 函数

最简单的 SQL 函数可能就是没有参数并且返回一个 基本类型,比如 integer 的函数:

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

SELECT one();

one
-----
  1

请注意我们在函数体里面定义了一个字段别名,用于函数结果(名字是 result), 但是字段别名在函数外面是不可见的。因此,结果是以 one 为标签的,而不是 result

定义一个接受基本类型做参数的 SQL 函数几乎一样简单。 在下面的例子里,请注意我们在函数中是如何用$1$2 引用参数的。

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

SELECT add_em(1, 2) AS answer;
                                                                                                                                                                            
 answer
--------
      3

下面是一个更有用的函数,我们可以用它对一个银行帐号做扣款(借记消费 debit)动作:

CREATE FUNCTION tf1 (integer, numeric) RETURNS integer AS '
    UPDATE bank 
        SET balance = balance - $2
        WHERE acctountno = $1;
        SELECT 1;'
LANGUAGE 'sql';

一个用户可以象下面这样用这个函数给帐户 17 扣款 $100.00:

SELECT tf1( 17,100.0);

实际上我们可能喜欢函数有一个比常量 1 更有用一些的结果。 所以更有可能的定义是

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

它修改余额并返回新的余额。

SQL 里面的任何命令集都可以打成一个包, 定义成一个函数。除了 SELECT 命令, 这些命令可以包含数据修改(也就是说, INSERTUPDATE, 和DELETE)。 不过,最后的命令必须是一条返回函数声明的返回类型的 SELECT。 另外,如果你想定义那些执行动作但是不返回有用的数值的 SQL 函数, 你可以把它定义成返回 void。这时候它不能以 SELECT 为最后一条语句。比如:

CREATE FUNCTION clean_EMP () RETURNS void AS '
    DELETE FROM EMP
        WHERE EMP.salary <= 0;
' LANGUAGE SQL;

SELECT clean_EMP();

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

(1 row)

33.4.2. 复合类型的SQL函数

当我们声明的函数用复合类型做参数时, 我们不仅要声明我们需要哪个参数(像上面我们使用 $1$2一样),而且要声明参数的字段。比如, 假设 emp 是一个包含雇员信息的表,并且因此也是该表每行的复合类型的名字。这里就是一个函数 double_salary,它计算某人薪水翻番之后的数值:

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

CREATE FUNCTION double_salary(emp) RETURNS integer 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
------+-------
 Sam  |  2400

请注意这里使用 $1.salary 的语法选择参数行数值的一个字段。 还要注意SELECT命令是如何使用一个表的名字表示该表的整个当前行作为复合数值。 表里面的行也可以像下面这样引用:

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

这样强调的是行的本意。

我们也可以写一个返回复合类型的函数。 下面是一个返回一行 emp 函数的例子:

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

在这个例子中我们给每个字段都赋予了一个常量, 当然我们可以用任何计算或表达式来代替这些常量。

注意定义函数的两个重要的问题:

返回一行(复合类型)的函数可以用作一个表函数,象下面描述地那样。 我们还可以在 SQL 表达式的环境里调用它,但是只有在你从该行中抽取一个字段或者把整个行传递给另外一个接受同样复合类型的函数中才可以。

下面是一个例子,显示了如何从一个行类型中抽取出一个属性:

SELECT (new_emp()).name;

 name
------
 None

我们需要一个额外的圆括弧以防止分析器误解:

SELECT new_emp().name;
ERROR:  parser: parse error at or near "."

另外一个选择是使用函数表示法进行字段抽取。解释这些问题的简单方法是我们通常交互使用attribute(table)table.attribute的表示法。

SELECT name(new_emp());

 name
------
 None

--
-- 下面的与这句话相同:
--  SELECT EMP.name AS youngster FROM EMP WHERE EMP.age < 30
--
SELECT name(EMP) AS youngster
    FROM emp
    WHERE age(EMP) < 30;

 youngster
-----------
 Sam

另外一个使用函数返回行结果的方法是声明另外一个函数, 该函数接受一个行类型参数,然后把函数结果传递给这个第二个函数:

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

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

33.4.3. 当做表数据源的 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
(2 rows)

如这个例子显示的那样,我们可以象对待一个普通表的字段一样对待函数的结果字段。

请注意我们只从该函数中获取了一行。这是因为我们没有说 SETOF。 这个问题我们在下一节描述。

33.4.4. 返回集合的 SQL 函数

如果一个 SQL 函数声明为返回 SETOF sometype。 这时候,该函数的最后的SELECT查询一直执行到结束,并且它输出的每行都当做该结果集合的一个元素返回。

这个特性通常用于把函数放在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 FUNCTION listchildren(text) RETURNS SETOF text AS
'SELECT name FROM nodes WHERE parent = $1'
LANGUAGE SQL;

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

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里,请注意没有出现Child2Child3等的行。 这是因为listchildren 为这些参数返回一个空集合, 因此不生成任何结果行。

33.4.5. 多态的 SQL 函数

SQL 函数可以声明为接受并返回多态的类型 anyelementanyarray。参阅 Section 33.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 "anyarray"/"anyelement" type because input has type "unknown"

PostgreSQL 允许有多态的参数的函数返回一个固定类型,但是反过来不行。比如:

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 function returning "anyarray" or "anyelement" must have at least one argument of either type.