1 概述

1.1 案例介绍

PL/pgSQL是一种程序语言,叫做过程化SQL语言(Procedural Language/Postgres SQL),PL/pgSQL是PostgreSQL数据库对SQL语句的扩展。在普通SQL语句的使用上增加了编译语言的特点,所以PL/pgSQL就是把数据操作和查询语句组织在PL/pgSQL代码的过程性单元中,通过逻辑判断、循环等操作实现复杂的功能或者计算的程序语言。

本案例选择Gauss数据库作为示例,直观地展示 PL/pgSQL在商业应用开发与过程化编程能力和实际应用开发中为开发者带来的便利。

通过实际操作,让大家深入了解如何利用 PL/pgSQL 开发并部署一个函数功能模块。在这个过程中,大家将学习到从函数创建、数据批量读取到SQL程序编写以及与触发器集成等一系列关键步骤,从而掌握 PL/pgSQL 的基本使用方法,体验其在应用开发中的优势。

1.2 适用对象

  • 企业
  • 个人开发者
  • 高校学生

1.3 案例时间

本案例总时长预计60分钟。

1.4 案例流程

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库

说明:

  1. 领取开发者空间云数据库GaussDB生态版;
  2. 在空间开发桌面终端进入GaussDB的DAS工具;
  3. 进行数据库的图形化SQL界面,进行GaussDB之PL/pgSQL的操作;

1.5 资源总览

资源名称

规格

单价(元)

时长(分钟)

开发者空间-云数据库GaussDB

鲲鹏通用计算增强型 kc1 | 2vCPUs | 4G | HCE2.0

免费

60

最新案例动态,请查阅《基于开发者空间GaussDB云数据库的PLpgSQL实践二》。小伙伴快来领取华为开发者空间体验实操吧!

2 OpenGauss数据库PL/pgSQL实践

2.1 领取开发者空间GaussDB数据库

免费领取GaussDB在线试用版(2025年 06月 21日 - 2025年 12月 31日)。地址如下: https://developer.huaweicloud.com/signup/75dae31d0eb04cdcab822c76d35eb9a1 有1000个名额,数量有限,速度为王。

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_02

领取后,按提示即可创建数据库,如下:

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_03

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_04

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_05

登录数据库

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_06

输入创建数据库时的密码,点击测试连接,通过后点击登录

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_07

创建测试数据库,点击确定

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_08

查看数据库兼容类型

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_09

执行sql:

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_10

3 PL/PGSQL实践

PLPGSQL是数据库的编程语言。相当于在数据库中用SQL语言进行逻辑处理与代码开发。可以把业务系统中封装的功能模块下沉到数据库端实现,以达到减轻业务系统的逻辑压力、降低架构复杂度和简化业务系统实现难度。

PLPGSQL是一种块结构型语言,例如匿名块,存储过程和函数体的完整文本必须是块。块定义如下:

[ <<标签>> ]
[ 声明
变量;
变量 := 赋值 ]
BEGINSQL QUERY
END [ 标签];

PLPGSQL定义的功能模块(存储过程和函数)可以互相嵌套。例如SQL块中嵌套子SQL块,存储过程引用PLPGSQL定义的其他函数和模块功能。

3.1 游标之数据批量处理

游标用于对 SQL 查询的结果集做批处理读取场景。当SQL查询的结果集数据量过大,一次性读取会导致内存不够缓存。所以出现了游标的功能,用COUSOR对大数据量的结果集中分批处理(例如一次取1000条数据),直到循环多次,批量把结果集里的数据读取完毕。

3.1.1 声明游标变量

PL/pgSQL语法中,所有游标都必须通过游标变量去访问,游标变量是特殊的数据类型 refcursor。游标变量的声明语法如下:

name [ [ NO ] SCROLL ] CURSOR [ ( arguments ) ] FOR query;

当关键字 SCROLL 被指定,则游标可以向后滚动处理数据,如果指定了NO SCROLL,则游标向后读取数据将被拒绝。如果没有指定 SCROLL,游标是否自动向后读取数据取决了SQL查询。arguments 如果被指定,则其以逗号分隔以对应 query 查询数据类型的列表,arguments的字段名称将替换掉 query查询中对应的字段名。示例如下:

DECLAREcurs1 refcursor;curs2 CURSOR FOR SELECT * FROM tenk1;curs3 CURSOR (key integer) FOR SELECT * FROM tenk1 WHERE unique1 = key;

这3个游标变量都属于 refcursor 数据类型,但curs1能被用于任何Query,curs2已经被绑定了一个完整的query查询,最后的curs3游标绑定了一个参数化的Query(当curs3被打开时,key用一个整形数值替代)。curs1由于没有绑定于任何指定的Query,所以可称之为末绑定的游标变量curs1。

