24.2. 描述 #
24.2.1. PL/pgSQL 的结构
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没有嵌套事务。
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),你也不需要修改你的函数定义。-
nametable%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 时有用。
语法和例子:
RENAMEoldnameTOnewname; 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 DIAGNOSTICSvariable=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
statementsEND LOOP;一个必须由 EXIT 语句显式终止的无条件循环。可选的标签可以被嵌套循环的 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
使用 WHILE 语句,只要条件表达式的求值为真,你就可以在一系列语句上循环。
[<<label>>] WHILE
expressionLOOPstatementsEND 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
nameIN [ 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;
24.2.7. 使用 RECORD #
记录与行类型类似,但它们没有预定义的结构。它们被用在选取和 FOR 循环中,保存 SELECT 操作产生的一个实际数据库行。
24.2.7.2. 赋值 #
把一个完整的选取赋给记录或行可以这样完成:
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;
24.2.7.3. 在记录上迭代 #
使用一种特殊类型的 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 语句那样,在预计划查询的速度和动态查询的灵活性之间做出选择。
24.2.8. 中止与消息 #
使用 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;
这会中止事务并写入数据库日志。
24.2.9. 异常
Postgres没有一个非常聪明的异常处理模型。每当解析器、规划器/优化器或执行器判定一条语句无法再继续处理时,整个事务都会被中止,系统跳回主循环去从客户端应用获取下一条查询。
可以钩进错误机制来注意到这种情况的发生。但目前无法判断究竟是什么导致了中止(输入/输出转换错误、浮点错误、解析错误)。而且此时数据库后端可能处于不一致状态,因此返回上层执行器或发出更多命令可能会损坏整个数据库。即便可以,此时事务已中止的信息已经发送给了客户端应用,恢复操作没有任何意义。
因此,当 PL/pgSQL 在函数或触发器过程执行期间遇到中止时,它目前唯一会做的事情就是写一些额外的 DEBUG 级别日志消息,指出这件事发生在哪个函数中以及在哪里(行号和语句类型)。