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

41.6. 控制结构 #

41.6.1. 从函数返回
41.6.2. 从存储过程返回
41.6.3. 调用存储过程
41.6.4. 条件分支结构
41.6.5. 简单循环
41.6.6. 遍历查询结果集循环
41.6.7. 遍历数组循环
41.6.8. 捕获异常与错误处理
41.6.9. 获取当前执行位置信息

控制结构可能是 PL/pgSQL 最有用(也最重要)的部分。借助 PL/pgSQL 的控制结构,你可以以非常灵活而强大的方式操纵 PostgreSQL 数据。

41.6.1. 从函数返回 #

有两个命令可用于从函数返回数据:RETURN 和 RETURN NEXT。

41.6.1.1. RETURN 语句 #

RETURN expression;

带表达式的 RETURN 会终止函数, 并把 expression 的值返回给调用者。 这种形式用于不返回集合的 PL/pgSQL 函数。

在返回标量类型的函数中,表达式的结果会像赋值时描述的那样 被自动转换为函数的返回类型。但要返回复合(行)值, 必须书写一个恰好交付所需列集合的表达式。 这可能需要使用显式转换。

如果你用输出参数声明了函数,只需书写不带表达式的 RETURN。输出参数变量的当前值将被返回。

如果把函数声明为返回 void, 可以使用 RETURN 语句提前退出函数; 但不要在 RETURN 后面写表达式。

函数的返回值不能处于未定义状态。如果控制流到达函数顶层块的末尾 而没有遇到 RETURN 语句,将发生运行时错误。 不过,这一限制不适用于带输出参数的函数和返回 void 的函数。在这些情况下,如果顶层块执行完毕, 会自动执行一条 RETURN 语句。

一些示例:

-- functions returning a scalar type
RETURN 1 + 2;
RETURN scalar_var;

-- functions returning a composite type
RETURN composite_type_var;
RETURN (1, 2, 'three'::text);  -- must cast columns to correct types

41.6.1.2. RETURN NEXT 与 RETURN QUERY 语句 #

RETURN NEXT expression;
RETURN QUERY query;
RETURN QUERY EXECUTE command-string [ USING expression [, ... ] ];

当 PL/pgSQL 函数被声明为返回 SETOF sometype 时, 要遵循的过程略有不同。在这种情况下,要返回的各个项 由一系列 RETURN NEXT 或 RETURN QUERY 命令指定, 然后用一条不带参数的最终 RETURN 命令 表明函数已执行完毕。RETURN NEXT 既可以用于标量数据类型,也可以用于复合数据类型; 对于复合结果类型,将返回一个完整的“表”的结果。 RETURN QUERY 把执行一个查询的结果 追加到函数的结果集中。RETURN NEXT 与 RETURN QUERY 可以在一个集合返回函数中 自由混合使用,此时它们的结果会被拼接起来。

RETURN NEXT 和 RETURN QUERY 实际上并不从函数返回 —— 它们只是把零行或多行追加到函数的结果集中。 然后执行继续进行到 PL/pgSQL 函数中的 下一条语句。随着一条条 RETURN NEXT 或 RETURN QUERY 命令被执行,结果集逐渐构建起来。 最后一条不带参数的 RETURN 会使控制流退出函数(或者你也可以直接让控制流到达函数末尾)。

RETURN QUERY 有一个变体 RETURN QUERY EXECUTE, 它指定要动态执行的查询。参数表达式可以通过 USING 插入计算得到的查询字符串中, 方式与 EXECUTE 命令完全相同。

如果你用输出参数声明了函数,只需书写不带表达式的 RETURN NEXT。每次执行时, 输出参数变量的当前值都会被保存,最终作为结果的一行返回。 注意,当有多个输出参数时必须把函数声明为返回 SETOF record;当只有一个 sometype 类型的输出参数时, 声明为返回 SETOF sometype, 这样才能创建带输出参数的集合返回函数。

下面是一个使用 RETURN NEXT 的函数示例:

CREATE TABLE foo (fooid INT, foosubid INT, fooname TEXT);
INSERT INTO foo VALUES (1, 2, 'three');
INSERT INTO foo VALUES (4, 5, 'six');

