• 作者:老汪软件技巧
  • 发表时间:2024-09-25 07:00
  • 浏览量:

本文是《PostgreSQL技术问答》系列文章中的一篇。关于这个系列的由来,可以参阅开篇文章:

《PostgreSQL技术问答00 - Why Postgres》

文章的编号只是一个标识,在系列中没有明确的逻辑顺序和意义。读者进行阅读时,不用太关注这个方面。

本文主要讨论的内容是在Postgres中,另一个比较核心和重要的功能: Function - 函数。

由于相关内容比较多,完整的内容被分为上下两个部分,本文是相关内容的下半部分。这部分一般是一些辅助性和扩展性的内容。比如函数和定义的查询,对数据记录进行遍历,错误处理,事务的支持等等。

如何查看当前数据库中的函数和定义

在创建了函数和对象之后,我们经常需要对其存在和定义进行查询和确认。在标准SQL会话环境中,可以使用pg_proc系统视图,来查看函数和过程,包括其类型和定义。如果在psql环境中,这个操作更加简单,使用\df命令即可。下面是几个简单的操作示例:

defaultdb=> \df
 List of functions
 Schema |           Name           |                Result data type                |                   Argument data types                   | Type 
--------+--------------------------+------------------------------------------------+---------------------------------------------------------+------
 public | akeys                    | text[]                                         | hstore                                                  | func
 public | avals                    | text[]                                         | hstore                                                  | func
 public | bm_transform             | real                                           |                                                         | func
...
-- 查看特定函数
defaultdb=> \df  x_rangeint
                              List of functions
 Schema |    Name    | Result data type |     Argument data types      | Type 
--------+------------+------------------+------------------------------+------
 public | x_rangeint | integer          | istart integer, iend integer | func
 
-- 系统表查询
defaultdb=>  select proname,prokind,prosrc from pg_proc where proname = 'insert_adata';
   proname    | prokind |                         prosrc                          
--------------+---------+---------------------------------------------------------
 insert_adata | p       |                                                        +
              |         |   INSERT INTO adata(ivalues) VALUES (Array[aa+1,bb+2]);+
              |         |   INSERT INTO adata(ivalues) VALUES (Array[bb+1,aa+2]);+
              |         | 
(1 row)

函数的参数支持默认值吗?

笔者在撰写本文时,才发现有这么一个有趣的特性,就是Postgres的函数的输入参数,是支持使用默认值的,这无疑给其应用带来了很大的方便。下面是一些简单的示例:

CREATE FUNCTION foo(a int, b int DEFAULT 2, c int DEFAULT 3)
RETURNS int
LANGUAGE SQL
AS $$
    SELECT $1 + $2 + $3;
$$;
defaultdb=> select s3(10,20,30), s3(10,20), s3(10);
 s3 | s3 | s3 
----+----+----
 60 | 33 | 15
(1 row)
defaultdb=> 
defaultdb=> select s3();
ERROR:  function s3() does not exist
LINE 1: select s3();
               ^
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.

使用的方式,也很简单,就是在参数声明的类型后面使用DEFAULT关键字和默认值,在应用的时候基本上和JS的可选参数一样,就是从右向左匹配。如果参数不可选,但又没有提供,就会出现未找到函数定义的错误。

如何对数据查询结果进行遍历

在本文的上半部分中,已经讨论了程序的循环控制结构。但与普通的编程系统不同,在关系数据库系统中,循环控制的应用,更多是基于一个数据集合,而非简单的数值累加。就是这个循环,是基于一个数据库记录集和结果集的。下面是这一类的应用方式:


-- 结果集遍历
[ <<label>> ]
FOR target IN query LOOP
    statements
END LOOP [ label ];
-- 示例,遍历当前实体化视图对象
    FOR mviews IN
       SELECT n.nspname AS mv_schema,
              c.relname AS mv_name,
              pg_catalog.pg_get_userbyid(c.relowner) AS owner
         FROM pg_catalog.pg_class c
    LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
        WHERE c.relkind = 'm'
     ORDER BY 1
    LOOP
        -- Now "mviews" has one record with information about the materialized view
        RAISE NOTICE 'Refreshing materialized view %.% (owner: %)...',
                     quote_ident(mviews.mv_schema),
                     quote_ident(mviews.mv_name),
                     quote_ident(mviews.owner);
        EXECUTE format('REFRESH MATERIALIZED VIEW %I.%I', mviews.mv_schema, mviews.mv_name);
    END LOOP;

可以看到,在这种情况下,遍历对象可以是一个查询结果集,或者数组。遍历时,系统可以从中取出当前的对象来进行处理。

如何对数组进行遍历

这里还要补充一种遍历循环的场景,就是Postgres也支持对数组类型的数据,来进行遍历变量,遍历的当前数组,来使用数组的成员。这个数组,可以是任意类型的数组表达式。


-- 基本结构
[ <<label>> ]
FOREACH target [ SLICE number ] IN ARRAY expression LOOP
    statements
END LOOP [ label ];
-- 示例
DO  $$
DECLARE 
	inumber int;
BEGIN
	FOREACH inumber IN ARRAY ARRAY[2,3,4,5] 
	LOOP
		RAISE NOTICE 'Number: %' , inumber; 
	END LOOP;
END;
$$ LANGUAGE plpgsql;

这里有一些要点:

