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

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

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

49.2. PL/pgSQL

PL/pgSQL 是一个可装载的 Postgres 数据库系统过程语言。

这个软件包最初由 Jan Wieck 编写。

49.2.1. 概述

PL/pgSQL 的设计目标是创建一种可装载的过程 语言,它

  • 可以用于创建函数和触发器过程,

  • 为 SQL 语言增加控制结构,

  • 可以执行复杂计算,

  • 继承所有用户定义的类型、函数和操作符,

  • 可以定义为被服务器信任,

  • 易于使用。

PL/pgSQL 调用处理器在后端第一次调用函数时解析函数的源文本并 产生一棵内部二进制指令树。产生的字节码在调用处理器中通过 函数的对象 ID 来标识。这确保了通过 DROP/CREATE 序列更改函数 无需建立新的数据库连接即可生效。

对于函数中使用的所有表达式和 SQL 语句, PL/pgSQL 字节码解释器都会使用 SPI 管理器的 SPI_prepare() 和 SPI_saveplan() 函数创建预备执行计划。这是在 PL/pgSQL 函数中 第一次处理各条语句时完成的。因此,包含许多需要执行计划的 语句的条件代码函数,只会预备和保存那些在数据库连接的整个 生命周期内真正用到的计划。

除了用户定义类型的输入/输出转换和计算函数之外, C 语言函数中能定义的任何事情都可以用 PL/pgSQL 完成。可以 创建复杂的条件计算函数,之后用它们定义操作符或在函数 索引中使用。

49.2.2. 描述

49.2.2.1. PL/pgSQL 的结构

PL/pgSQL 语言不区分大小写。所有关键字和 标识符都可以混合使用大写和小写。

PL/pgSQL 是一种面向块的语言。块的定义是

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

块的语句部分可以有任意数量的子块。子块可以用来 对语句块外部隐藏变量。块的声明 部分中声明的变量在每次进入该块时 都会被初始化为它们的默认值, 而不是每次函数调用只初始化一次。

重要的是不要误解 PL/pgSQL 中用于分组语句的 BEGIN/END 与 用于事务控制的数据库命令之间的区别。函数和触发器过程不能 启动或提交事务,而且 Postgres 没有嵌套事务。

49.2.2.2. 注释

PL/pgSQL 中有两种注释。双横线 '--' 开始一个延伸到行尾的注释。'/*' 开始一个延伸到下一个 '*/' 出现处的块注释。 块注释不能嵌套,但双横线注释可以 放在块注释内,双横线也可以隐藏 块注释定界符 '/*' 和 '*/'。

49.2.2.3. 声明

块或其子块中使用的所有变量、行和记录都必须在块的声明 区中声明,唯一例外是在整数范围上迭代的 FOR 循环的循环变量。 传给 PL/pgSQL 函数的参数会用通常的标识符 $n 自动声明。 声明使用如下语法:

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

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

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

name class%ROWTYPE;

声明一个具有给定类的结构的行。类必须是 数据库中已存在的表名或视图名。行的字段 用点表示法访问。函数的参数可以是 复合类型(完整的表行)。在这种情况下, 相应的标识符 $n 将是一个行类型,但必须 用下面描述的 ALIAS 命令为它起别名。行中只能 访问表行的用户属性,不能访问 Oid 或其他 系统属性(因此该行可以来自视图,而视图行 没有用的系统属性)。

行类型的字段继承表中 char() 等 数据类型的字段大小或精度。

name RECORD;

记录类似于行类型,但没有预定义的结构。 它们用于选择和 FOR 循环中,保存 SELECT 操作返回的 一条实际数据库行。同一个记录可以 用于不同的选择。当记录中没有实际行时访问记录或试图 给记录字段赋值都会 导致运行时错误。

触发器中的 NEW 和 OLD 行会作为记录传给过程。 这是必要的,因为在 Postgres 中 同一个触发器过程可以处理不同表的 触发器事件。

name ALIAS FOR $n;

为了让代码更易读,可以为函数的位置参数 定义别名。

对于作为参数传给函数的复合类型,这种别名是必需的。 SQL 函数中的点表示法 $1.salary 在 PL/pgSQL 中 不允许使用。

RENAME oldname TO newname;

更改变量、记录或行的名称。当触发器 过程内部需要用另一个名称引用 NEW 或 OLD 时, 这很有用。

49.2.2.4. 数据类型

变量的类型可以是数据库中 任何已存在的基本类型。上面声明区中的 type 定义为:

  • Postgres-basetype

  • variable%TYPE

  • class.field%TYPE