CREATE OR REPLACE FUNCTION get_all_foo() RETURNS SETOF foo AS
$BODY$
DECLARE
    r foo%rowtype;
BEGIN
    FOR r IN
        SELECT * FROM foo WHERE fooid > 0
    LOOP
        -- can do some processing here
        RETURN NEXT r; -- return current row of SELECT
    END LOOP;
    RETURN;
END;
$BODY$
LANGUAGE plpgsql;

SELECT * FROM get_all_foo();

下面是一个使用 RETURN QUERY 的函数示例:

CREATE FUNCTION get_available_flightid(date) RETURNS SETOF integer AS
$BODY$
BEGIN
    RETURN QUERY SELECT flightid
                   FROM flight
                  WHERE flightdate >= $1
                    AND flightdate < ($1 + 1);

    -- Since execution is not finished, we can check whether rows were returned
    -- and raise exception if not.
    IF NOT FOUND THEN
        RAISE EXCEPTION 'No flight at %.', $1;
    END IF;

    RETURN;
 END;
$BODY$
LANGUAGE plpgsql;

-- Returns available flights or raises exception if there are no
-- available flights.
SELECT * FROM get_available_flightid(CURRENT_DATE);

Note

如上所述,RETURN NEXT 和 RETURN QUERY 的当前实现会在从函数返回之前 存储整个结果集。这意味着,如果 PL/pgSQL 函数产生一个非常大的结果集,性能可能会很差: 为避免内存耗尽,数据会被写到磁盘上,但在整个结果集生成完毕之前, 函数本身不会返回。PL/pgSQL 的未来版本可能允许用户定义没有这一限制的集合返回函数。 目前,数据开始写入磁盘的时机由 work_mem 配置变量控制。有足够内存把更大的结果集保存在内存中的管理员 可以考虑调大这个参数。

41.6.2. 从存储过程返回 #

过程没有返回值。因此过程可以不以 RETURN 语句结束。如果你想用 RETURN 语句提前退出代码, 只写不带表达式的 RETURN。

如果过程有输出参数,输出参数变量的最终值将被返回给调用者。

41.6.3. 调用存储过程 #

PL/pgSQL 函数、过程或 DO 块可以使用 CALL 调用一个过程。输出参数的处理方式与 CALL 在普通 SQL 中的工作方式不同。过程的每个 OUT 或 INOUT 参数 都必须对应 CALL 语句中的一个变量, 过程返回的任何内容都会在该过程返回后被赋回那个变量。 例如:

CREATE PROCEDURE triple(INOUT x int)
LANGUAGE plpgsql
AS $$
BEGIN
    x := x * 3;
END;
$$;

DO $$
DECLARE myvar int := 5;
BEGIN
  CALL triple(myvar);
  RAISE NOTICE 'myvar = %', myvar;  -- prints 15
END;
$$;

对应输出参数的变量可以是简单变量,也可以是复合类型变量的字段。 目前,它不能是数组的元素。

41.6.4. 条件分支结构 #

IF 和 CASE 语句让你可以 根据特定条件执行备选的命令。 PL/pgSQL 有三种形式的 IF:

  • IF ... THEN ... END IF

  • IF ... THEN ... ELSE ... END IF

  • IF ... THEN ... ELSIF ... THEN ... ELSE ... END IF

以及两种形式的 CASE:

  • CASE ... WHEN ... THEN ... ELSE ... END CASE

  • CASE WHEN ... THEN ... ELSE ... END CASE

41.6.4.1. IF-THEN 结构 #

IF boolean-expression THEN
    statements
END IF;

IF-THEN 语句是 IF 最简单的形式。THEN 与 END IF 之间的语句会在条件为真时被执行。 否则它们将被跳过。

示例:

IF v_user_id <> 0 THEN
    UPDATE users SET email = v_email WHERE user_id = v_user_id;
END IF;

41.6.4.2. IF-THEN-ELSE 结构 #

IF boolean-expression THEN
    statements
ELSE
    statements
END IF;

IF-THEN-ELSE 语句对 IF-THEN 做了扩展, 允许你指定一组在条件不为真时应当执行的备选语句。 (注意,这包括条件求值为 NULL 的情况。)

示例:

IF parentid IS NULL OR parentid = ''
THEN
    RETURN fullname;