当游标Query使用FOR UPDATE / SHARE(互斥锁 / 共享锁)时,则不能指定关键字SCROLL。对于Query涉及volatile函数时,则推荐使用NO SCROLLSCROLL的实现场景是游标对Query输出结果集多次读取以保证读取结果的一致性(多次读取的数据内容保持不变),而Query里的volatile函数无法保证这一点。

3.1.2 打开游标

语法1:

OPEN unbound_cursorvar [ [ NO ] SCROLL ] FOR query;

该语法操作是打开游标去执行游标绑定的Query查询。游标必须已经声明的refcursor变量,Query查询必须是SELECT,或返回元组的其他内容(例如EXPLAIN)。

示例如下:

OPEN curs1 FOR SELECT * FROM foo WHERE key = mykey;

语法2:

OPEN unbound_cursorvar [ [ NO ] SCROLL ] FOR EXECUTE query_string 
[ USING expression [, ... ] ];

该游标变量被打开并指定的Query查询去执行。

OPEN curs1 FOR EXECUTE format('SELECT * FROM %I WHERE col1 = $1',tabname) USING keyvalue;

此用例中,通过Query中函数format()写入表名,col1的值是通过USING插入的,因此这不需要引用。

语法3:

OPEN bound_cursorvar [ ( [ argument_name := ] argument_value [, ...] ) ];

这种方法是游标在声明时,已经绑定了动态SQL,而在打开游标时,需要对该游标适配具体的参数值,使其在动态SQL中被替换掉。这种方法下,SQL计划一直保存在缓存中,直到游标执行结束。这种方法不能指定关键字SCROLLNO SCROLL,因为游标的滚动行为已经确定。该语法3与语法2的EXECUTE关键字并不等价。

游标的参数值用两种方法传递:位置和命名。用参数位置表示时,所有参数都是按顺序指定的。在参数命名法中,使用 := 指定每个参数的名称,将其与参数表达式分开。

如下示例:

OPEN curs2;
OPEN curs3(42);
OPEN curs3(key := 42);

下面示例中的curs4,效果与上面curs3一样。

DECLAREkey integer;curs4 CURSOR FOR SELECT * FROM tenk1 WHERE unique1 = key;
BEGINkey := 42;OPEN curs4;
3.1.3 使用游标

通过上面语法打开游标后,用 FETCH 语句来操作它。当PL/pgSQL函数要批量返回数据时,可以让函数返回refcursor值,调用函数对游标进行操作。refcursor变量值只能引用打开的游标,直到事务结束时被隐匿的关闭。

3.1.3.1 FETCH

语法如下:

FETCH [ direction { FROM | IN } ] cursor INTO target;

FETCH从游标中检索下一行(在指示的方向上)到目标中,该目标可能是行变量、记录变量或逗号分隔的简单变量列表,就像SELECT INTO一样。如果没有合适的行,则将目标设置为NULL(s)。与SELECT INTO一样,可以检查特殊变量,以查看是否获取了行。如果没有获得行,则光标定位在最后一行之后或第一行之前,具体取决于移动方向。

方向子句可以是SQL FETCH命令中允许的任何变体,但可以读取多行的变体除外;例如,它可以是NEXTPRIORFirstLastABSOLUTE countRELATION countForwardbackward。省略方向与指定NEXT相同。在使用计数的表单中,计数可以是任何整数值表达式(与SQL FETCH命令不同,SQL FETCH命令只允许使用整数常量)。需要向后移动的方向值很可能会失败,除非游标是用SCROLL选项声明或打开的。

游标必须是引用打开的游标入口的refcursor变量的名称。示例如下:

FETCH curs1 INTO rowvar;
FETCH curs2 INTO foo, bar, baz;
FETCH LAST FROM curs3 INTO x, y;
FETCH RELATIVE -2 FROM curs4 INTO x;
3.1.3.2 MOVE

语法如下:

MOVE [ direction { FROM | IN } ] cursor;

关键字MOVE在不检索任何数据的情况下重新定位游标。MOVE的原理与FETCH类似,其不同在于MOVE只重新定位光标,而不返回移动到的行。而SELECT INTO 通过指定变量FOUND能够检测其移动的位置是否在数据集中,避免发生错误。如果没有指定的drection,则游标会移到数据集最后一行的下一行位置,或者第一行的前面,其取决于指定的drection。

MOVE curs1;
MOVE LAST FROM curs3;
MOVE RELATIVE -2 FROM curs4;
MOVE FORWARD 2 FROM curs4;
3.1.3.3 UPDATE/DELETE WHERE CURRENT OF

语法如下:

