pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。
PL/pgSQL 是一种块结构语言。所有关键字 和标识符都可以用大小写混合的形式使用。块的定义是:
[<<label>>] [DECLAREdeclarations] BEGINstatementsEND;
块的语句部分中可以有任意数量的子块。子块可以用来把变量 隐藏在语句块的外部世界之外。
块前面的声明部分中声明的变量在每次进入该块时都会被初始化 为它们的默认值,而不是每次函数调用只初始化一次。例如:
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没有嵌套事务。
PL/pgSQL 中有两种注释。双横线--开始一个 延伸到行尾的注释。/*开始一个块注释,它 延伸到下一个*/出现的位置。块注释不能嵌套, 但双横线注释可以被包进块注释中,而双横线也可以隐藏块注释 的定界符/*和*/。
块或其子块中使用的所有变量、行和记录都必须在块的声明部分 中声明。唯一的例外是遍历一段整数值范围的 FOR 循环的循环 变量。
PL/pgSQL 变量可以有任何 SQL 数据类型,例如 INTEGER、VARCHAR和 CHAR。所有变量的默认值都是 SQL的 NULL 值。
下面是一些变量声明的例子:
user_id INTEGER; quantity NUMBER(5); url VARCHAR;
声明的语法如下:
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;
传递给函数的变量用标识符$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';
使用%TYPE和%ROWTYPE属性,你可以 声明与另一个数据库项(例如一个表字段)具有相同数据类型 或结构的变量。
%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';
使用 RENAME 你可以更改变量、记录或行的名字。这在需要 在触发器过程内部用另一个名字引用 NEW 或 OLD 时有用。
语法和例子:
RENAMEoldnameTOnewname; RENAME id TO user_id; RENAME this_var TO that_var;
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 中让这些 值被正确解释的唯一办法。
如果在表达式或语句中使用了记录字段,这些字段的数据类型 在同一表达式的各次调用之间不应改变。在编写为多张表处理 事件的触发器过程时要牢记这一点。
凡是没有被 PL/pgSQL 解析器按下面说明理解的东西,都会被放进 一个查询并发送到数据库引擎执行。这样的查询不应返回任何 数据。
把一个值赋给变量或行/记录字段写作:
identifier:=expression;
如果表达式的结果数据类型与变量的数据类型不匹配,或者变量 具有已知的尺寸/精度(如char(20)),结果值会被 PL/pgSQL 字节码解释器使用结果类型的输出函数和变量类型的 输入函数隐式转换。注意这可能导致类型的输入函数产生运行时 错误。
user_id := 20; tax := subtotal * 0.06;
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 '');
你经常会想在 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';
GET DIAGNOSTICSvariable=item[ , ... ]
这条命令允许检索系统状态指示器。每个 item都是一个关键字,标识要赋给指定 变量的状态值(该变量应当具有接收它的正确数据类型)。当前可用的 状态项有:ROW_COUNT,发送到 SQL引擎的最后一条SQL查询 所处理的行数;以及RESULT_OID,最近的 SQL查询插入的最后一行的 Oid。注意 RESULT_OID只在 INSERT 查询之后有用。
RETURN expression
函数终止,expression的值会被返回给 上层执行器。函数的返回值不能没有定义。如果控制到达函数顶层 块的末尾而没有碰到 RETURN 语句,就会发生运行时错误。
表达式的结果会被自动转换成函数的返回类型,转换方式与 赋值中描述的相同。
控制结构可能是 PL/SQL 中最有用(也最重要)的部分。借助 PL/pgSQL 的控制结构,你可以以非常灵活而强大的方式操纵 PostgreSQL数据。
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 和 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 的基础上增加了一种能力: 你可以指定在条件求值为 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;
当你使用"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;
使用 LOOP、WHILE、FOR 和 EXIT 语句,你可以迭代地控制你的 PL/pgSQL 程序的执行流程。
[<<label>>]
LOOP
statements
END LOOP;
一个必须由 EXIT 语句显式终止的无条件循环。可选的标签 可以被嵌套循环的 EXIT 语句用来指定应该终止哪一层嵌套。
EXIT [label] [ WHENexpression];
如果没有给出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 语句,只要条件表达式的求值为真,你就可以 在一系列语句上循环。
[<<label>>] WHILEexpressionLOOPstatementsEND 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;
[<<label>>] FORnameIN [ REVERSE ]expression..expressionLOOPstatementsEND 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;
记录与行类型类似,但它们没有预定义的结构。它们被用在选取和 FOR 循环中,保存 SELECT 操作产生的一个实际数据库行。
把一个完整的选取赋给记录或行可以这样完成:
SELECT INTOtargetexpressionsFROM ...;
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;
使用一种特殊类型的 FOR 循环,你可以遍历一个查询的结果并 相应地操纵这些数据。语法如下:
[<<label>>] FORrecord | rowINselect_clauseLOOPstatementsEND 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>>] FORrecord | rowIN EXECUTEtext_expressionLOOPstatementsEND LOOP;
它与前一种形式类似,区别在于源 SELECT 语句被指定为一个字符串 表达式,该表达式在每次进入 FOR 循环时被求值并重新制定计划。 这样程序员就可以像使用普通 EXECUTE 语句那样,在预计划查询的 速度和动态查询的灵活性之间做出选择。
使用 RAISE 语句把消息抛进Postgres 的 elog 机制。
RAISElevel'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;
这会中止事务并写入数据库日志。
Postgres没有一个非常聪明的异常处理 模型。每当解析器、规划器/优化器或执行器判定一条语句无法再 继续处理时,整个事务都会被中止,系统跳回主循环去从客户端 应用获取下一条查询。
可以钩进错误机制来注意到这种情况的发生。但目前无法判断究竟 是什么导致了中止(输入/输出转换错误、浮点错误、解析错误)。 而且此时数据库后端可能处于不一致状态,因此返回上层执行器或 发出更多命令可能会损坏整个数据库。即便可以,此时事务已中止 的信息已经发送给了客户端应用,恢复操作没有任何意义。
因此,当 PL/pgSQL 在函数或触发器过程执行期间遇到中止时, 它目前唯一会做的事情就是写一些额外的 DEBUG 级别日志消息, 指出这件事发生在哪个函数中以及在哪里(行号和语句类型)。
译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。