40.5. 基本语句

本节以及随后的一节里, 描述所有PL/pgSQL明确可以理解的语句类型。 任何无法识别为这样类型的语句将被做为SQL命令看待, 并且被发送到主数据库引擎执行, 正如在节第 40.5.2 节第 40.5.3 节中描述的那样。

40.5.1. 赋值

给一个PL/pgSQL变量的赋值如下:

variable { := | = } expression;

如上所述,语句中的表达式是用一个发送到主数据库引擎的SELECT命令计算的。 该表达式必须生成单一的数值。表达式必须只能生成一个值 (如果变量一个行或者record,那么该值可能是一个行)。 目标变量可以是一个简单的变量(可以用一个block的名字来描述), 行或record变量的字段,或者是一个简单变量或字段的数组元素。 等号(=)可以用来替代PL/SQL兼容的:=

如果表达式的结果数据类型和变量数据类型不一致, 或者变量具有已知的尺寸/精度(比如char(20)), 结果值将隐含地被PL/pgSQL解释器用结果类型的输出 函数和变量类型的输入函数转换。 注意,如果结果数值的字符串形式不是输入函数可以接受的形式, 那么这样做可能导致类型输入函数产生的运行时错误。

例子:

tax := subtotal * 0.06;
my_record.user_id := 20;

40.5.2. 执行一个没有结果的查询

对于不返回任何行的SQL命令,例如没有RETURNING子句的INSERT, 你可以简单的在PL/pgSQL函数内写上该语句, 然后执行该函数即可。

出现在查询文本中的任何PL/pgSQL变量名都会被参数符号代替, 并在运行时将参数值替换为变量的当前值。 就像之前描述的表达式进程,可以查看资料第 40.10.1 节

当以这种方式执行一条SQL命令,这条命令在PL/pgSQL 中缓存并且在执行规划中重新使用。 正如在第 40.10.2 节中讨论的。

有时评估一个表达式或SELECT查询但是丢弃其结果也是有用的, 例如,调用一个具有副作用的函数,但对它的结果不感兴趣。 要在PL/pgSQL中这样做,可以使用PERFORM语句:

PERFORM query;