UPDATE table SET ... WHERE CURRENT OF cursor;
DELETE FROM table WHERE CURRENT OF cursor;

当游标位于表的某一元组时,则使用该语法,游标指定的元组可以被修改更新或者删除元组。如果要限制游标查询,则应该使用FOR UPDATE

UPDATE foo SET dataval = myval WHERE CURRENT OF curs1;
3.1.3.4 关闭游标

语法如下:

CLOSE cursor;

对打开的游标执行关闭操作,可以理解为游标资源的释放。但该操作应该在事务结束之前,对游标进行释放。如果在事务结束时没有关闭游标,则结束事务时也会自动对游标进行关闭操作。

示例如下:

CLOSE curs1;
3.1.4 通过游标批量处理结果集

PL/pgSQL函数返回一个游标时,在处理大数据集时,返回多行或者多列比较高效。

下面示例中,游标名称被调用者指定的用法:

DROP TABLE IF EXISTS test CASCADE;
CREATE TABLE test (col text);
INSERT INTO test VALUES ('123');CREATE FUNCTION reffunc(refcursor) RETURNS refcursor AS '
BEGINOPEN $1 FOR SELECT col FROM test;RETURN $1;
END;
' LANGUAGE plpgsql;

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_11

BEGIN;
SELECT reffunc('funccursor');
FETCH ALL IN funccursor;
COMMIT;

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_12

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_13

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_14

游标名称自动生成的用例:

-- 此处自行创建用例表test
DROP TABLE IF EXISTS test;
CREATE TABLE test(col int);
CREATE OR REPLACE FUNCTION reffunc2() RETURNS refcursor AS $$
DECLAREref_temp refcursor;
BEGINOPEN ref_temp FOR SELECT col FROM test;RETURN ref_temp;
END;
$$ LANGUAGE plpgsql;

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_15

-- need to be in a transaction to use cursors.
BEGIN;
SELECT reffunc2();
FETCH ALL IN "<unnamed portal 3>";
COMMIT;

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_16

FETCH ALL IN 后面的内容根据实际内容而改变。