如何输出状态和日志信息呢?

如果使用Nodejs,我们通常可以使用console.log方法,来输出一些我们感兴趣的信息和内容。plpgsql中,也有类似的功能,它使用Raise语句。Raise语句的标准形式如下:

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;
-- 错误抛出
RAISE division_by_zero;
RAISE SQLSTATE '22012';

post函数的用法__postevent函数

简单说明一下:

实际上,完整的Raise的选项和使用是比较复杂的,但作为一般的应用开发人员,并不是特别关心这些内容,因为通常不会直接来处理会话输出的错误信息,最多就是在编写函数和调试过程中使用。所以这里不再展开讨论。

PG技术文档中,相关的详细内容的链接如下:

如何进行错误处理

一般的编程语言中,使用try-catch语句块,来进行错误的捕获和处理。plpgsql中,使用BEGIN...EXCPTION...END语句,来进行错误处理(类似于try...catch)。然后在后续的WHEN THEN结构中,可以针对具体的错误类型进行处理。相关的示例如下:


-- 标准结构
[ <<label>> ]
[ DECLARE
    declarations ]
BEGIN
    statements
EXCEPTION
    WHEN condition [ OR condition ... ] THEN
        handler_statements
    [ WHEN condition [ OR condition ... ] THEN
          handler_statements
      ... ]
END;
-- 示例
INSERT INTO mytab(firstname, lastname) VALUES('Tom', 'Jones');
BEGIN
    UPDATE mytab SET firstname = 'Joe' WHERE lastname = 'Jones';
    x := x + 1;
    y := x / 0;
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE 'caught division_by_zero';
        RETURN x;
END;

这里可以看到一些要点:

如何获取更详细的诊断信息

除了错误捕获之外,为了方便函数执行过程的审查和监控,Postgres提供了DIAGNOSTICS相关语句,来帮助开发和使用者了解在函数执行过程中的信息和状态。这些信息大体可以分为错误类的信息和调用类的信息。下面简单展示了其使用的示例代码:

-- 错误诊断
DECLARE
  text_var1 text;
  text_var2 text;
  text_var3 text;
BEGIN
  -- some processing which might cause an exception
  ...
EXCEPTION WHEN OTHERS THEN
  GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT,
                          text_var2 = PG_EXCEPTION_DETAIL,
                          text_var3 = PG_EXCEPTION_HINT;
END;
-- 调用堆叠, 注意其中的嵌套调用关系
CREATE OR REPLACE FUNCTION outer_func() RETURNS integer AS $$
BEGIN
  RETURN inner_func();
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION inner_func() RETURNS integer AS $$
DECLARE
  stack text;
BEGIN
  GET DIAGNOSTICS stack = PG_CONTEXT;
  RAISE NOTICE E'--- Call Stack ---\n%', stack;
  RETURN 1;
END;
$$ LANGUAGE plpgsql;
SELECT outer_func();
NOTICE:  --- Call Stack ---
PL/pgSQL function inner_func() line 5 at GET DIAGNOSTICS
PL/pgSQL function outer_func() line 3 at RETURN
CONTEXT:  PL/pgSQL function outer_func() line 3 at RETURN
 outer_func
 ------------
           1
(1 row)

这里面,可选的诊断项目变量包括:

如何处理事务

使用函数和过程还有一个比较大的好处,就是可以使用事务,并且将其封装在一个相对封闭的代码块中,让外部程序可以比较简单方便的调用。

从逻辑上而言,在函数中的事务操作,和普通代码块中,没有任何的区别。而且在函数中,事务处理是自动的。BEGIN语句默认就开启了一个事务,EXCPTION中可以控制异常处理,并定义是否回滚(字段)或者提交事务。RETURN和END时,如果没有其他设置,则会自动提交事务。默认情况下,这个处理是透明和自动的。关于复杂的事务控制,可能会涉及到其他内容,笔者打算在专门的章节中进行讨论,这里就不再深入探讨和提供简单的示例了。

如何测试和调试函数或者过程

笔者确实不知晓在Postgres系统或者生态中,是否提供了像一般编程语言一样的过程化执行和调试工具。只发现可以使用Raise语句一样,可以在会话中自定义输出一些过程中的信息作为调试信息。

此外,plpgsql还提供了assert语句,来执行断言测试:

ASSERT condition [ , message ];

技术文档中,没有提供很好的示例,只是做了简单的说明。condition是一个逻辑表达式,当结果值为真时,不做任何处理。如果结果为假,则会抛出一个 “assertion failed”错误。从标准范式格式来看,这个错误信息的文字内容是可以自定义的。当然,代码块中,需要配套的Exception语句,进行错误的处理。

显然,基于其执行的环境和原理,对Postgres函数的调试,可能并不像在一般编程语言中那么方便。比如笔者现在也没有接触到可以使用所谓的SQL开发调试工具,可以使用步进和断点的方式来进行plpgsql程序的调试,这些限制使得PG函数的编写和调试过程不是很方面,基本上只能通过日志的方式检查和实现。

如果读者知道相关的情况和更好的方案,希望不吝赐教。

小结

本文是《PostgreSQL技术问答 - Function函数》 下半部分的内容。讨论了在Postgres中编写和使用函数的一些扩展性的特性,包括函数和定义查询、结果集遍历循环、事务支持和测试调试等方面的内容。