↑↓ 选择 ↵ 打开 ⌫ 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

不受支持的版本: 7.1
历史版本PostgreSQL 7.1 已于 2006 年 4 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本手册首页。

24.2. 描述 #

24.2.1. PL/pgSQL 的结构

PL/pgSQL 是一种块结构语言。所有关键字 和标识符都可以用大小写混合的形式使用。块的定义是:

[<<label>>]
[DECLARE
    declarations]
BEGIN
    statements
END;

块的语句部分中可以有任意数量的子块。子块可以用来把变量 隐藏在语句块的外部世界之外。

块前面的声明部分中声明的变量在每次进入该块时都会被初始化 为它们的默认值,而不是每次函数调用只初始化一次。例如:

CREATE FUNCTION somefunc() RETURNS INTEGER AS '
DECLARE
   quantity INTEGER := 30;
BEGIN
   RAISE NOTICE ''Quantity here is %'',quantity;  -- Quantity here is 30
   quantity := 50;
   --
   -- Create a sub-block
   --
   DECLARE
      quantity INTEGER := 80;
   BEGIN
      RAISE NOTICE ''Quantity here is %'',quantity;  -- Quantity here is 80
   END;

   RAISE NOTICE ''Quantity here is %'',quantity;  -- Quantity here is 50
END;
' LANGUAGE 'plpgsql';

重要的一点是不要把 PL/pgSQL 中用于给语句分组的 BEGIN/END 与用于事务控制的数据库命令混淆。PL/pgSQL 的 BEGIN/END 只用于 分组;它们不开始也不结束事务。函数和触发器过程总是在由 外层查询建立的事务中执行——它们不能开始或提交事务,因为 Postgres没有嵌套事务。

24.2.2. 注释

PL/pgSQL 中有两种注释。双横线--开始一个 延伸到行尾的注释。/*开始一个块注释,它 延伸到下一个*/出现的位置。块注释不能嵌套, 但双横线注释可以被包进块注释中,而双横线也可以隐藏块注释 的定界符/*和*/。

24.2.3. 变量和常量

块或其子块中使用的所有变量、行和记录都必须在块的声明部分 中声明。唯一的例外是遍历一段整数值范围的 FOR 循环的循环 变量。

PL/pgSQL 变量可以有任何 SQL 数据类型,例如 INTEGER、VARCHAR和 CHAR。所有变量的默认值都是 SQL的 NULL 值。

下面是一些变量声明的例子:

user_id INTEGER;
quantity NUMBER(5);
url VARCHAR;

24.2.3.1. 带默认值的常量和变量 #

声明的语法如下:

name [ CONSTANT ] type [ NOT NULL ] [ { DEFAULT | := } value ];

声明为 CONSTANT 的变量的值不能被更改。如果指定了 NOT NULL, 赋予 NULL 值会导致一个运行时错误。由于所有变量的默认值都是 SQL的 NULL 值,所有声明为 NOT NULL 的变量 还必须指定一个默认值。

默认值在每次函数被调用时都会被求值。因此把 'now'赋给一个timestamp类型的 变量会让该变量拥有实际这次函数调用的时间,而不是函数被 预编译为字节码时的时间。

例子:

quantity INTEGER := 32;
url varchar := ''http://mysite.com'';
user_id CONSTANT INTEGER := 10;

24.2.3.2. 传递给函数的变量 #

传递给函数的变量用标识符$1、 $2等命名(最多 16 个)。一些例子:

CREATE FUNCTION sales_tax(REAL) RETURNS REAL AS '
DECLARE
    subtotal ALIAS FOR $1;
BEGIN
    return subtotal * 0.06;
END;
' LANGUAGE 'plpgsql';


CREATE FUNCTION instr(VARCHAR,INTEGER) RETURNS INTEGER AS '
DECLARE
    v_string ALIAS FOR $1;
    index ALIAS FOR $2;
BEGIN
    -- Some computations here
END;
' LANGUAGE 'plpgsql';

24.2.3.3. 属性 #

使用%TYPE和%ROWTYPE属性,你可以 声明与另一个数据库项(例如一个表字段)具有相同数据类型 或结构的变量。

%TYPE

%TYPE提供变量或数据库列的数据类型。你可以 用它来声明将保存数据库值的变量。例如,假设你的 users表中有一个名为user_id的列。 要声明一个与 users 数据类型相同的变量,可以这样写:

user_id users.user_id%TYPE;

使用%TYPE你不需要知道你引用的结构的 数据类型,而且最重要的是,如果被引用项的数据类型将来 发生改变(例如你把 user_id 的表定义改为 REAL),你也不 需要修改你的函数定义。