下面用例展示,从函数中返回多个游标的用法:(注:根据Function实际,创建前需要建表table_1和table_2

-- 此处简单创建用例表table_1, table_2
create table table_1(id int);
create table table_2(id int); 
CREATE FUNCTION myfunc(refcursor, refcursor) RETURNS SETOF refcursor AS $$
BEGINOPEN $1 FOR SELECT * FROM table_1;RETURN NEXT $1;OPEN $2 FOR SELECT * FROM table_2;RETURN NEXT $2;
END;
$$ LANGUAGE plpgsql;

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_17

-- need to be in a transaction to use cursors.
BEGIN;SELECT * FROM myfunc('a', 'b');FETCH ALL FROM a;
FETCH ALL FROM b;
COMMIT;

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_18

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_19

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_20

3.1.4.1 使用LOOP循环体从游标中获取结果集

使用FOR关键字的LOOP循环体,用迭代变量获取游标返回的元组数据,循环处理游标中的数据结果。语法如下:

[ <<label>> ]
FOR recordvar IN bound_cursorvar [ ( [ argument_name := ] argument_value [, ...] ) ] LOOPstatements
END LOOP [ label ];

这里使用的游标必须已经绑定到Query,并且不能是已经被打开的游标变量。FOR语句会自动打开游标,并在循环退出时自动关闭游标。当FORLOOP使用带参数的游标时,则必须使用表达式的实际参数值。其方式与OPEN方式相同。

3.2 自定义函数实现

PL/pgSQL中自定义函数非常常见。通常用CREATE FUNCTIONA表示该操作。函数必须要有返回值RETURNS。其它与存储过程内容一致。但当函数没有要返回的结果时,可以只写

RETURN;表示返回NULL。

自定义函数其他章节已经有过多介绍和示例,此处不再过多说明。

示例:(函数get_score_point根据输入的vscore成绩分数返回对应的绩点)

CREATE or REPLACE FUNCTION get_score_point(vscore INT)
RETURNS DECIMAL
as $$
DECLARE
BEGINRETURN (CASEWHEN vscore <= 59 THEN0WHEN vscore <= 69 THEN0.1WHEN vscore <= 79 THEN0.2WHEN vscore <= 89 THEN0.3ELSE0.4END);
END; $$ LANGUAGE PLPGSQL;

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_21

示例:(函数add_mask根据SQL查询对部分字段脱敏,调用dbe_output.print_line输出元组数据)

CREATE OR REPLACE FUNCTION add_mask(id1 TEXT, id2 TEXT) RETURNS TEXT AS $$
DECLAREvar1 TEXT;var2 int;cursor c1 is select case when id::text = id2 then '***' else id::text end as id, ranking from (select id, dense_rank() over(order by sum_point desc) as ranking from 
(select id,get_score_point(math)+get_score_point(art)+get_score_point(phy) as sum_point from stu)) where id::text = id1;
BEGINopen c1;loop fetch c1 into var1, var2;exit when c1%notfound;return dbe_output.print_line('学号是:'||var1||', 排名是:'||var2);end loop;close c1;
END;
$$ LANGUAGE PLPGSQL;

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_22

该用例中使用了dbe_output.print_line函数包,是GaussDB的兼容Oracle的商业特性,因为dbe_output高级包属于Oracle兼容性功能。

3.3 触发器实现

TRIGGER

当数据库中表数据被改变或者发生数据库事件(events)时,可定义触发器函数,触发对应的操作。触发器函数与自定义函数类似,不同的是触发器函数返回的是触发器return trigger。

3.3.1 数据变更触发器

一般触发器函数是没有参数且返回触发器类型,但是触发器函数有自己的默认参数。其默认参数有12个,这里主要介绍常用的两个:NEW和OLD,其都是record数据类型。NEW参数主要用于行级操作的INSERT/UPDATE触发器。OLD参数主要用于行级操作的UPDATE/DELETE触发器。

下面示例,当表的一个元组插入或更新时,当前用户名和时间则会被写入元组last_user和last_date字段。并检查employee表的name字段是否被给定和salary的数据是否正确。

CREATE TABLE emp (empname           text,salary            integer,last_date         timestamp,last_user         text
);CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$BEGIN-- Check that empname and salary are givenIF NEW.empname IS NULL THENRAISE EXCEPTION 'empname cannot be null';END IF;IF NEW.salary IS NULL THENRAISE EXCEPTION '% cannot have null salary', NEW.empname;END IF;-- Who works for us when they must pay for it?IF NEW.salary < 0 THENRAISE EXCEPTION '% cannot have a negative salary', NEW.empname;END IF;-- Remember who changed the payroll whenNEW.last_date := current_timestamp;NEW.last_user := current_user;RETURN NEW;END;
$emp_stamp$ LANGUAGE plpgsql;CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON empFOR EACH ROW EXECUTE PROCEDURE emp_stamp();

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_23

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_24

下面示例表示,当表emp元组的任何insert,update或者delete时,其记录都将被写到emp_audit表中。当时的时间和用户名也一并被写入。

CREATE TABLE emp (empname           text NOT NULL,salary            integer
);CREATE TABLE emp_audit(operation         char(1)   NOT NULL,stamp             timestamp NOT NULL,userid            text      NOT NULL,empname           text      NOT NULL,salary            integer
);CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$BEGIN---- Create a row in emp_audit to reflect the operation performed on emp,-- making use of the special variable TG_OP to work out the operation.--IF (TG_OP = 'DELETE') THENINSERT INTO emp_audit SELECT 'D', now(), current_user, OLD.*;ELSIF (TG_OP = 'UPDATE') THENINSERT INTO emp_audit SELECT 'U', now(), current_user, NEW.*;ELSIF (TG_OP = 'INSERT') THENINSERT INTO emp_audit SELECT 'I', now(), current_user, NEW.*;END IF;RETURN NULL; -- result is ignored since this is an AFTER triggerEND;
$emp_audit$ LANGUAGE plpgsql;CREATE TRIGGER emp_audit
AFTER INSERT OR UPDATE OR DELETE ON empFOR EACH ROW EXECUTE PROCEDURE process_emp_audit();

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_25

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_26

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_27

示例:

CREATE TABLE emp (empname           text PRIMARY KEY,salary            integer
);CREATE TABLE emp_audit(operation         char(1)   NOT NULL,userid            text      NOT NULL,empname           text      NOT NULL,salary            integer,stamp             timestamp NOT NULL
);CREATE VIEW emp_view ASSELECT e.empname,e.salary,max(ea.stamp) AS last_updatedFROM emp eLEFT JOIN emp_audit ea ON ea.empname = e.empnameGROUP BY 1, 2;CREATE OR REPLACE FUNCTION update_emp_view() RETURNS TRIGGER AS $$BEGIN---- Perform the required operation on emp, and create a row in emp_audit-- to reflect the change made to emp.--IF (TG_OP = 'DELETE') THENDELETE FROM emp WHERE empname = OLD.empname;IF NOT FOUND THEN RETURN NULL; END IF;OLD.last_updated = now();INSERT INTO emp_audit VALUES('D', current_user, OLD.*);RETURN OLD;ELSIF (TG_OP = 'UPDATE') THENUPDATE emp SET salary = NEW.salary WHERE empname = OLD.empname;IF NOT FOUND THEN RETURN NULL; END IF;NEW.last_updated = now();INSERT INTO emp_audit VALUES('U', current_user, NEW.*);RETURN NEW;ELSIF (TG_OP = 'INSERT') THENINSERT INTO emp VALUES(NEW.empname, NEW.salary);NEW.last_updated = now();INSERT INTO emp_audit VALUES('I', current_user, NEW.*);RETURN NEW;END IF;END;
$$ LANGUAGE plpgsql;CREATE TRIGGER emp_audit
INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_viewFOR EACH ROW EXECUTE PROCEDURE update_emp_view();

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_28

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_29

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_30

示例:

--
-- Main tables - time dimension and sales fact.
--
CREATE TABLE time_dimension (time_key                    integer NOT NULL,day_of_week                 integer NOT NULL,day_of_month                integer NOT NULL,month                       integer NOT NULL,quarter                     integer NOT NULL,year                        integer NOT NULL
);
CREATE UNIQUE INDEX time_dimension_key ON time_dimension(time_key);CREATE TABLE sales_fact (time_key                    integer NOT NULL,product_key                 integer NOT NULL,store_key                   integer NOT NULL,amount_sold                 numeric(12,2) NOT NULL,units_sold                  integer NOT NULL,amount_cost                 numeric(12,2) NOT NULL
);
CREATE INDEX sales_fact_time ON sales_fact(time_key);--
-- Summary table - sales by time.
--
CREATE TABLE sales_summary_bytime (time_key                    integer NOT NULL,amount_sold                 numeric(15,2) NOT NULL,units_sold                  numeric(12) NOT NULL,amount_cost                 numeric(15,2) NOT NULL
);
CREATE UNIQUE INDEX sales_summary_bytime_key ON sales_summary_bytime(time_key);--
-- Function and trigger to amend summarized column(s) on UPDATE, INSERT, DELETE.
--
CREATE OR REPLACE FUNCTION maint_sales_summary_bytime() RETURNS TRIGGER
AS $maint_sales_summary_bytime$DECLAREdelta_time_key          integer;delta_amount_sold       numeric(15,2);delta_units_sold        numeric(12);delta_amount_cost       numeric(15,2);BEGIN-- Work out the increment/decrement amount(s).IF (TG_OP = 'DELETE') THENdelta_time_key = OLD.time_key;delta_amount_sold = -1 * OLD.amount_sold;delta_units_sold = -1 * OLD.units_sold;delta_amount_cost = -1 * OLD.amount_cost;ELSIF (TG_OP = 'UPDATE') THEN-- forbid updates that change the time_key --- (probably not too onerous, as DELETE + INSERT is how most-- changes will be made).IF ( OLD.time_key != NEW.time_key) THENRAISE EXCEPTION 'Update of time_key : % -> % not allowed',OLD.time_key, NEW.time_key;END IF;delta_time_key = OLD.time_key;delta_amount_sold = NEW.amount_sold - OLD.amount_sold;delta_units_sold = NEW.units_sold - OLD.units_sold;delta_amount_cost = NEW.amount_cost - OLD.amount_cost;ELSIF (TG_OP = 'INSERT') THENdelta_time_key = NEW.time_key;delta_amount_sold = NEW.amount_sold;delta_units_sold = NEW.units_sold;delta_amount_cost = NEW.amount_cost;END IF;-- Insert or update the summary row with the new values.<<insert_update>>LOOPUPDATE sales_summary_bytimeSET amount_sold = amount_sold + delta_amount_sold,units_sold = units_sold + delta_units_sold,amount_cost = amount_cost + delta_amount_costWHERE time_key = delta_time_key;EXIT insert_update WHEN found;BEGININSERT INTO sales_summary_bytime (time_key,amount_sold,units_sold,amount_cost)VALUES (delta_time_key,delta_amount_sold,delta_units_sold,delta_amount_cost);EXIT insert_update;EXCEPTIONWHEN UNIQUE_VIOLATION THEN-- do nothingEND;END LOOP insert_update;RETURN NULL;END;
$maint_sales_summary_bytime$ LANGUAGE plpgsql;CREATE TRIGGER maint_sales_summary_bytime
AFTER INSERT OR UPDATE OR DELETE ON sales_factFOR EACH ROW EXECUTE PROCEDURE maint_sales_summary_bytime();
INSERT INTO sales_fact VALUES(1,1,1,10,3,15);
INSERT INTO sales_fact VALUES(1,2,1,20,5,35);
INSERT INTO sales_fact VALUES(2,2,1,40,15,135);
INSERT INTO sales_fact VALUES(2,3,1,10,1,13);
SELECT * FROM sales_summary_bytime;
DELETE FROM sales_fact WHERE product_key = 1;
SELECT * FROM sales_summary_bytime;
UPDATE sales_fact SET units_sold = units_sold * 2;
SELECT * FROM sales_summary_bytime;

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_31

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_32

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_33

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_34

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_35

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_36

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_37

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_38

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_39

示例:(下面示例中new_table和old_table需要替换)

CREATE TABLE emp (empname           text NOT NULL,salary            integer
);CREATE TABLE emp_audit(operation         char(1)   NOT NULL,stamp             timestamp NOT NULL,userid            text      NOT NULL,empname           text      NOT NULL,salary            integer
);CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$BEGIN---- Create rows in emp_audit to reflect the operations performed on emp,-- making use of the special variable TG_OP to work out the operation.--IF (TG_OP = 'DELETE') THENINSERT INTO emp_auditSELECT 'D', now(), current_user, o.* FROM old_table o;ELSIF (TG_OP = 'UPDATE') THENINSERT INTO emp_auditSELECT 'U', now(), current_user, n.* FROM new_table n;ELSIF (TG_OP = 'INSERT') THENINSERT INTO emp_auditSELECT 'I', now(), current_user, n.* FROM new_table n;END IF;RETURN NULL; -- result is ignored since this is an AFTER triggerEND;
$emp_audit$ LANGUAGE plpgsql;CREATE TRIGGER emp_audit_insAFTER INSERT ON empREFERENCING NEW TABLE AS new_tableFOR EACH STATEMENT EXECUTE PROCEDURE process_emp_audit();
CREATE TRIGGER emp_audit_updAFTER UPDATE ON empREFERENCING OLD TABLE AS old_table NEW TABLE AS new_tableFOR EACH STATEMENT EXECUTE PROCEDURE process_emp_audit();
CREATE TRIGGER emp_audit_delAFTER DELETE ON empREFERENCING OLD TABLE AS old_table
FOR EACH STATEMENT EXECUTE PROCEDURE process_emp_audit();
3.3.2 事件触发器

事件触发器即当有event发生时,触发对应的操作,该类触发器函数的默认参数有两个:TG_EVENT text和TG_TAG text。示例如下:

注: 事件触发器在OpenGauss上支持,GaussDB集中式并不支持事件触发器!

CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
BEGINRAISE NOTICE 'snitch: % %', tg_event, tg_tag;
END;
$$ LANGUAGE plpgsql;CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE PROCEDURE snitch();

下面是OpenGauss的示例。

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_40

3.4 打印输出说明

3.4.1 存储过程打印输出结果集

存储过程没有Return变量,因此存储过程结束时也没有Return语句。此场景下,如果要提前结束其运行过程,则使用没有表达式的Return语句。如果该存储过程有Output输出参数,则其会返回最终结果。

PL/pgSQL的函数,存储过程,Do匿名块都可以内部调用存储过程。Output参数与Call调用的处理方式不同。存储过程的每个Out 或者 InOut 参数必须对应Call语句中的一个变量,存储过程返回的任何值都将由输出参数返回结果。

CREATE PROCEDURE triple(INOUT x int)
LANGUAGE plpgsql
AS $$
BEGINx := x * 3;
END;
$$;DO $$
DECLARE myvar int := 5;
BEGINCALL triple(myvar);RAISE NOTICE 'myvar = %', myvar;  -- prints 15
END;
$$;

存储过程中输出参数对应的变量可以是简单数值,也可以是复合数值,但目前Output 参数不能是数组元素。

注意 raise 多用于调试 :

RAISE LEVEL ... LEVEL 有6种错误级别: debug, log, info, notice, warning, exception(默认)

RETURN

return 字段 \|\| ',' \|\| 字段;

语法如下:

RAISE [ level ] 'format' [, expression [, ... ]] [ USING option = expression [, ... ] ];
RAISE [ level ] condition_name [ USING option = expression [, ... ] ];
RAISE [ level ] SQLSTATE 'sqlstate' [ USING option = expression [, ... ] ];
RAISE [ level ] USING option = expression [, ... ];
RAISE ;

例如:

RAISE NOTICE 'Calling cs_create_job(%)', v_job_id;

变量v_job_id将会替换掉%。

RAISE unique_violation USING MESSAGE = 'Duplicate user ID: ' || user_id;

示例:

-- 编写add_mask(id1,id2)函数,当id1是当前查询用户时,显示正常ID,如果不是则显示为-- id2create or replace function add_mask(id1 text, id2 text)returns text as $$declarevar1 text;beginselect current_user into var1;if var1 = id1 thenreturn 'current user is ' || id1;elsereturn 'current user is ' || id2;end if;end;$$ language plpgsql;

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_41

3.4.2 函数输出结果集
3.4.2.1 RETURN语句

自定义函数可以声明返回任意数据集,可通过RETURN NEXT,RETURN QUERY,RETURNS TABLE,RETURNS SETOF。

语法如下:

RETURN expression;

函数用RETURN语句终止运行并返回结果给调用者。这种形式用于不返回结果集合的PL/pgSQL函数。

在返回标量类型的函数中,表达式的结果将自动转换为函数的返回类型。但要返回复合(行)值,则必须编写一个表达式精确地返回所请求的列集,该方法需要显式构造。

如果函数声明了参数输出值,那么函数 return 后不跟表达式,将返回当前值给输出参数变量。如果将函数声明为返回 void 类型,则可以使用 return 语句提前退出函数,return后面不能写表达式。

函数的返回值不能为undefined。如果PL/pgSQL控制块执行到函数的顶层块的末尾而未命中return语句,则将发生运行报错。但是,此限制不适用于带有输出参数的函数和返回void的函数。在这些情况下,如果顶级块完成,则自动执行return语句。

例如:

-- functions returning a scalar type
RETURN 1 + 2;
RETURN scalar_var;-- functions returning a composite type
RETURN composite_type_var;
RETURN (1, 2, 'three'::text);  -- must cast columns to correct types
3.4.2.2 RETURN NEXT和RETURN QUERY

语法如下:

RETURN NEXT expression;
RETURN QUERY query;
RETURN QUERY EXECUTE command-string [ USING expression [, ... ] ];

当PL/pgSQL函数被声明返回SETOF sometype时,则存储过程的实现略有不同。

在该场景下,RETURN NEXT 或 RETURN QUERY 返回的一系列结果集,用 RETURN 不带参数的语句来声明函数执行结束。RETURN NEXT 既可以表示标题数据类型,也可以表示复合数据类型。而对于复合数据类型,RETURN NEXT 将返回结果整个表的结果集。RETURN QUERY 是将QUERY的执行结果集返回给函数结果集。在函数的单个返回结果集中,RETURN NEXT 和 RETURN QUERY 可以混合使用,该场景下函数的结果集将以串联方式展现。而一个无参数的 RETURN语句则会控制函数的执行结束以达到控制函数执行流程的结果。

RETURN QUERY 有一个 RETURN QUERY EXECUTE 的变量,它用于指向SQL的动态执行结果。参数表达式可通过 WITH 插入到 QERUY 字符串里,其方法与 EXECUTE 方法相同。

如果声明函数时没给Output参数,则在使用Reutrn Next时不应该跟表达式。当有多个Output参数时,函数则应该声明返回SETOF。如下面RETURN NEXT的用例:

CREATE TABLE foo (fooid INT, foosubid INT, fooname TEXT);
INSERT INTO foo VALUES (1, 2, 'three');
INSERT INTO foo VALUES (4, 5, 'six');CREATE OR REPLACE FUNCTION get_all_foo() RETURNS SETOF foo AS
$BODY$
DECLAREr foo%rowtype;
BEGINFOR r INSELECT * FROM foo WHERE fooid > 0LOOP-- can do some processing hereRETURN NEXT r; -- return current row of SELECTEND LOOP;RETURN;
END;
$BODY$
LANGUAGE plpgsql;SELECT * FROM get_all_foo();

基于开发者空间GaussDB云数据库的PLpgSQL实践二_GaussDB_42

基于开发者空间GaussDB云数据库的PLpgSQL实践二_数据库_43

RETURN QUERY用例:(需要提前创建flight表,字段至少包含flightid和flightdate)

CREATE FUNCTION get_available_flightid(date) RETURNS SETOF integer AS
$BODY$
BEGINRETURN QUERY SELECT flightidFROM flightWHERE flightdate >= $1AND flightdate < ($1 + 1);-- Since execution is not finished, we can check whether rows were returned-- and raise exception if not.IF NOT FOUND THENRAISE EXCEPTION 'No flight at %.', $1;END IF;RETURN;END;
$BODY$
LANGUAGE plpgsql;-- Returns available flights or raises exception if there are no
-- available flights.
SELECT * FROM get_available_flightid(CURRENT_DATE);

3.5 PL/PGSQL之事务管理

调用存储过程使用CALL关键字,如同调用匿名块使用DO一样。在PL/pgSQL语法中,可以使用COMMIT和ROLLBACK结束事务。使用COMMIT或者ROLLBACK结束事务后,另一个新事务会自动启动。所以没有单独的START TRANSACTION。这些与PL/pgSQL中的BEGIN和END是不同的。

示例如下:(需要创建表test1(a int),其中a字段至少有奇数和偶数值)

CREATE PROCEDURE transaction_test1()
LANGUAGE plpgsql
AS $$
BEGINFOR i IN 0..9 LOOPINSERT INTO test1 (a) VALUES (i);IF i % 2 = 0 THENCOMMIT;ELSEROLLBACK;END IF;END LOOP;
END;
$$;CALL transaction_test1();

事务的控制仅受顶层的CALL或DO调用,或者嵌套的CALL或DO调用中可用。如果调用栈为CALL proc1() -\> CALL proc2() -\> CALL proc3(),那么第二个和第三个过程可以执行事务控制动作。但如果调用栈是CALL proc1() -\> SELECT func2() -\> CALL proc3(),由于中间有SELECT,那么最后一个过程不能做事务控制。

PL/pgSQL不支持保存点(保存点/回滚到保存点/释放保存点命令)。

在Cursor LOOP中有些特殊注意点:参数如下示例:

CREATE PROCEDURE transaction_test2()
LANGUAGE plpgsql
AS $$
DECLAREr RECORD;
BEGINFOR r IN SELECT * FROM test2 ORDER BY x LOOPINSERT INTO test1 (a) VALUES (r.x);COMMIT;END LOOP;
END;
$$;CALL transaction_test2();

在非只读的游标循环中不允许使用事务操作(例如UPDATE...Returning)。

3.6 常用SQL操作

3.6.1 union all/ union

该语法用于合并两个SELECT查询结果集。union会对两个查询结果集去除重复的数据。而union all不会对结果集去重。

select * from score1 order by chinese limit 10
union 
select * from score2 order by chinese limit 10;select * from score1 order by chinese limit 10
union all
select * from score2 order by chinese limit 10;
3.6.2 dense() rank()

用于对结果集排序。

dense_rank() over([partition by column] order by column desc)
  • DENSE_RANK函数为各组内值生成连续排序序号,其中,相同的值具有相同序号
CREATE TABLE dense_rank_t1(a int, b int);
INSERT INTO dense_rank_t1 VALUES(1,1),(1,1),(1, 2),(1, 3),(2, 4),(2, 5),(3,6);
SELECT a,b,DENSE_RANK() OVER(PARTITION BY a ORDER BY b) FROM dense_rank_t1;

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_44

基于开发者空间GaussDB云数据库的PLpgSQL实践二_华为开发者空间_45

3.6.3 not in/ not exist

查看两个班级相同的科目, 202201班在score2中不存在的成绩, 要求使用not in(考试时详细确认题目要求, 查看是具体哪些科目成绩)

select chinese from score where chinese not in (select chinese from score2);
3.6.4 merge into

将目标表和源表中的数据针对关联条件进行匹配, 匹配时对目标表进行update更新操作, 不匹配时对目标表进行insert写入操作

MERGE INTO products p USING newproducts np ON (p.product_id = np.product_id)   WHEN MATCHED THEN  UPDATE SET p.product_name = np.product_name, p.category = np.category WHERE p.product_name != 'play gym'  WHEN NOT MATCHED THEN  INSERT VALUES (np.product_id, np.product_name, np.category) WHERE np.category = 'books';
3.6.5 首字母大写函数initcap
-- family_name跟first_name用'.'拼接,要求首字母大写
select id, initcap(family_name||'.'||first_name), sexmark, grade from su;
3.6.6 系统视图的权限查询
select t1.*,rolname from (select datname,(aclexplode(datacl)).grantee, (aclexplode(datacl)).privilege_type from pg_database) t1,pg_roles where grantee=pg_roles.oid and rolname='sjh111' and datname not like '%template%';
-- 查看用户user1和数据库的相关权限,题目提示用pg_database和pg_roles,要求显示数据库名、用户名、数据库的权限(一定要背下来,原题,而且不要去格式美化)
SELECT a.datname, b.rolname, string_agg(a.priv_t, ',')from (SELECT datname, (aclexplode(COALESCE(datacl, acldefault('d' :: "char", datdba)))).grantee as grantee, (aclexplode(COALESCE(datacl, acldefault('d' :: "char", datdba)))).privilege_type as priv_tFROM "pg_database"WHERE datname not like '%template%' ) a,"pg_roles" bWHERE (a.grantee = 0 or a.grantee = b.oid)AND b.rolname = 'user1'GROUP BY a.datname, b.rolname;
3.6.7 插入一条数据,当主键冲突时将mark改为'F'

语法如下:

INSERT INTO xxx ON DUPLICATE KEY UPDATE expression

示例:

insert into su values(2,'tom','jerry','tom','H',63) on duplicate key update mark='F'

至此,PL/PGSQL的学习告一段落。