ELSE
    RETURN hp_true_filename(parentid) || '/' || fullname;
END IF;

IF v_count > 0 THEN
    INSERT INTO users_count (count) VALUES (v_count);
    RETURN 't';
ELSE
    RETURN 'f';
END IF;

41.6.4.3. IF-THEN-ELSIF 结构 #

IF boolean-expression THEN
    statements
[ ELSIF boolean-expression THEN
    statements
[ ELSIF boolean-expression THEN
    statements
    ...
]
]
[ ELSE
    statements ]
END IF;

有时备选项不止两个。IF-THEN-ELSIF 提供了一种依次检查多个备选项的便捷方法。 各个 IF 条件会被依次测试, 直到找到第一个为真的条件。然后执行与之关联的语句, 之后控制流传递到 END IF 之后的下一条语句。 (后续的 IF 条件不会再被测试。) 如果没有任何 IF 条件为真, 则执行 ELSE 块(如果有)。

下面是一个示例:

IF number = 0 THEN
    result := 'zero';
ELSIF number > 0 THEN
    result := 'positive';
ELSIF number < 0 THEN
    result := 'negative';
ELSE
    -- hmm, the only other possibility is that number is null
    result := 'NULL';
END IF;

关键字 ELSIF 也可以拼写成 ELSEIF。

完成同一任务的另一种方法是嵌套 IF-THEN-ELSE 语句,如下例所示:

IF demo_row.sex = 'm' THEN
    pretty_sex := 'man';
ELSE
    IF demo_row.sex = 'f' THEN
        pretty_sex := 'woman';
    END IF;
END IF;

不过,这种方法要求为每个 IF 写一个匹配的 END IF, 因此当备选项较多时,它比使用 ELSIF 繁琐得多。

41.6.4.4. 简单 CASE 语句 #

CASE search-expression
    WHEN expression [, expression [ ... ]] THEN
      statements
  [ WHEN expression [, expression [ ... ]] THEN
      statements
    ... ]
  [ ELSE
      statements ]
END CASE;

CASE 的简单形式基于操作数相等提供条件执行。 search-expression 会被求值(一次), 并依次与各 WHEN 子句中的 expression 比较。 如果找到匹配,则执行对应的 statements,随后控制流传递到 END CASE 之后的下一条语句。 (后续的 WHEN 表达式不会再被求值。) 如果没有找到匹配,则执行 ELSE 的 statements; 但如果 ELSE 不存在, 将引发 CASE_NOT_FOUND 异常。

下面是一个简单的示例:

CASE x
    WHEN 1, 2 THEN
        msg := 'one or two';
    ELSE
        msg := 'other value than one or two';
END CASE;

41.6.4.5. 搜索型 CASE 语句 #

CASE
    WHEN boolean-expression THEN
      statements
  [ WHEN boolean-expression THEN
      statements
    ... ]
  [ ELSE
      statements ]
END CASE;

CASE 的搜索形式基于布尔表达式的真假提供条件执行。 各 WHEN 子句的 boolean-expression 会被依次求值, 直到发现一个结果为 true 的表达式。 然后执行对应的 statements, 随后控制流传递到 END CASE 之后的下一条语句。 (后续的 WHEN 表达式不会再被求值。) 如果没有发现为真的结果,则执行 ELSE 的 statements; 但如果 ELSE 不存在, 将引发 CASE_NOT_FOUND 异常。

下面是一个示例:

CASE
    WHEN x BETWEEN 0 AND 10 THEN
        msg := 'value is between zero and ten';
    WHEN x BETWEEN 11 AND 20 THEN
        msg := 'value is between eleven and twenty';
END CASE;

这种形式的 CASE 与 IF-THEN-ELSIF 完全等价, 区别在于一条规则:到达被省略的 ELSE 子句 会导致错误,而不是什么都不做。

41.6.5. 简单循环 #

使用 LOOP、EXIT、 CONTINUE、WHILE、FOR 和 FOREACH 语句, 你可以让你的 PL/pgSQL 函数 重复执行一系列命令。

41.6.5.1. LOOP 基础循环 #

[ <<label>> ]
LOOP
    statements
END LOOP [ label ];