name table%ROWTYPE;

声明一个具有给定表结构的行。 table必须是数据库中已存在的表或 视图的名字。行的字段用点号记法访问。函数的参数可以是复合 类型(完整的表行)。这种情况下,对应的标识符 $n 将是一个 行类型,但它必须用上面描述的 ALIAS 命令声明别名。

行中只能访问表行的用户属性,不能访问 OID 或其他系统 属性(因为该行可能来自一个视图)。行类型的字段继承表中 该字段的尺寸或精度(对于char()等数据类型)。

DECLARE
    users_rec users%ROWTYPE;
  user_id users%TYPE;
BEGIN
    user_id := users_rec.user_id;
    ...

create function cs_refresh_one_mv(integer) returns integer as '
   DECLARE
        key ALIAS FOR $1;
        table_data cs_materialized_views%ROWTYPE;
   BEGIN
        SELECT INTO table_data * FROM cs_materialized_views
               WHERE sort_key=key;

        IF NOT FOUND THEN
           RAISE EXCEPTION ''View '' || key || '' not found'';
           RETURN 0;
        END IF;

        -- The mv_name column of cs_materialized_views stores view
        -- names.
 
        TRUNCATE TABLE table_data.mv_name;
        INSERT INTO table_data.mv_name || '' '' || table_data.mv_query;

        return 1;
end;
' LANGUAGE 'plpgsql';

24.2.3.4.  RENAME #

使用 RENAME 你可以更改变量、记录或行的名字。这在需要 在触发器过程内部用另一个名字引用 NEW 或 OLD 时有用。

语法和例子:

RENAME oldname TO newname;

RENAME id TO user_id;
RENAME this_var TO that_var;

24.2.4. 表达式

PL/pgSQL 语句中使用的所有表达式都由后端的执行器处理。 表面上包含常量的表达式实际上可能需要运行时求值(例如 'now' 对于 timestamp 类型),因此 PL/pgSQL 解析器不可能识别除 NULL 关键字之外的 真正常量值。所有表达式都在内部通过执行查询

SELECT expression

并使用SPI管理器来求值。在表达式中,变量 标识符的出现会被参数替代,而变量的实际值则通过参数数组 传递给执行器。PL/pgSQL 函数中使用的所有表达式都只准备和 保存一次。这条规则唯一的例外是 EXECUTE 语句,它的查询在 每次遇到时都需要重新解析。

Postgres主解析器所做的类型检查对 常量值的解释有一些副作用。详细说来,下面这两个函数的行为 是有差别的:

CREATE FUNCTION logfunc1 (text) RETURNS timestamp AS '
    DECLARE
        logtxt ALIAS FOR $1;
    BEGIN
        INSERT INTO logtable VALUES (logtxt, ''now'');
        RETURN ''now'';
    END;
' LANGUAGE 'plpgsql';

和

CREATE FUNCTION logfunc2 (text) RETURNS timestamp AS '
    DECLARE
        logtxt ALIAS FOR $1;
        curtime timestamp;
    BEGIN
        curtime := ''now'';
        INSERT INTO logtable VALUES (logtxt, curtime);
        RETURN curtime;
    END;
' LANGUAGE 'plpgsql';

对于logfunc1(),Postgres 主解析器在为 INSERT 准备计划时就知道字符串 'now'应当被解释为timestamp, 因为 logtable 的目标字段就是该类型。于是它会在此刻把它 变成一个常量,而这个常量值随后在后端的整个生命期内 logfunc1()的所有调用中都被使用。不用说, 这并不是程序员想要的结果。

对于logfunc2(),Postgres 主解析器不知道'now'应当变成什么类型,因此 它返回一个包含字符串'now'的text 数据类型。在向局部变量 curtime 赋值期间,PL/pgSQL 解释器通过 调用text_out()和timestamp_in() 函数把这个字符串转换成 timestamp 类型。

Postgres主解析器所做的这种类型检查 是在 PL/pgSQL 基本完成之后才实现的。这是 6.3 与 6.4 之间的 一个差异,影响所有使用SPI管理器预备计划 特性的函数。以上述方式使用局部变量是目前 PL/pgSQL 中让这些 值被正确解释的唯一办法。

如果在表达式或语句中使用了记录字段,这些字段的数据类型 在同一表达式的各次调用之间不应改变。在编写为多张表处理 事件的触发器过程时要牢记这一点。

24.2.5. 语句

凡是没有被 PL/pgSQL 解析器按下面说明理解的东西,都会被放进 一个查询并发送到数据库引擎执行。这样的查询不应返回任何 数据。