variable 是在同一函数中先前声明、 在此处可见的 变量的名称。

class 是已存在的表 或视图的名称,field 是其中某个 属性的名称。

使用 class.field%TYPE 会使 PL/pgSQL在后端生命周期内第一次调用函数时 查找该属性的定义。 假设有一个带 char(20) 属性的表和一些在局部变量中 处理其内容的 PL/pgSQL 函数。现在有人 认为 char(20) 不够用,转储表、删除它、 把这个属性重新定义为 char(40) 再重建表并恢复数据。哈——他忘了那些 函数。其中的计算会把值 截断为 20 个字符。但如果它们是用 class.field%TYPE 声明定义的,就会自动处理大小变化, 包括新表模式把该属性定义为 text 类型的情况。

49.2.2.5. 表达式

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

    SELECT expression
    

用 SPI 管理器求值。在表达式中,变量 标识符的出现被替换为参数,变量的 实际值通过参数数组传给执行器。PL/pgSQL 函数中使用的所有表达式都只预备和 保存一次。

Postgres 主解析器所做的类型检查对 常量值的解释有 一些副作用。具体来说, 下面两个函数的行为有所不同:

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

and

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

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

在 logfunc2() 的例子中,Postgres 主解析器不知道 'now' 应该变成什么类型,因此它返回一个包含 字符串 'now' 的 text 数据类型。在赋值给局部变量 curtime 时, PL/pgSQL 解释器会调用 text_out() 和 datetime_in() 函数把这个字符串 转换为 datetime 类型。

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

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

49.2.2.6. 语句

PL/pgSQL 解析器按下面的说明不理解的任何内容 都会被放进一个查询并发给数据库引擎 执行。这种查询不应返回任何数据。

赋值

给变量或行/记录字段赋一个值 的写法是

    identifier := expression;
    

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

把一个完整的选择结果赋给记录或行可以 用

    SELECT expressions INTO target FROM ...;
    

完成。target 可以是记录、行变量或 逗号分隔的变量和记录/行字段列表。

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

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

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

如果选择返回多行,只有第一行会被移入 目标字段。其余的都被悄悄丢弃。

调用另一个函数

Prostgres 数据库中定义的所有函数 都返回一个值。因此,调用函数的常规 方式是执行 SELECT 查询或做一次赋值(导致 一次 PL/pgSQL 内部的 SELECT)。但有时人们 并不关心函数的结果。

    PERFORM query
    

通过 SPI 管理器执行 'SELECT query' 并 丢弃结果。局部 变量等标识符仍会被替换为参数。

从函数返回
    RETURN expression
    

函数终止,expression 的值 将返回给上层执行器。函数的返回值 不能未定义。如果控制流到达函数顶层块 的末尾仍未遇到 RETURN 语句,将发生运行时 错误。

表达式的结果会按赋值一节所述 自动转换成函数的返回类型。

中止与消息

如上面的示例所示,有一条 RAISE 语句可以把 消息抛进 Postgres 的 elog 机制。

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

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

条件
    IF expression THEN
        statements
    [ELSE
        statements]
    END IF;
    

expression 必须返回一个 至少可以转换成布尔类型的值。

循环

循环有多种类型。

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

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

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

条件循环,只要 expression 的求值为真就一直执行。

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

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

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

记录或行会被依次赋予 select 子句产生的所有 行,并对每一行执行这些语句。如果循环被 EXIT 语句终止,最后赋值的行在循环 之后仍然可以访问。

    EXIT [ label ] [ WHEN expression ];
    

如果没有给出 label, 则终止最内层的循环,接下来执行 END LOOP 之后的语句。 如果给出了 label,它 必须是当前层或嵌套循环块上层的标签。 然后命名的循环或块被终止,控制流 继续到对应循环/块的 END 之后的语句。

49.2.2.7. 触发器过程

PL/pgSQL 可以用来定义触发器过程。它们像平常一样用 CREATE FUNCTION 命令创建为一个没有 参数、返回类型为 OPAQUE 的函数。

用作触发器过程的函数有一些 Postgres 特有的细节。

首先它们在顶层块的声明区中自动创建了一些 特殊变量。它们是

NEW

数据类型 RECORD;在行级触发器的 INSERT/UPDATE 操作中保存新数据库行的变量。

OLD

数据类型 RECORD;在行级触发器的 UPDATE/DELETE 操作中保存旧数据库行的变量。

TG_NAME

数据类型 name;包含实际触发的 触发器名称的变量。

TG_WHEN