这将执行query并丢弃其结果。 用SELECT命令重写query,并将SELECT 替换为PERFORM, 对于WITH查询,使用PERFORM并且将查询放在括号中(在这种情况下, 查询只仅仅返回一行)。 这样,PL/pgSQL变量将会在查询中被照常替换。 另外,如果查询生成至少一行结果的话, 特殊变量FOUND将会被设为真,否则将被设为假。 (查阅第 40.5.5 节

注意: 有些人可能期望直接写SELECT就能同样达到此目的, 但目前确实只有PERFORM一种方法。 诸如SELECT这样返回行的查询将会被当作错误拒绝, 除非其带有一个下面将要讨论的INTO子句。

例如:

PERFORM create_mv('cs_session_page_requests_mv', my_query);

40.5.3. 执行一个仅有单行结果的查询

如果一个SQL命令的结果是一个单独的行(可能有多个字段), 那么可以将其赋予一个记录变量、行类型变量、标量变量的列表。 这可以通过在基本SQL命令之后添加一个INTO子句达到。例如:

SELECT select_expressions INTO [STRICT] target FROM ...;
INSERT ... RETURNING expressions INTO [STRICT] target;
UPDATE ... RETURNING expressions INTO [STRICT] target;
DELETE ... RETURNING expressions INTO [STRICT] target;

这里的target可以是一个记录变量、 行变量、逗号分隔的简单变量列表、逗号分隔记录/行字段列表。 PL/pgSQL变量将被照常代入查询的其余部分, 适用于带有RETURNINGSELECT, INSERT/UPDATE/DELETE, 以及返回行集合的命令(比如EXPLAIN)。 除INTO子句外, SQL命令与其在PL/pgSQL外面时完全相同。

提示: 请注意,上面带有INTOSELECTPostgreSQL普通的SELECT INTO命令是不一样的, 后者的INTO目标是一个新创建的表。 如果你想在PL/pgSQL函数里从一个SELECT结果中创建表, 那么请使用CREATE TABLE ... AS SELECT语法。

如果将一行或者一个变量列表用做目标,那么查询的结果必需作为数目或者数据类型精确匹配目标的结构, 否则就会产生运行时错误。如果目标是一个记录变量,那么它自动将自己配置成命令结果列的行类型。

INTO子句几乎可以出现在SQL命令的任何地方。 习惯上把它写在SELECT命令的select_expressions列表的之前或之后, 对于其它命令则位于结尾。 我们建议你遵守这个约定,以防万一PL/pgSQL分析器在未来的版本中变得更加严格。

如果没有在INTO指定STRICT,那么target将被设为查询返回结果的第一行或者 NULL(查询返回零行), 请注意,除非用ORDER BY进行排序,否则"the first row"是不明确的。 第一行之后的所有结果都将被丢弃。 你可以检查特殊变量FOUND(参见Section 39.5.5)来判断查询是否至少返回一行。

SELECT * INTO myrec FROM emp WHERE empname = myname;
IF NOT FOUND THEN
    RAISE EXCEPTION 'employee % not found', myname;
END IF;

如果指定了STRICT选项, 那么查询必须返回恰好一个行或者是运行时的错误, 要么是NO_DATA_FOUND(没有行),要么是TOO_MANY_ROWS(多于一行)。 可以使用异常块来捕获这些错误。例如:

BEGIN
    SELECT * INTO STRICT myrec FROM emp WHERE empname = myname;
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE EXCEPTION 'employee % not found', myname;
        WHEN TOO_MANY_ROWS THEN
            RAISE EXCEPTION 'employee % not unique', myname;
END;

成功执行了一个带有STRICT的命令之后,FOUND将总是被设为真。

对于带有RETURNINGINSERT/UPDATE/DELETE, 即使没有指定STRICTPL/pgSQL也会在返回多行时报错。 这是因为没有ORDER BY之类的选项用于确定究竟返回那一行。

如果为该函数启用了print_strict_params,那么当由于不满足 STRICT的要求而抛出错误时,那么该错误消息的DETAIL 部分将包括传递到该查询中的参数的信息。你可以通过设置 plpgsql.print_strict_params修改所有函数的 print_strict_params设置,不过只影响随后编译的函数。 也可以通过使用一个编译器选项在每个函数的底部启用它,例如:

CREATE FUNCTION get_userid(username text) RETURNS int
AS $$
#print_strict_params on
DECLARE
userid int;
BEGIN
    SELECT users.userid INTO STRICT userid
        FROM users WHERE users.username = get_userid.username;
    RETURN userid;
END
$$ LANGUAGE plpgsql;

一旦失败,这个函数会产生一个下面这样的错误消息:

ERROR:  query returned no rows
DETAIL:  parameters: $1 = 'nosuchuser'
CONTEXT:  PL/pgSQL function get_userid(text) line 6 at SQL statement

注意: STRICT兼容 Oracle PL/SQL的SELECT INTO行为以及相关语句。

对于如何处理一个SQL查询中返回的多行,参见第 40.6.4 节

40.5.4. 执行动态命令

你经常会希望在你的PL/pgSQL函数里生成动态命令。 也就是那些每次执行的时候都会涉及不同表或不同数据类型的命令。在这样的情况下, PL/pgSQL试图为命令(正如第 40.10.2 节讨论的) 缓冲执行计划的一般企图将不再合适。 为了处理这样的问题,提供了EXECUTE语句:

EXECUTE command-string [ INTO [STRICT] target ] [ USING expression [, ... ] ];

这里的command-string是一个生成字符串(类型为text)的表达式, 该字符串包含要执行的命令。 而target是一个记录变量、行变量、逗号分隔的简单变量列表、 逗号分隔的记录/行列表,来存储命令的结果。通过使用USING表达式,将参数值插入到命令中。

请特别注意在该命令字符串里将不会发生任何PL/pgSQL变量代换。 变量的数值必需在构造的时候插入该字符串的值,或者也可以使用下面介绍的参数。

同时,对于通过EXECUTE执行的命令,没有预先设置缓存计划。 相反,在该语句每次运行的时候,命令都准备一次。 命令字符串可以在过程里动态地生成以便于对各种不同的表和字段进行操作。

INTO子句声明SQL命令的结果应该传递到哪里。 如果提供了一个行变量或者一个变量列表, 那么它必须和查询生成的结果的结构一样(如果使用了记录变量,那么它回自动调整为匹配结果的结构)。 如果返回了多行,那么只有第一行将被赋予INTO变量。 如果返回零行,那么将给INTO变量赋予NULL。 如果没有声明INTO子句,则抛弃查询结果。

如果使用了STRICT选项,那么在查询没有恰好返回一个行的情况下将会报错。

该命令可以使用那些在命令中被引用为$1, $2等的参数值。 这些标签指向的是在USING子句中使用的值。 这样做可以很好的将数据值以文本类型插入到命令字符串中: 避免了运行期间在数据值和文本类型之间转换的开销, 并且这种方法不是倾向于进行SQL-injection,因为没有进行引用和转义的必要。例如:

EXECUTE 'SELECT count(*) FROM mytable WHERE inserted_by = $1 AND inserted <= $2'
   INTO c
   USING checked_user, checked_date;

需要注意的是,参数标签只能用于数据值— 如果想要使用动态的已知的表或列的名字, 那么必须将它们以文本字符串类型插入到命令中。 例如,当上面那个查询需要在一个动态选择的表上执行时,你可以这么做:

EXECUTE 'SELECT count(*) FROM '
    || tabname::regclass
    || ' WHERE inserted_by = $1 AND inserted <= $2'
   INTO c
   USING checked_user, checked_date;

另一个关于参数标签的限制是, 它们只能在SELECT, INSERT, UPDATEDELETE命令中使用。 在另一种语法类型中,通常称为通用语法中,可以将参数值以文本类型插入, 哪怕它们只是数据值。

如在上面第一个例子中的,带有一个简单常量字符串和USING参数的EXECUTE命令, 它在功能上等同于直接在PL/pgSQL中写命令, 并且允许PL/pgSQL变量自动替换。 最重要的不同之处在于,EXECUTE会在每一次执行时,根据当前的参数值更新该命令计划, 在这一点上,PL/pgSQL可能创建一个命令计划,并将 其放于缓存中以待重新使用。 当命令计划对参数值的依赖性很强时, 对于使用EXECUTE积极确保通用计划不被选择是很有帮助的。

EXECUTE命令目前不支持SELECT INTO, 但是支持一个纯SELECT命令,并且声明一个INTO作为命令本身的一部分。

注意: PL/pgSQL中的EXECUTE语法与 PostgreSQL服务器支持的EXECUTE语法无关。 服务器支持的EXECUTE语法不能 被PL/pgSQL函数直接使用(并且也没有必要)。

例 40-1. 动态查询中的引用值

使用动态命令的时候经常需要转义单引号。 建议使用美元符界定函数体内的固定文本。 如果你有没有使用美元符界定的老代码,请参考第 40.11.1 节, 这样在把老代码转换成更合理的结构时,会节省你的一些精力。

插入到构造出来的查询中的动态数值也需要特殊处理, 因为他们自己可能包含引号字符。 一个例子(这里都假设你使用了美元符作为整体,所以引号标记不需要加倍):

EXECUTE 'UPDATE tbl SET '
        || quote_ident(colname)
        || ' = '
        || quote_literal(newvalue)
        || ' WHERE key = '
        || quote_literal(keyvalue);

这个例子显示了quote_identquote_literal 函数的使用(参阅第 9.4 节)。 为了安全,包含字段和表标识符的变量应该传递给quote_ident函数。 那些包含数值的表达式,如果中的数值在构造出来的命令字符串里是文本字符串, 那么应该传递给quote_literal。 它们俩都会采取合适的步骤把输入文本包围在单或双引号里, 并且对任何嵌入其中的特殊字符进行合适的转义处理。

因为quote_literal被标记为STRICT, 当发出带有null参数的请求时, 往往会返回一个null。在上面的例子中,如果newvalue或者 keyvalue是null, 整个动态查询字符串会变成null,最终EXECUTE会报错。 可以通过使用quote_nullable函数来避免该错误, 除了当发出带有null参数的请求时,往往会返回一个字符串NULL之外, 该函数与quote_literal一样工作。例如:

EXECUTE 'UPDATE tbl SET '
        || quote_ident(colname)
        || ' = '
        || quote_nullable(newvalue)
        || ' WHERE key = '
        || quote_nullable(keyvalue);

如果处理的参数值是null,那么应该用quote_nullable来代替quote_literal

通常,应该注意确保查询中的null值返回意料之外的结果。例如:

'WHERE key = ' || quote_nullable(keyvalue)

如果keyvalue是null,那么该WHERE子句永远不会成功, 因为当=操作符带有null操作数,操作返回的结果往往是null。 如果想让null同普通关键字一样使用,那么将上面的命令修改如下:

'WHERE key IS NOT DISTINCT FROM ' || quote_nullable(keyvalue)

目前,IS NOT DISTINCT FROM处理效率不如=,因此如非必要, 不用这么做。关于null和IS DISTINCT的资料可参阅第 9.2 节

请注意美元符界定只对包围固定文本有用。如果想像下面这样做上面的例子,那就太糟糕了:

EXECUTE 'UPDATE tbl SET '
        || quote_ident(colname)
        || ' = $$'
        || newvalue
        || '$$ WHERE key = '
        || quote_literal(keyvalue);

因为如果newvalue的内容碰巧含有$$,那么这段代码就有毛病了。 同样的问题可能出现在你选用的任何美元符界定分隔符上。 因此,要想安全地包围事先不知道的文本, 必须恰当的使用quote_literalquote_nullable或者quote_ident

动态SQL语句可以使用format函数安全构建 (参阅第 9.4 节)。比如:

EXECUTE format('UPDATE tbl SET %I = %L WHERE key = %L', colname, newvalue, keyvalue);

USING子句连接中使用format函数:

EXECUTE format('UPDATE tbl SET %I = $1 WHERE key = $2', colname)
   USING newvalue, keyvalue;

这种形式更有效,因为参数newvalue and keyvalue 不转换为文本。

关于动态命令和EXECUTE的另一个例子 是例 40-9, 这个例子制作并执行了一个定义新函数的CREATE FUNCTION命令。

40.5.5. 获取结果状态

有好几种方法可以判断一条命令的效果。 第一个方法是使用GET DIAGNOSTICS,它的形式如下:

GET [ CURRENT ] DIAGNOSTICS variable { = | := } item [ , ... ];

这条命令允许检索系统状态标识符。每个item是一个关键字, 表示一个将要赋予该特定变量的状态值(该变量应该和要接收的数值类型相同)。 当前可用的状态项有ROW_COUNT、最后一个SQL命 令发送到SQL引擎处理的行数量、RESULT_OID, 最后一条SQL命令插入的最后一行的OID。 请注意RESULT_OID只有在一个向包含OID的表中INSERT 的命令之后才有用。GET DIAGNOSTICS中冒号等号(:=) 可以用来替换SQL标准的=

例如:

GET DIAGNOSTICS integer_var = ROW_COUNT;

另外一个判断命令效果的方法是一个boolean类型的特殊变量FOUND, 它在每个PL/pgSQL函数调用中FOUND开始都为假。 并被下列语句设置:

其他的PL/pgSQL声明不会改变FOUND的位置。 尤其需要注意的一点是:EXECUTE会修改GET DIAGNOSTICS的输出, 但不会修改FOUND的输出。

FOUND是每个PL/pgSQL里的局部变量; 任何对它的任何修改只影响当前的函数。

40.5.6. 什么也不做

有时一个什么也不做的占位语句也是很有用的。 例如,用于if/then/else 的空分支。 可以使用NULL语句达到这个目的。

NULL;

比如,下面的两段代码是相等的:

BEGIN
    y := x / 0;
EXCEPTION
    WHEN division_by_zero THEN
        NULL;  -- ignore the error
END;

BEGIN
    y := x / 0;
EXCEPTION
    WHEN division_by_zero THEN  -- ignore the error
END;

究竟使用哪一个取决于个人的喜好。

注意: 在Oracle的PL/SQL中,不允许出现空语句列, 所以在这种情况下必须使用NULL语句, 而PL/pgSQL允许你什么也不写。