24.2.5.1. 赋值 #

把一个值赋给变量或行/记录字段写作:

identifier := expression;

如果表达式的结果数据类型与变量的数据类型不匹配,或者变量 具有已知的尺寸/精度(如char(20)),结果值会被 PL/pgSQL 字节码解释器使用结果类型的输出函数和变量类型的 输入函数隐式转换。注意这可能导致类型的输入函数产生运行时 错误。

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

24.2.5.2. 调用另一个函数 #

Postgres数据库中定义的所有函数都 返回一个值。因此,调用函数的通常方式是执行一个 SELECT 查询 或做一次赋值(产生一个 PL/pgSQL 内部的 SELECT)。

但有时人们并不关心函数的结果。这些情况下,使用 PERFORM 语句。

PERFORM query

它通过SPI 管理器执行一个 SELECT query并丢弃 结果。像局部变量这样的标识符仍然会被替换为参数。

PERFORM create_mv(''cs_session_page_requests_mv'',''
     select   session_id, page_id, count(*) as n_hits,
              sum(dwell_time) as dwell_time, count(dwell_time) as dwell_count
     from     cs_fact_table
     group by session_id, page_id '');

24.2.5.3. 执行动态查询 #

你经常会想在 PL/pgSQL 函数内部生成动态查询,或者你有会生成 其他函数的函数。PL/pgSQL 为这些场合提供了 EXECUTE 语句。

EXECUTE query-string

其中query-string是一个 text类型的字符串,包含要执行的 query。

在使用动态查询时,你必须面对 PL/pgSQL 中单引号的转义问题。 请参阅"从 Oracle PL/SQL 移植"一章中的表格,那里有能为你 节省一些力气的详细解释。

与 PL/pgSQL 中的所有其他查询不同,由 EXECUTE 语句运行的 query不会在服务器的生命期内只准备 和保存一次。相反,query在语句每次 运行时都会被重新准备。 query-string可以在过程内动态创建, 以便对可变的表和字段执行操作。

SELECT 查询的结果会被 EXECUTE 丢弃,而且 EXECUTE 中目前不 支持 SELECT INTO。因此,从动态创建的 SELECT 中提取结果的唯一 办法是使用稍后描述的 FOR ... EXECUTE 形式。

一个例子:

EXECUTE ''UPDATE tbl SET ''
        || quote_ident(fieldname)
        || '' = ''
        || quote_literal(newvalue)
        || '' WHERE ...'';

这个例子展示了quote_ident(TEXT) 和quote_literal(TEXT)函数的用法。 包含字段和表标识符的变量应当传给 quote_ident()函数。包含动态查询字符串的 字面量成分的变量应当传给quote_literal()。 这两个函数都会采取适当的步骤,返回被单引号或双引号括起、 且内嵌特殊字符被正确处理的输入文本。

下面是一个大得多的动态查询和 EXECUTE 的例子:

CREATE FUNCTION cs_update_referrer_type_proc() RETURNS INTEGER AS '
DECLARE
    referrer_keys RECORD;  -- Declare a generic record to be used in a FOR
    a_output varchar(4000);
BEGIN 
    a_output := ''CREATE FUNCTION cs_find_referrer_type(varchar,varchar,varchar) 
                  RETURNS varchar AS '''' 
                     DECLARE 
                         v_host ALIAS FOR $1; 
                         v_domain ALIAS FOR $2; 
                         v_url ALIAS FOR $3; ''; 

    -- 
    -- Notice how we scan through the results of a query in a FOR loop
    -- using the FOR <record> construct.
    --

    FOR referrer_keys IN select * from cs_referrer_keys order by try_order LOOP
        a_output := a_output || '' if v_'' || referrer_keys.kind || '' like '''''''''' 
                 || referrer_keys.key_string || '''''''''' then return '''''' 
                 || referrer_keys.referrer_type || ''''''; end if;''; 
    END LOOP; 
  
    a_output := a_output || '' return null; end; '''' language ''''plpgsql'''';''; 
 
    -- This works because we are not substituting any variables
    -- Otherwise it would fail. Look at PERFORM for another way to run functions
    
    EXECUTE a_output; 
end; 
' LANGUAGE 'plpgsql';

24.2.5.4. 获取其他结果状态 #

GET DIAGNOSTICS variable = item [ , ... ]