LOOP 定义一个无条件循环,它会无限重复, 直到被 EXIT 或 RETURN 语句终止。可选的 label 可以被嵌套循环中的 EXIT 和 CONTINUE 语句用来指明这些语句针对哪个循环。

41.6.5.2. EXIT 循环跳出 #

EXIT [ label ] [ WHEN boolean-expression ];

如果没有给出 label, 则终止最内层循环,并接着执行 END LOOP 之后的语句。如果给出了 label,它必须是当前层或某个外层的 嵌套循环或块的标签。此时被指名的循环或块会被终止, 控制流继续进行到该循环/块对应 END 之后的语句。

如果指定了 WHEN,则只有当 boolean-expression 为真时才会退出循环。 否则,控制流传递到 EXIT 之后的语句。

EXIT 可以用于所有类型的循环; 它并不仅限于无条件循环。

当与 BEGIN 块一起使用时, EXIT 会把控制流传递到块结束之后的下一条语句。 注意,为此目的必须使用标签;不带标签的 EXIT 永远不会被认为匹配一个 BEGIN 块。(与 8.4 之前的 PostgreSQL 版本不同, 那些版本允许不带标签的 EXIT 匹配一个 BEGIN 块。)

示例:

LOOP
    -- some computations
    IF count > 0 THEN
        EXIT;  -- exit loop
    END IF;
END LOOP;

LOOP
    -- some computations
    EXIT WHEN count > 0;  -- same result as previous example
END LOOP;

<<ablock>>
BEGIN
    -- some computations
    IF stocks > 100000 THEN
        EXIT ablock;  -- causes exit from the BEGIN block
    END IF;
    -- computations here will be skipped when stocks > 100000
END;

41.6.5.3. CONTINUE 循环推进 #

CONTINUE [ label ] [ WHEN boolean-expression ];

如果没有给出 label, 则开始最内层循环的下一次迭代。也就是说, 循环体中所有剩余的语句都被跳过, 控制流返回循环控制表达式(如果有), 以决定是否需要再一次循环迭代。 如果存在 label, 它指明将继续执行哪个循环。

如果指定了 WHEN,则只有当 boolean-expression 为真时 才开始循环的下一次迭代。否则, 控制流传递到 CONTINUE 之后的语句。

CONTINUE 可以用于所有类型的循环; 它并不仅限于无条件循环。

示例:

LOOP
    -- some computations
    EXIT WHEN count > 100;
    CONTINUE WHEN count < 50;
    -- some computations for count IN [50 .. 100]
END LOOP;

41.6.5.4. WHILE 条件循环 #

[ <<label>> ]
WHILE boolean-expression LOOP
    statements
END LOOP [ label ];

WHILE 语句会在 boolean-expression 求值为真的情况下重复一系列语句。 该表达式会在每次进入循环体之前被检查。

例如:

WHILE amount_owed > 0 AND gift_certificate_balance > 0 LOOP
    -- some computations here
END LOOP;

WHILE NOT done LOOP
    -- some computations here
END LOOP;

41.6.5.5. FOR 整数范围循环 #

[ <<label>> ]
FOR name IN [ REVERSE ] expression .. expression [ BY expression ] LOOP
    statements
END LOOP [ label ];

这种形式的 FOR 创建一个遍历整数范围的循环。 变量 name 被自动定义为 integer 类型,并且只存在于循环内部 (该变量名的任何现有定义在循环内都会被忽略)。 给出范围下界和上界的两个表达式在进入循环时各求值一次。 如果未指定 BY 子句,迭代步长为 1; 否则步长是 BY 子句中指定的值, 该值同样在进入循环时求值一次。 如果指定了 REVERSE, 那么每次迭代之后是减去步长值,而不是加上。

一些整数 FOR 循环的示例:

FOR i IN 1..10 LOOP
    -- i will take on the values 1,2,3,4,5,6,7,8,9,10 within the loop
END LOOP;

FOR i IN REVERSE 10..1 LOOP
    -- i will take on the values 10,9,8,7,6,5,4,3,2,1 within the loop
END LOOP;

FOR i IN REVERSE 10..1 BY 2 LOOP
    -- i will take on the values 10,8,6,4,2 within the loop
END LOOP;