数据类型 text;按触发器的 定义为 'BEFORE' 或 'AFTER' 的字符串。

TG_LEVEL

数据类型 text;按触发器的 定义为 'ROW' 或 'STATEMENT' 的字符串。

TG_OP

数据类型 text;取值为 'INSERT'、'UPDATE' 或 'DELETE' 的字符串, 指明触发器是为哪个操作实际触发的。

TG_RELID

数据类型 oid;导致 触发器调用的表的对象 ID。

TG_RELNAME

数据类型 name;导致触发器 调用的表的名称。

TG_NARGS

数据类型 integer;CREATE TRIGGER 语句中传给触发器 过程的参数个数。

TG_ARGV[]

数据类型 text 数组;来自 CREATE TRIGGER 语句的参数。 索引从 0 计数并可以用表达式给出。无效 索引(< 0 或 >= tg_nargs)得到 NULL 值。

其次它们必须返回 NULL,或者返回一个恰好具有 触发该触发器的表的结构的记录/行。 AFTER 触发器总是可以返回 NULL 值而 无任何影响。BEFORE 触发器返回 NULL 时会通知触发器 管理器跳过这一实际行的操作。 否则,返回的记录/行会替换操作中被插入/更新的 行。可以直接在 NEW 中替换单个值 并返回它,或者构造一个完整的新记录/行 返回。

49.2.2.8. 异常

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

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

因此,PL/pgSQL 目前在函数或触发器 过程执行期间遇到中止时,唯一能做的 就是写一些额外的 DEBUG 级别日志消息, 说明发生在哪个函数的什么地方(行号和 语句类型)。

49.2.3. 示例

这里只给出少量函数来演示编写 PL/pgSQL 函数有多么容易。更复杂的例子可以 查看 PL/pgSQL 的回归测试。

在 PL/pgSQL 中编写函数的一个 痛苦细节是单引号的处理。CREATE FUNCTION 时函数的 源文本必须是 字面字符串。字面字符串中的单引号必须 加倍或用反斜线引用。我们仍在寻找 优雅的替代方案。在此之前,应当像 下面的例子那样把单引号加倍。这个问题 在 Postgres 未来版本中的 任何解决方案都将是向上兼容的。

49.2.3.1. 一些简单的 PL/pgSQL 函数

下面两个 PL/pgSQL 函数与 C 语言 函数讨论中的对应函数等价。

    CREATE FUNCTION add_one (int4) RETURNS int4 AS '
        BEGIN
            RETURN $1 + 1;
        END;
    ' LANGUAGE 'plpgsql';
    
    CREATE FUNCTION concat_text (text, text) RETURNS text AS '
        BEGIN
            RETURN $1 || $2;
        END;
    ' LANGUAGE 'plpgsql';
    

49.2.3.2. 复合类型上的 PL/pgSQL 函数

这同样是 C 函数一节中示例的 PL/pgSQL 等价实现。

    CREATE FUNCTION c_overpaid (EMP, int4) RETURNS bool AS '
        DECLARE
            emprec ALIAS FOR $1;
            sallim ALIAS FOR $2;
        BEGIN
            IF emprec.salary ISNULL THEN
                RETURN ''f'';
            END IF;
            RETURN emprec.salary > sallim;
        END;
    ' LANGUAGE 'plpgsql';
    

49.2.3.3. PL/pgSQL 触发器过程

这个触发器确保每当表中插入或更新一行时, 当前的用户名和时间都会被盖印到 该行上。它还确保给出了雇员的名字,并且 工资是正值。

    CREATE TABLE emp (
        empname text,
        salary int4,
        last_date datetime,
        last_user name);

    CREATE FUNCTION emp_stamp () RETURNS OPAQUE AS
        BEGIN
            -- Check that empname and salary are given
            IF NEW.empname ISNULL THEN
                RAISE EXCEPTION ''empname cannot be NULL value'';
            END IF;
            IF NEW.salary ISNULL THEN
                RAISE EXCEPTION ''% cannot have NULL salary'', NEW.empname;
            END IF;

            -- Who works for us when she must pay for?
            IF NEW.salary < 0 THEN
                RAISE EXCEPTION ''% cannot have a negative salary'', NEW.empname;
            END IF;

            -- Remember who changed the payroll when
            NEW.last_date := ''now'';
            NEW.last_user := getpgusername();
            RETURN NEW;
        END;
    ' LANGUAGE 'plpgsql';

    CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
        FOR EACH ROW EXECUTE PROCEDURE emp_stamp();
    

提交更正

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