这条命令允许检索系统状态指示器。每个 item都是一个关键字,标识要赋给指定 变量的状态值(该变量应当具有接收它的正确数据类型)。当前可用的 状态项有:ROW_COUNT,发送到 SQL引擎的最后一条SQL查询 所处理的行数;以及RESULT_OID,最近的 SQL查询插入的最后一行的 Oid。注意 RESULT_OID只在 INSERT 查询之后有用。

24.2.5.5. 从函数返回 #

RETURN expression

函数终止,expression的值会被返回给 上层执行器。函数的返回值不能没有定义。如果控制到达函数顶层 块的末尾而没有碰到 RETURN 语句,就会发生运行时错误。

表达式的结果会被自动转换成函数的返回类型,转换方式与 赋值中描述的相同。

24.2.6. 控制结构 #

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

24.2.6.1. 条件控制:IF 语句 #

IF语句让你可以根据某些条件采取行动。 PL/pgSQL 有三种 IF 形式:IF-THEN、IF-THEN-ELSE、 IF-THEN-ELSE IF。注意:所有 PL/pgSQL 的 IF 语句都需要一个 对应的END IF语句。在 ELSE-IF 语句中 你需要两个:第一个 IF 一个,第二个(ELSE IF)一个。

IF-THEN

IF-THEN 语句是 IF 的最简单形式。如果条件为真,THEN 和 END IF 之间的语句会被执行。否则,执行 END IF 之后的 语句。

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

IF-THEN-ELSE 语句在 IF-THEN 的基础上增加了一种能力: 你可以指定在条件求值为 FALSE 时应当执行的语句。

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;

IF 语句可以嵌套,见下面的例子:

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

当你使用"ELSE IF"语句时,你实际上是把一个 IF 语句嵌套 在 ELSE 语句中。因此每个嵌套的 IF 都需要一个 END IF 语句,外层的 IF-ELSE 也需要一个。

例如:

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

24.2.6.2. 迭代控制:LOOP、WHILE、FOR 和 EXIT #

使用 LOOP、WHILE、FOR 和 EXIT 语句,你可以迭代地控制你的 PL/pgSQL 程序的执行流程。

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

一个必须由 EXIT 语句显式终止的无条件循环。可选的标签 可以被嵌套循环的 EXIT 语句用来指定应该终止哪一层嵌套。

EXIT
EXIT [ label ] [ WHEN expression ];

如果没有给出label,最内层的 循环会被终止,接下来执行 END LOOP 之后的语句。如果给出了 label,它必须是当前或嵌套循环块 某个上层的标签。此时被命名的循环或块被终止,控制转移到 该循环/块对应的 END 之后的语句。

例子:

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

LOOP
    -- some computations
    EXIT WHEN count > 0;
END LOOP;

BEGIN
    -- some computations
    IF stocks > 100000 THEN
        EXIT;  -- illegal. Can't use EXIT outside of a LOOP
    END IF;
END;
WHILE

使用 WHILE 语句,只要条件表达式的求值为真,你就可以 在一系列语句上循环。

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

例如:

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

WHILE NOT boolean_expression LOOP
    -- some computations here
END LOOP;
FOR
[<<label>>]
FOR name IN [ REVERSE ] expression .. expression LOOP
    statements
END LOOP;

一个遍历一段整数值范围的循环。变量 name被自动创建为 integer 类型并且 只存在于循环内部。给出范围下界和上界的两个表达式只在进入 循环时被求值。迭代步长总是 1。

FOR 循环的一些例子(关于在 FOR 循环中遍历记录,参见 第 24.2.7 节):

FOR i IN 1..10 LOOP
  -- some expressions here

    RAISE NOTICE 'i is %',i;
END LOOP;

FOR i IN REVERSE 1..10 LOOP
    -- some expressions here
END LOOP;

24.2.7. 使用 RECORD #

记录与行类型类似,但它们没有预定义的结构。它们被用在选取和 FOR 循环中,保存 SELECT 操作产生的一个实际数据库行。

24.2.7.1. 声明 #

一个 RECORD 类型的变量可以用于不同的选取。当记录中没有 实际行时访问记录或试图给记录字段赋值都会导致运行时错误。 它们可以这样声明:

name RECORD;

24.2.7.2. 赋值 #

把一个完整的选取赋给记录或行可以这样完成:

SELECT  INTO target expressions FROM ...;

target可以是记录、行变量,或者 由变量和记录/行字段组成的逗号分隔列表。注意这与 Postgres 对 SELECT INTO 的通常解释——INTO 的目标是一张新创建的 表——完全不同。(如果你想在 PL/pgSQL 函数内部从 SELECT 结果创建表,使用等价语法CREATE TABLE AS SELECT。)