如果下界大于上界(在 REVERSE 的情况下 是小于),则循环体完全不会被执行。不会引发任何错误。

如果 FOR 循环上附加了 label,则整数循环变量 可以用该 label 以限定名的形式引用。

41.6.6. 遍历查询结果集循环 #

使用另一种类型的 FOR 循环, 你可以遍历查询的结果并对数据进行相应处理。其语法是:

[ <<label>> ]
FOR target IN query LOOP
    statements
END LOOP [ label ];

target 是记录变量、行变量, 或以逗号分隔的标量变量列表。 target 会被依次赋予 query 产生的每一行, 循环体对每一行执行一次。下面是一个示例:

CREATE FUNCTION refresh_mviews() RETURNS integer AS $$
DECLARE
    mviews RECORD;
BEGIN
    RAISE NOTICE 'Refreshing all materialized views...';

    FOR mviews IN
       SELECT n.nspname AS mv_schema,
              c.relname AS mv_name,
              pg_catalog.pg_get_userbyid(c.relowner) AS owner
         FROM pg_catalog.pg_class c
    LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
        WHERE c.relkind = 'm'
     ORDER BY 1
    LOOP

        -- Now "mviews" has one record with information about the materialized view

        RAISE NOTICE 'Refreshing materialized view %.% (owner: %)...',
                     quote_ident(mviews.mv_schema),
                     quote_ident(mviews.mv_name),
                     quote_ident(mviews.owner);
        EXECUTE format('REFRESH MATERIALIZED VIEW %I.%I', mviews.mv_schema, mviews.mv_name);
    END LOOP;

    RAISE NOTICE 'Done refreshing materialized views.';
    RETURN 1;
END;
$$ LANGUAGE plpgsql;

如果循环被 EXIT 语句终止, 最后被赋值的行值在循环结束后仍然可以访问。

这种 FOR 语句中使用的 query 可以是任何向调用者返回行的 SQL 命令:SELECT 是最常见的情况, 但你也可以使用带 RETURNING 子句的 INSERT、UPDATE、 DELETE 或 MERGE。 某些实用命令(例如 EXPLAIN)也可以使用。

PL/pgSQL 变量会被替换为查询参数, 查询计划会被缓存以备重用,如 Section 41.11.1 和 Section 41.11.2 中详细讨论的那样。

FOR-IN-EXECUTE 语句是遍历行的另一种方式:

[ <<label>> ]
FOR target IN EXECUTE text_expression [ USING expression [, ... ] ] LOOP
    statements
END LOOP [ label ];

它与前一种形式相似,区别在于源查询被指定为字符串表达式, 该表达式在每次进入 FOR 循环时被求值并重新规划。 这允许程序员像使用普通 EXECUTE 语句那样, 在预规划查询的速度与动态查询的灵活性之间做出选择。 与 EXECUTE 一样, 参数值可以通过 USING 插入动态命令中。

指定要遍历其结果的查询的另一种方式是把它声明为游标。 这在 Section 41.7.4 中描述。

41.6.7. 遍历数组循环 #

FOREACH 循环与 FOR 循环非常相似, 但它遍历的不是 SQL 查询返回的行,而是数组值的元素。 (总体而言,FOREACH 旨在遍历复合值表达式的组成部分; 用于遍历数组之外的其他复合值的变体将来可能会加入。) 遍历数组的 FOREACH 语句是:

[ <<label>> ]
FOREACH target [ SLICE number ] IN ARRAY expression LOOP
    statements
END LOOP [ label ];

在没有 SLICE 的情况下,或者指定了 SLICE 0 时,循环会遍历对 expression 求值所产生的数组的 单个元素。target 变量会被依次赋予 每个元素值,循环体对每个元素执行一次。 下面是一个遍历整数数组元素的示例:

CREATE FUNCTION sum(int[]) RETURNS int8 AS $$
DECLARE
  s int8 := 0;
  x int;
BEGIN
  FOREACH x IN ARRAY $1
  LOOP
    s := s + x;
  END LOOP;
  RETURN s;
END;
$$ LANGUAGE plpgsql;

无论数组维度有多少,元素都会按存储顺序被访问。 尽管 target 通常只是单个变量, 但在遍历复合值(记录)数组时,它可以是一个变量列表。 在这种情况下,对于每个数组元素,变量会从复合值的 相应列依次赋值。

