pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
PL/pgSQL 语言不区分大小写。所有关键字和标识符都可以用 大小写混合的形式使用。
PL/pgSQL 是一种面向块的语言。块的定义是
[<<label>>]
[DECLARE
declarations]
BEGIN
statements
END;
块的语句部分中可以有任意数量的子块。子块可以用来把变量 隐藏在语句块的外部世界之外。块前面的声明部分中声明的变量 在每次进入该块时都会被初始化为它们的默认值,而不是每次 函数调用只初始化一次。
重要的一点是不要误解 PL/pgSQL 中用于给语句分组的 BEGIN/END 与用于事务控制的数据库命令的含义。函数和触发器过程不能开始 或提交事务,而且Postgres没有嵌套 事务。
PL/pgSQL 中有两种注释。双横线'--'开始一个延伸到行尾的注释。 '/*'开始一个块注释,它延伸到下一个'*/'出现的位置。块注释不能 嵌套,但双横线注释可以被包进块注释中,而双横线也可以隐藏 块注释的定界符'/*'和'*/'。
块或其子块中使用的所有变量、行和记录都必须在块的声明部分中 声明,唯一的例外是遍历一段整数值范围的 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 中是不允许的。
oldname TO newname;更改变量、记录或行的名字。这在需要在触发器过程内部用另一个 名字引用 NEW 或 OLD 时有用。
变量的类型可以是数据库中任何已有的基本类型。上面声明部分 中的type定义为:
Postgres基本类型
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 类型也没问题。
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';
和
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 中让这些值被正确解释的唯一 办法。
如果在表达式或语句中使用了记录字段,这些字段的数据类型在 同一表达式的各次调用之间不应改变。在编写为多张表处理事件的 触发器过程时要牢记这一点。
凡是没有被 PL/pgSQL 解析器按下面说明理解的东西,都会被放进 一个查询并发送到数据库引擎执行。这样的查询不应返回任何数据。
把一个值赋给变量或行/记录字段写作
identifier := expression;
如果表达式的结果数据类型与变量的数据类型不匹配,或者变量 具有已知的尺寸/精度(如 char(20)),结果值会被 PL/pgSQL 字节码 解释器使用结果类型的输出函数和变量类型的输入函数隐式转换。 注意这可能导致类型的输入函数产生运行时错误。
把一个完整的选取赋给记录或行可以这样完成
SELECT INTOtargetexpressionsFROM ...;
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 机制。
RAISElevelformat'' [,identifier[...]];
在格式中,"%"用作后续逗号分隔标识符的占位符。 可用的级别有 DEBUG(在生产运行的数据库中被无声地抑制)、 NOTICE(写进数据库日志并转发给客户端应用)和 EXCEPTION (写进数据库日志并中止事务)。
IFexpressionTHENstatements[ELSEstatements] END IF;
expression必须返回一个至少可以转换 为布尔类型的值。
有多种类型的循环。
[<<label>>]
LOOP
statements
END LOOP;
一个必须由 EXIT 语句显式终止的无条件循环。可选的标签可以被 嵌套循环的 EXIT 语句用来指定应该终止哪一层嵌套。
[<<label>>] WHILEexpressionLOOPstatementsEND LOOP;
一个条件循环,只要expression的求值 为真它就一直执行。
[<<label>>] FORnameIN [ REVERSE ]expression..expressionLOOPstatementsEND LOOP;
一个遍历一段整数值范围的循环。变量 name被自动创建为 integer 类型并且 只存在于循环内部。给出范围下界和上界的两个表达式只在进入循环 时被求值。迭代步长总是 1。
[<<label>>] FORrecord | rowINselect_clauseLOOPstatementsEND LOOP;
记录或行会被赋以 select 子句产生的所有行,并且语句对每一行 都执行。如果循环被 EXIT 语句终止,最后赋值的行在循环之后仍然 可以访问。
EXIT [label] [ WHENexpression];
如果没有给出label,最内层的循环会被 终止,接下来执行 END LOOP 之后的语句。如果给出了 label,它必须是当前或嵌套循环块某个 上层的标签。此时被命名的循环或块被终止,控制转移到该循环/块 对应的 END 之后的语句。
PL/pgSQL 可以用来定义触发器过程。它们用通常的 CREATE FUNCTION 命令创建,是一个没有参数、返回类型为 OPAQUE 的函数。
用作触发器过程的函数有一些Postgres 特有的细节。
首先它们的顶层块声明部分中会自动创建一些特殊变量。它们是
数据类型 RECORD;在行级触发器中保存 INSERT/UPDATE 操作的新 数据库行的变量。
数据类型 RECORD;在行级触发器中保存 UPDATE/DELETE 操作的旧 数据库行的变量。
数据类型 name;包含实际被触发的触发器名字的变量。
数据类型 text;依据触发器定义而是'BEFORE'或'AFTER'之一的 字符串。
数据类型 text;依据触发器定义而是'ROW'或'STATEMENT'之一的 字符串。
数据类型 text;'INSERT'、'UPDATE'或'DELETE'之一的字符串,指出 触发器实际是为哪个操作而触发。
数据类型 oid;导致触发器调用的表的对象 ID。
数据类型 name;导致触发器调用的表的名字。
数据类型 integer;CREATE TRIGGER 语句中给触发器过程的参数 个数。
text 类型的数组;来自 CREATE TRIGGER 语句的参数。索引从 0 开始计数并且可以用表达式给出。无效的索引(< 0 或 >= tg_nargs)得到 NULL 值。
其次它们必须返回 NULL,或者返回一个恰好包含触发该触发器的 表的结构的记录/行。AFTER 触发器总是返回 NULL 值也没有影响。 BEFORE 触发器返回 NULL 时会通知触发器管理器跳过对这一实际 行的操作。否则,返回的记录/行会在操作中替换被插入/更新的行。 可以直接在 NEW 中替换单个值然后返回它,也可以构造一个完整的 新记录/行来返回。
Postgres没有一个非常聪明的异常处理 模型。每当解析器、规划器/优化器或执行器判定一条语句无法再 继续处理时,整个事务都会被中止,系统跳回主循环去从客户端 应用获取下一条查询。
可以钩进错误机制来注意到这种情况的发生。但目前无法判断究竟 是什么导致了中止(输入/输出转换错误、浮点错误、解析错误)。 而且此时数据库后端可能处于不一致状态,因此返回上层执行器或 发出更多命令可能会损坏整个数据库。即便可以,此时事务已中止 的信息已经发送给了客户端应用,恢复操作没有任何意义。
因此,当 PL/pgSQL 在函数或触发器过程执行期间遇到中止时, 它目前唯一会做的事情就是写一些额外的 DEBUG 级别日志消息, 指出这件事发生在哪个函数中以及在哪里(行号和语句类型)。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。