如果用一行或一个变量列表作为目标,所选中的值必须与目标的 结构完全匹配,否则会发生运行时错误。FROM 关键字后面可以跟 SELECT 语句允许的任何有效的限定条件、分组、排序等。

一旦一个记录或行被赋给了 RECORD 变量,你就可以用"."(点号) 记法访问该记录中的字段:

DECLARE
    users_rec RECORD;
    full_name varchar;
BEGIN
    SELECT INTO users_rec * FROM users WHERE user_id=3;

  full_name := users_rec.first_name || '' '' || users_rec.last_name;

有一个名为 FOUND 的boolean类型特殊变量,可以 在 SELECT INTO 之后立即使用它来检查赋值是否成功。

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

你也可以用 IS NULL(或 ISNULL)条件来测试 RECORD/ROW 是否 为 NULL。如果选取返回多行,只有第一行会被移入目标字段。 其余的行都会被无声地丢弃。

DECLARE
    users_rec RECORD;
    full_name varchar;
BEGIN
    SELECT INTO users_rec * FROM users WHERE user_id=3;

    IF users_rec.homepage IS NULL THEN
        -- user entered no homepage, return "http://"

        return ''http://'';
    END IF;
END;

24.2.7.3. 在记录上迭代 #

使用一种特殊类型的 FOR 循环,你可以遍历一个查询的结果并 相应地操纵这些数据。语法如下:

[<<label>>]
FOR record | row IN select_clause LOOP
    statements
END LOOP;

记录或行会被赋以 select 子句产生的所有行,并且循环体对 每一行都执行。下面是一个例子:

create function cs_refresh_mviews () returns integer as '
DECLARE
     mviews RECORD;

     -- Instead, if you did:
     -- mviews  cs_materialized_views%ROWTYPE;
     -- this record would ONLY be usable for the cs_materialized_views table

BEGIN
     PERFORM cs_log(''Refreshing materialized views...'');

     FOR mviews IN SELECT * FROM cs_materialized_views ORDER BY sort_key LOOP

         -- Now "mviews" has one record from cs_materialized_views

         PERFORM cs_log(''Refreshing materialized view '' || mview.mv_name || ''...'');
         TRUNCATE TABLE mview.mv_name;
         INSERT INTO mview.mv_name || '' '' || mview.mv_query;
     END LOOP;

     PERFORM cs_log(''Done refreshing materialized views.'');
     return 1;
end;
' language 'plpgsql';

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

FOR-IN EXECUTE 语句是在记录上迭代的另一种方式:

[<<label>>]
FOR record | row IN EXECUTE text_expression LOOP 
    statements
END LOOP;

它与前一种形式类似,区别在于源 SELECT 语句被指定为一个字符串 表达式,该表达式在每次进入 FOR 循环时被求值并重新制定计划。 这样程序员就可以像使用普通 EXECUTE 语句那样,在预计划查询的 速度和动态查询的灵活性之间做出选择。

24.2.8. 中止与消息 #

使用 RAISE 语句把消息抛进Postgres 的 elog 机制。

RAISE level 'format' [, identifier [...]];

在格式中,%用作后续逗号分隔标识符的占位符。 可用的级别有 DEBUG(在生产运行的数据库中被无声地抑制)、 NOTICE(写进数据库日志并转发给客户端应用)和 EXCEPTION (写进数据库日志并中止事务)。

RAISE NOTICE ''Id number '' || key || '' not found!'';
RAISE NOTICE ''Calling cs_create_job(%)'',v_job_id;

在最后这个例子中,v_job_id 会替换字符串中的 %。

RAISE EXCEPTION ''Inexistent ID --> %'',user_id;

这会中止事务并写入数据库日志。

24.2.9. 异常

Postgres没有一个非常聪明的异常处理 模型。每当解析器、规划器/优化器或执行器判定一条语句无法再 继续处理时,整个事务都会被中止,系统跳回主循环去从客户端 应用获取下一条查询。

可以钩进错误机制来注意到这种情况的发生。但目前无法判断究竟 是什么导致了中止(输入/输出转换错误、浮点错误、解析错误)。 而且此时数据库后端可能处于不一致状态,因此返回上层执行器或 发出更多命令可能会损坏整个数据库。即便可以,此时事务已中止 的信息已经发送给了客户端应用,恢复操作没有任何意义。

因此,当 PL/pgSQL 在函数或触发器过程执行期间遇到中止时, 它目前唯一会做的事情就是写一些额外的 DEBUG 级别日志消息, 指出这件事发生在哪个函数中以及在哪里(行号和语句类型)。

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。