在 SLICE 值为正数时,FOREACH 遍历的是数组的切片而不是单个元素。 SLICE 值必须是不大于数组维度数的整数常量。 target 变量必须是一个数组, 它会接收数组值的一系列切片,每个切片具有 SLICE 所指定的维度数。 下面是一个遍历一维切片的示例:

CREATE FUNCTION scan_rows(int[]) RETURNS void AS $$
DECLARE
  x int[];
BEGIN
  FOREACH x SLICE 1 IN ARRAY $1
  LOOP
    RAISE NOTICE 'row = %', x;
  END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT scan_rows(ARRAY[[1,2,3],[4,5,6],[7,8,9],[10,11,12]]);

NOTICE:  row = {1,2,3}
NOTICE:  row = {4,5,6}
NOTICE:  row = {7,8,9}
NOTICE:  row = {10,11,12}

41.6.8. 捕获异常与错误处理 #

默认情况下,PL/pgSQL 函数中发生的任何错误 都会中止该函数以及外层事务的执行。你可以通过使用带 EXCEPTION 子句的 BEGIN 块 来捕获错误并从中恢复。其语法是对 BEGIN 块 常规语法的扩展:

[ <<label>> ]
[ DECLARE
    declarations ]
BEGIN
    statements
EXCEPTION
    WHEN condition [ OR condition ... ] THEN
        handler_statements
    [ WHEN condition [ OR condition ... ] THEN
          handler_statements
      ... ]
END;

如果没有发生错误,这种形式的块只是执行所有 statements,然后控制流传递到 END 之后的下一条语句。但如果 statements 中发生了错误, 对它们的进一步处理会被放弃,控制流传递到 EXCEPTION 列表。该列表会被搜索, 找到第一个与所发生错误匹配的 condition。 如果找到匹配,则执行相应的 handler_statements, 然后控制流传递到 END 之后的下一条语句。 如果没有找到匹配,错误会向外传播,就好像 EXCEPTION 子句根本不存在一样: 该错误可以被带 EXCEPTION 的外层块捕获; 如果没有这样的块,它将中止函数的处理。

condition 的名称可以是 Appendix A 中列出的任何名称。 类别名匹配该类别中的任何错误。特殊的条件名 OTHERS 匹配除 QUERY_CANCELED 和 ASSERT_FAILURE 之外的每种错误类型。(按名称捕获这两种错误类型是可能的, 但通常并不明智。)条件名不区分大小写。 此外,错误条件也可以通过 SQLSTATE 代码指定;例如下面两种写法是等价的:

WHEN division_by_zero THEN ...
WHEN SQLSTATE '22012' THEN ...

如果在被选中的 handler_statements 内部发生了新的错误,它不会被这个 EXCEPTION 子句捕获,而是向外传播。外层的 EXCEPTION 子句可以捕获它。

当错误被 EXCEPTION 子句捕获时, PL/pgSQL 函数的局部变量 保持错误发生时的状态,但块内对持久数据库状态的所有更改 都会被回滚。作为一个例子,考虑以下片段:

INSERT INTO mytab(firstname, lastname) VALUES('Tom', 'Jones');
BEGIN
    UPDATE mytab SET firstname = 'Joe' WHERE lastname = 'Jones';
    x := x + 1;
    y := x / 0;
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE 'caught division_by_zero';
        RETURN x;
END;

当控制流到达对 y 的赋值时, 将以 division_by_zero 错误失败。 该错误会被 EXCEPTION 子句捕获。 RETURN 语句返回的值将是 x 递增后的值,但 UPDATE 命令的效果已经被回滚。不过,位于块之前的 INSERT 命令不会被回滚, 因此最终的结果是数据库中包含的是 Tom Jones,而不是 Joe Jones。

Tip

包含 EXCEPTION 子句的块在进入和退出时的开销 明显高于不包含它的块。因此,请不要在不需要时使用 EXCEPTION。

Example 41.2. UPDATE 与 INSERT 中的异常处理

这个例子使用异常处理来酌情执行 UPDATE 或 INSERT。建议应用使用带 ON CONFLICT DO UPDATE 的 INSERT,而不是真正使用这种模式。 这个例子主要用于说明 PL/pgSQL 控制流结构的用法:

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- Do nothing, and loop to try the UPDATE again.
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

这段代码假定 unique_violation 错误是由该 INSERT 引起的,而不是由表上触发器函数中的 某个 INSERT 之类的操作引起的。 如果表上有多个唯一索引,它也可能行为不正确, 因为无论哪个索引引发了错误,它都会重试该操作。 使用接下来讨论的特性来检查被捕获的错误是否正是预期的那个, 可以获得更高的安全性。


41.6.8.1. 获取错误详细信息 #

异常处理程序经常需要识别所发生的具体错误。 在 PL/pgSQL 中获取当前异常信息 有两种方式:特殊变量和 GET STACKED DIAGNOSTICS 命令。

在异常处理程序内部,特殊变量 SQLSTATE 包含与被引发异常对应的错误代码 (可能的错误代码列表见 Table A.1)。 特殊变量 SQLERRM 包含与该异常关联的错误消息。 这些变量在异常处理程序之外是未定义的。

在异常处理程序内部,还可以使用 GET STACKED DIAGNOSTICS 命令检索 当前异常的信息,其形式为:

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

每个 item 是一个关键字, 标识要赋给指定 variable (其数据类型应适合接收该值)的状态值。 当前可用的状态项见 Table 41.2。

Table 41.2. 错误诊断项说明

NameTypeDescription
RETURNED_SQLSTATEtext该异常的 SQLSTATE 错误代码
COLUMN_NAMEtext与该异常相关的列的名称
CONSTRAINT_NAMEtext与该异常相关的约束的名称
PG_DATATYPE_NAMEtext与该异常相关的数据类型的名称
MESSAGE_TEXTtext该异常主消息的文本
TABLE_NAMEtext与该异常相关的表的名称
SCHEMA_NAMEtext与该异常相关的模式的名称
PG_EXCEPTION_DETAILtext该异常详细信息消息的文本(如有)
PG_EXCEPTION_HINTtext该异常提示消息的文本(如有)
PG_EXCEPTION_CONTEXTtext描述异常发生时调用栈的文本行 (见 Section 41.6.9)

如果异常没有为某个状态项设置值,将返回一个空字符串。

下面是一个示例:

DECLARE
  text_var1 text;
  text_var2 text;
  text_var3 text;
BEGIN
  -- some processing which might cause an exception
  ...
EXCEPTION WHEN OTHERS THEN
  GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT,
                          text_var2 = PG_EXCEPTION_DETAIL,
                          text_var3 = PG_EXCEPTION_HINT;
END;

41.6.9. 获取当前执行位置信息 #

GET DIAGNOSTICS 命令(在 Section 41.5.5 中已有描述) 检索当前执行状态的信息(而前面讨论的 GET STACKED DIAGNOSTICS 命令报告的是 上一次错误发生时的执行状态)。它的 PG_CONTEXT 状态项对于标识当前执行位置很有用。PG_CONTEXT 返回一个文本字符串,其中包含描述调用栈的文本行。 第一行指向当前函数和当前正在执行的 GET DIAGNOSTICS 命令。 第二行及后续行指向调用栈更上层处的调用函数。例如:

CREATE OR REPLACE FUNCTION outer_func() RETURNS integer AS $$
BEGIN
  RETURN inner_func();
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION inner_func() RETURNS integer AS $$
DECLARE
  stack text;
BEGIN
  GET DIAGNOSTICS stack = PG_CONTEXT;
  RAISE NOTICE E'--- Call Stack ---
%', stack;
  RETURN 1;
END;
$$ LANGUAGE plpgsql;

SELECT outer_func();

NOTICE:  --- Call Stack ---
PL/pgSQL function inner_func() line 5 at GET DIAGNOSTICS
PL/pgSQL function outer_func() line 3 at RETURN
CONTEXT:  PL/pgSQL function outer_func() line 3 at RETURN
 outer_func
 ------------
           1
(1 row)

GET STACKED DIAGNOSTICS ... PG_EXCEPTION_CONTEXT 返回同类的栈追踪,但描述的是检测到错误的位置, 而不是当前位置。