《PostgreSQL 開發指南》第 26 篇 存儲過程

概述

在 PostgreSQL 中,除了標準 SQL 語句之外還支持使用各種過程語言(例如 PL/pgSQL、C、PL/Tcl、PL/Python、PL/Perl、PL/Java 等 )創建複雜的過程和函數,稱爲存儲過程(Stored Procedure)和自定義函數(User-Defined Function)。存儲過程支持許多過程元素,例如控制結構、循環和複雜的計算。

使用存儲過程帶來的好處包括:

  • 減少應用和數據庫之間的網絡傳輸。所有的 SQL 語句都存儲在數據庫服務器中,應用程序只需要發送函數調用並獲取除了結果,避免了發送多個 SQL 語句並等待結果。
  • 提高應用的性能。因爲自定義函數和存儲過程進行了預編譯並存儲在數據庫服務器中。
  • 可重用性。存儲過程和函數的功能可以被多個應用同時使用。

當然,使用存儲過程也可能帶來一些問題:

  • 導致軟件開發緩慢。因爲存儲過程需要單獨學習,而且很多開發人員並不具備這種技能。
  • 不易進行版本管理和代碼調試。
  • 不同數據庫管理系統之間無法移植,語法存在較大的差異。

本文主要介紹 PL/pgSQL 存儲過程,它和 Oracle PL/SQL 非常類似,是 PostgreSQL默認支持的存儲過程。使用 PL/pgSQL 的原因包括:

  • PL/pgSQL 簡單易學,無論是否具有編程基礎都能夠很快學會。
  • PL/pgSQL 是 PostgreSQL 默認支持的過程語言,PL/pgSQL 開發的自定義函數可以和內置函數一樣使用。
  • PL/pgSQL 提高了許多強大的功能,例如遊標,可以實現複雜的函數。

PL/pgSQL 代碼塊結構

PL/pgSQL 是一種塊狀語言,因此存儲過程和函數以代碼塊的形式進行組織。以下是一個 PL/pgSQL 代碼塊的定義:

[ <<label>> ]
[ DECLARE
    declarations ]
BEGIN
    statements;
	...
END [ label ];

其中,label 是一個可選的代碼塊標籤,可以用於 EXIT 語句退出指定的代碼塊,或者限定變量的名稱;DECLARE 是一個可選的聲明部分,用於定義變量;BEGIN 和 END 之間是代碼主體,也就是主要的功能代碼;所有的語句都使用分號(;)結束,END 之後的分號表示代碼塊結束。

以下是一個簡單的代碼塊示例:

DO $$ 
DECLARE
  name text;
BEGIN 
  name := 'PL/pgSQL';
  RAISE NOTICE 'Hello %!', name;
END $$;

以上是一個匿名塊,與此相對的是命名塊(也就是存儲過程和函數)。其中,DO 語句用於執行匿名塊;我們定義了一個字符串變量 name,然後給它賦值並輸出一個信息;RAISE NOTICE 用於輸出通知消息。

$$ 用於替換單引號(’),因爲 PL/pgSQL 代碼主體必須是字符串文本,意味着代碼中所有的單引號都必須轉義(重複寫兩次)。對於上面的示例,需要寫成以下形式:

DO
'DECLARE
  name text;
BEGIN 
  name := ''PL/pgSQL'';
  RAISE NOTICE ''Hello %!'', name;
END ';

顯然這種寫法很不方便,因此 PL/pgSQL 提供了 $$ 避免單引號問題。我們經常還會遇到其他形式的符號,例如 $function$ 或者 $procedure$,作用也是一樣。

在 psql 客戶端運行以上代碼的結果如下:

postgres=# DO $$ 
postgres$# DECLARE
postgres$#   name text;
postgres$# BEGIN 
postgres$#   name := 'PL/pgSQL';
postgres$#   RAISE NOTICE 'Hello %!', name;
postgres$# END $$;

NOTICE:  Hello PL/pgSQL!

嵌套子塊

PL/pgSQL 支持代碼塊的嵌套,也就是將一個代碼塊嵌入其他代碼塊的主體中。被嵌套的代碼塊被稱爲子塊(subblock),包含子塊的代碼塊被稱爲外部塊(subblock )。子塊可以將代碼進行邏輯上的拆分,子塊中可以定義與外部塊重名的變量,而且在子塊內擁有更高的優先級。例如:

DO $$ 
<<outer_block>>
DECLARE
  name text;
BEGIN 
  name := 'outer_block';
  RAISE NOTICE 'This is %', name;

  DECLARE 
     name text := 'sub_block';
  BEGIN 
     RAISE NOTICE 'This is %', name;
     RAISE NOTICE 'The name from the outer block is %', outer_block.name;
  END;

  RAISE NOTICE 'This is %', name;
   
END outer_block $$;

首先,外部塊中定義了一個變量 name,值爲“outer_block”,輸出該變量的值;然後在子塊中定義了同名的變量,值爲“sub_block”,輸出該變量的值,並且通過代碼塊標籤輸出了外部塊的變量值;最後再次輸出該變量的值。以上代碼執行的輸出結果如下:

NOTICE:  This is outer_block
NOTICE:  This is sub_block
NOTICE:  The name from the outer block is outer_block
NOTICE:  This is outer_block

聲明與賦值

與其他編程語言類似,PL/pgSQL 支持定義變量和常量。

變量

變量是一個有意義的名字,代表了內存中的某個位置。變量總是屬於某個數據類型,變量的值可以在運行時被修改。

在使用變量之前,需要在代碼的聲明部分進行聲明:

variable_name data_type [ NOT NULL ] [ { DEFAULT | := | = } expression ];

其中,variable_name 是變量的名稱,通常需要指定一個有意義的名稱;data_type 是變量的類型,可以是任何 SQL 數據類型;如果指定了 NOT NULL,必須使用後面的表達式爲變量指定初始值。

以下是一些變量聲明的示例:

user_id integer;
quantity numeric(5) DEFAULT 0;
url varchar := 'http://mysite.com';

除了基本的 SQL 數據類型之外,PL/pgSQL 還支持基於表的字段或行或者其他變量定義變量:

myrow tablename%ROWTYPE;
myfield tablename.columnname%TYPE;
amount quantity%TYPE;

myrow 是一個行類型的變量,可以存儲查詢語句返回的數據行(數據行的結構要和 tablename 相同);myfield 的數據類型取決於 tablename.columnname 字段的定義;amount 和 quantity 的類型一致。

與行類型變量類似的還有記錄類型變量,例如:

arow RECORD;

記錄類型的變量沒有預定義的結構,只有當變量被賦值時才確定,而且可以在運行時被改變。記錄類型的變量可以用於任意查詢語句或者 FOR 循環變量。

除此之外,PL/pgSQL 還可以使用 ALIAS 定義一個變量別名:

newname ALIAS FOR oldname;

此時,newname 和 oldname 代表了相同的對象。

常量

如果在定義變量時指定了 CONSTANT 關鍵字,意味着定義的是常量。常量的值需要在聲明時初始化,並且不能修改。

以下示例通過定義常量 PI 計算圓的面積:

DO $$ 
DECLARE
  PI CONSTANT NUMERIC := 3.14159265;
  radius NUMERIC;
BEGIN 
   radius := 1.0;
   RAISE NOTICE 'The area is %', PI * radius * radius;
END $$;

NOTICE:  The area is 3.1415926500

常量可以用於避免魔數(magic number),提高代碼的可讀性;也可以減少代碼的維護工作,所有使用常量的代碼都會隨着常量值的修改而同步,不需要修改多個硬編碼的數據值。

控制結構

IF 語句

IF 語句可以基於條件選擇性執行操作, PL/pgSQL 提供了三種形式的 IF 語句。

  • IF … THEN … END IF
  • IF … THEN … ELSE … END IF
  • IF … THEN … ELSIF … THEN … ELSE … END IF

首先,最簡單的 IF 語句如下:

IF boolean-expression THEN
    statements
END IF;

如果表達式 boolean-expression 的值爲真,執行 THEN 之後的語句;否則,忽略這些語句。例如:

DO $$
BEGIN 
  IF 2 > 3 THEN
	RAISE NOTICE '2 大於 3';
  END IF;

  IF 2 < 3 THEN
	RAISE NOTICE '2 小於 3';
  END IF;
END $$;

NOTICE:  2 小於 3

第二種 IF 語句的語法如下:

IF boolean-expression THEN
    statements
ELSE
    other-statements
END IF;

如果表達式 boolean-expression 的值爲真,執行 THEN 之後的語句;否則,執行 ELSE 之後的語句。例如:

DO $$
BEGIN 
  IF 2 > 3 THEN
	RAISE NOTICE '2 大於 3';
  ELSE
	RAISE NOTICE '2 小於 3';
  END IF;
END $$;

NOTICE:  2 小於 3

第三種 IF 語句支持多個條件分支:

IF boolean-expression THEN
    statements
[ ELSIF boolean-expression THEN
    statements ] 
[ ELSIF boolean-expression THEN
    statements ]
...
[ ELSE
    statements ]
END IF;

依次判斷條件中的表達式,如果某個條件爲真,執行相應的語句;如果所有條件都爲假,執行 ELSE 後面的語句;如果沒有 ELSE 就什麼都不執行。例如:

DO $$
DECLARE
  i integer := 3;
  j integer := 3;
BEGIN 
  IF i > j THEN
	RAISE NOTICE 'i 大於 j';
  ELSIF i < j THEN
    RAISE NOTICE 'i 小於 j';
  ELSE
    RAISE NOTICE 'i 等於 j';
  END IF;
END $$;

NOTICE:  i 等於 j
DO

CASE 語句

除了 IF 語句之外,PostgreSQL 還提供了 CASE 語句,同樣可以根據不同的條件執行不同的分支語句。CASE 語句分爲兩種:簡單 CASE 和搜索 CASE 語句。

⚠️CASE 語句和第 15 篇中介紹的 CASE 表達式不是一個概念,CASE 表達式是一個 SQL 表達式。

簡單 CASE 語句的結構如下:

CASE search-expression
    WHEN expression [, expression [ ... ]] THEN
      statements
  [ WHEN expression [, expression [ ... ]] THEN
      statements
    ... ]
  [ ELSE
      statements ]
END CASE;

首先,計算 search-expression 的值;然後依次和 WHEN 中的表達式進行等值比較;如果找到了相等的值,執行相應的 statements;後續的分支不再進行判斷;如果沒有匹配的值,執行 ELSE 語句;如果此時沒有 ELSE,將會拋出 CASE_NOT_FOUND 異常。

例如:

DO $$
DECLARE
  i integer := 3;
BEGIN 
  CASE i
    WHEN 1, 2 THEN
      RAISE NOTICE 'one or two';
	WHEN 3, 4 THEN
      RAISE NOTICE 'three or four';
    ELSE
      RAISE NOTICE 'other value';
  END CASE;
END $$;

NOTICE:  three or four

簡單 CASE 語句只能進行簡單的等值比較,搜索 CASE 語句可以實現更復雜的控制邏輯:

CASE
    WHEN boolean-expression THEN
      statements
  [ WHEN boolean-expression THEN
      statements
    ... ]
  [ ELSE
      statements ]
END CASE;

依次判斷每個 WHEN 之後的表達式,如果爲真則執行相應的語句;後續的分支不再進行判斷;如果沒有匹配的值,執行 ELSE 語句;如果此時沒有 ELSE,將會拋出 CASE_NOT_FOUND 異常。例如:

DO $$
DECLARE
  i integer := 3;
BEGIN 
  CASE 
    WHEN i BETWEEN 0 AND 10 THEN
      RAISE NOTICE 'value is between zero and ten';
	WHEN i BETWEEN 11 AND 20 THEN
      RAISE NOTICE 'value is between eleven and twenty';
    ELSE
      RAISE NOTICE 'other value';
  END CASE;
END $$;

搜索 CASE 表達式可以構造任意複雜的判斷邏輯,實現 IF 語句的各種功能。

循環語句

PostgreSQL 提供了 4 種循環執行命令的語句:LOOP、WHILE、FOR 和 FOREACH 循環,以及循環控制的 EXIT 和 CONTINUE 語句。

首先,LOOP 用於定義一個無限循環語句:

[ <<label>> ]
LOOP
    statements
END LOOP [ label ];

一般需要使用 EXIT 或者 RETURN 語句退出循環,label 可以用於 EXIT 或者 CONTINUE 語句退出或者跳到執行的嵌套循環中。例如:

DO $$
DECLARE
  i integer := 0;
BEGIN 
  LOOP
    EXIT WHEN i = 5;
    i := i + 1;
    RAISE NOTICE 'Loop: %', i;
  END LOOP;
END $$;

NOTICE:  Loop: 1
NOTICE:  Loop: 2
NOTICE:  Loop: 3
NOTICE:  Loop: 4
NOTICE:  Loop: 5

其中,EXIT 語句用於退出循環。完整的 EXIT 語句如下:

EXIT [ label ] [ WHEN boolean-expression ];

另一個控制循環的語句是 CONTINUE:

CONTINUE [ label ] [ WHEN boolean-expression ];

CONTINUE 表示忽略後面的語句,直接進入下一次循環。例如:

DO $$
DECLARE
  i integer := 0;
BEGIN 
  LOOP
    EXIT WHEN i = 10;
    i := i + 1;
    CONTINUE WHEN mod(i, 2) = 1;
    RAISE NOTICE 'Loop: %', i;
  END LOOP;
END $$;

NOTICE:  Loop: 2
NOTICE:  Loop: 4
NOTICE:  Loop: 6
NOTICE:  Loop: 8
NOTICE:  Loop: 10

當變量 i 爲奇數時,直接進入下一次循環,不會打印出變量的值。

WHILE 循環的語法如下:

[ <<label>> ]
WHILE boolean-expression LOOP
    statements
END LOOP [ label ];

當表達式 boolean-expression 的值爲真時,循環執行其中的語句;然後重新計算表達式的值,當表達式的值假時退出循環。例如:

DO $$
DECLARE
  i integer := 0;
BEGIN 
  WHILE i < 5 LOOP
    i := i + 1;
    RAISE NOTICE 'Loop: %', i;
  END LOOP;
END $$;

NOTICE:  Loop: 1
NOTICE:  Loop: 2
NOTICE:  Loop: 3
NOTICE:  Loop: 4
NOTICE:  Loop: 5

FOR 循環可以用於遍歷一個整數範圍或者查詢結果集,遍歷整數範圍的語法如下:

[ <<label>> ]
FOR name IN [ REVERSE ] expression .. expression [ BY expression ] LOOP
    statements
END LOOP [ label ];

FOR 循環默認從小到大進行遍歷,REVERSE 表示從大到小遍歷;BY 用於指定每次的增量,默認爲 1。例如:

DO $$
BEGIN 
  FOR i IN 1..5 BY 2 LOOP
	RAISE NOTICE 'Loop: %', i;
  END LOOP;
END $$;

NOTICE:  Loop: 1
NOTICE:  Loop: 3
NOTICE:  Loop: 5

變量 i 不需要提前定義,可以在 FOR 循環內部使用。

遍歷查詢結果集的 FOR 循環如下:

[ <<label>> ]
FOR target IN query LOOP
    statements
END LOOP [ label ];

其中,target 可以是一個 RECORD 變量、行變量或者逗號分隔的標量列表。在循環中,target 代表了每次遍歷的行數據。例如:

DO $$
DECLARE
  emp record;
BEGIN 
  FOR emp IN (SELECT * FROM employees LIMIT 5) LOOP
	RAISE NOTICE 'Loop: %,%', emp.first_name, emp.last_name;
  END LOOP;
END $$;

NOTICE:  Loop: Steven,King
NOTICE:  Loop: Neena,Kochhar
NOTICE:  Loop: Lex,De Haan
NOTICE:  Loop: Alexander,Hunold
NOTICE:  Loop: Bruce,Ernst

FOREACH 循環與 FOR 循環類似,只不過變量的是一個數組:

[ <<label>> ]
FOREACH target [ SLICE number ] IN ARRAY expression LOOP
    statements
END LOOP [ label ];

如果沒有指定 SLICE 或者指定 SLICE 0,FOREACH 將會變量數組中的每個元素。例如:

DO $$
DECLARE
  x int;
BEGIN
  FOREACH x IN ARRAY (ARRAY[[1,2,3],[4,5,6]])
  LOOP
    RAISE NOTICE 'x = %', x;
  END LOOP;
END $$;

NOTICE:  x = 1
NOTICE:  x = 2
NOTICE:  x = 3
NOTICE:  x = 4
NOTICE:  x = 5
NOTICE:  x = 6

如果指定了一個正整數的 SLICE,FOREACH 將會變量數組的切片;SLICE 不能大於數組的維度。例如:

DO $$
DECLARE
  x int[];
BEGIN
  FOREACH x SLICE 1 IN ARRAY (ARRAY[[1,2,3],[4,5,6]])
  LOOP
    RAISE NOTICE 'row = %', x;
  END LOOP;
END $$;

NOTICE:  row = {1,2,3}
NOTICE:  row = {4,5,6}

以上示例通過 FOREACH 語句遍歷了數組的一維切片。

遊標

PL/pgSQL 遊標允許我們封裝一個查詢,然後每次處理結果集中的一條記錄。遊標可以將大結果集拆分成許多小的記錄,避免內存溢出;另外,我們可以定義一個返回遊標引用的函數,然後調用程序可以基於這個引用處理返回的結果集。

使用遊標的步驟大體如下:

  1. 聲明遊標變量;
  2. 打開遊標;
  3. 從遊標中獲取結果;
  4. 判斷是否存在更多結果。如果存在,執行第 3 步;否則,執行第 5 步;
  5. 關閉遊標。

我們直接通過一個示例演示使用遊標的過程:

DO $$
DECLARE 
  rec_emp RECORD;
  cur_emp CURSOR(p_deptid INTEGER) FOR
	SELECT first_name, last_name, hire_date 
    FROM employees
	WHERE department_id = p_deptid;
BEGIN
  -- 打開遊標
  OPEN cur_emp(60);
	
  LOOP
    -- 獲取遊標中的記錄
    FETCH cur_emp INTO rec_emp;
    -- 沒有找到更多數據時退出循環
    EXIT WHEN NOT FOUND;

    RAISE NOTICE '%,% hired at:%' , rec_emp.first_name, rec_emp.last_name, rec_emp.hire_date;
  END LOOP;
  
  -- Close the cursor
  CLOSE cur_emp;
END $$;

NOTICE:  Alexander,Hunold hired at:2006-01-03
NOTICE:  Bruce,Ernst hired at:2007-05-21
NOTICE:  David,Austin hired at:2005-06-25
NOTICE:  Valli,Pataballa hired at:2006-02-05
NOTICE:  Diana,Lorentz hired at:2007-02-07

首先,聲明瞭一個遊標 cur_emp,並且綁定了一個查詢語句,通過一個參數 p_deptid 獲取指定部門的員工;然後使用 OPEN 打開遊標;接着在循環中使用 FETCH 語句獲取遊標中的記錄,如果沒有找到更多數據退出循環語句;變量 rec_emp 用於存儲遊標中的記錄;最後使用 CLOSE 語句關閉遊標,釋放資源。

遊標是 PL/pgSQL 中的一個強大的數據處理功能,更多的使用方法可以參考官方文檔

錯誤處理

報告錯誤和信息

PL/pgSQL 提供了 RAISE 語句,用於打印消息或者拋出錯誤:

RAISE level format;

不同的 level 代表了錯誤的不同嚴重級別,包括:

  • DEBUG
  • LOG
  • NOTICE
  • INFO
  • WARNING
  • EXCEPTION

在上文示例中,我們經常使用 NOTICE 輸出一些信息。如果不指定 level,默認爲 EXCEPTION,將會拋出異常並且終止代碼運行。

format 是一個用於提供信息內容的字符串,可以使用百分號(%)佔位符接收參數的值, 兩個連寫的百分號(%%)表示輸出百分號自身。

以下是一些 RAISE 示例:

DO $$ 
BEGIN 
  RAISE DEBUG 'This is a debug text.';
  RAISE INFO 'This is an information.';
  RAISE LOG 'This is a log.';
  RAISE WARNING 'This is a warning at %', now();
  RAISE NOTICE 'This is a notice %%';
END $$;

INFO:  This is an information.
WARNING:  This is a warning at 2020-05-16 11:27:06.138569+08
NOTICE:  This is a notice %

從結果可以看出,並非所有的消息都會打印到客戶端和服務器日誌中。這個可以通過配置參數 client_min_messageslog_min_messages 進行設置。

對於 EXCEPTION 級別的錯誤,可以支持額外的選項:

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

其中,option 可以是以下選項:

  • MESSAGE,設置錯誤消息。如果 RAISE 語句中已經包含了 format 字符串,不能再使用該選項。
  • DETAIL,指定錯誤詳細信息。
  • HINT,設置一個提示信息。
  • ERRCODE,指定一個錯誤碼(SQLSTATE)。可以是文檔中的條件名稱或者五個字符組成的 SQLSTATE 代碼。
  • COLUMN、CONSTRAINT、DATATYPE、TABLE、SCHEMA,返回相關對象的名稱。

以下是一些示例:

RAISE EXCEPTION 'Nonexistent ID --> %', user_id
      USING HINT = 'Please check your user ID';

RAISE 'Duplicate user ID: %', user_id USING ERRCODE = 'unique_violation';
RAISE 'Duplicate user ID: %', user_id USING ERRCODE = '23505';

RAISE division_by_zero;
RAISE SQLSTATE '22012';

檢查斷言

PL/pgSQL 提供了 ASSERT 語句,用於調試存儲過程和函數:

ASSERT condition [ , message ];

其中,condition 是一個布爾表達式;如果它的結果爲真,ASSERT 通過;如果結果爲假或者 NULL,將會拋出 ASSERT_FAILURE 異常。message 用於提供額外的錯誤信息,默認爲“assertion failed”。例如:

DO $$ 
DECLARE 
  i integer := 1;
BEGIN 
  ASSERT i = 0, 'i 的初始值應該爲 0!';
END $$;

ERROR:  i 的初始值應該爲 0!
CONTEXT:  PL/pgSQL function inline_code_block line 5 at ASSERT

⚠️注意,ASSERT 只適用於代碼調試;輸出錯誤信息使用 RAISE 語句。

捕獲異常

默認情況下,PL/pgSQL 遇到錯誤時會終止代碼執行,同時撤銷事務。我們也可以在代碼塊中使用 EXCEPTION 捕獲錯誤並繼續事務:

[ <<label>> ]
[ DECLARE
    declarations ]
BEGIN
    statements
EXCEPTION
    WHEN condition [ OR condition ... ] THEN
        handler_statements
    [ WHEN condition [ OR condition ... ] THEN
          handler_statements
      ... ]
END;

如果代碼執行出錯,程序將會進入 EXCEPTION 模塊;依次匹配 condition,找到第一個匹配的分支並執行相應的 handler_statements;如果沒有找到任何匹配的分支,繼續拋出錯誤。

以下是一個除零錯誤的示例:

DO $$ 
DECLARE 
  i integer := 1;
BEGIN 
  i := i / 0;
EXCEPTION
  WHEN division_by_zero THEN
    RAISE NOTICE '除零錯誤!';
  WHEN OTHERS THEN
    RAISE NOTICE '其他錯誤!';
END $$;

NOTICE:  除零錯誤!

OTHERS 用於捕獲未指定的錯誤類型。

PL/pgSQL 還提供了捕獲詳細錯誤信息的 GET STACKED DIAGNOSTICS 語句,具體可以參考官方文檔

自定義函數

要創建一個自定義的 PL/pgSQL 函數,可以使用 CREATE FUNCTION 語句:

CREATE [ OR REPLACE ] FUNCTION
  name ( [ [ argmode ] [ argname ] argtype [ { DEFAULT | = } default_expr ] [, ...] ] )
  RETURNS rettype
AS $$
DECLARE
  declarations
BEGIN
  statements;
  ...
END; $$
LANGUAGE plpgsql;

CREATE 表示創建函數,OR REPLACE 表示替換函數定義;name 是函數名;括號內是參數,多個參數使用逗號分隔;argmode 可以是 IN(輸入)、OUT(輸出)、INOUT(輸入輸出)或者 VARIADIC(數量可變),默認爲 IN;argname 是參數名稱;argtype 是參數的類型;default_expr 是參數的默認值;rettype 是返回數據的類型;AS 後面是函數的定義,和上文中的匿名塊相同;最後,LANGUAGE 指定函數實現的語言,也可以是其他過程語言。

以下示例創建一個函數 get_emp_count,用於返回指定部門中的員工數量:

CREATE OR REPLACE FUNCTION get_emp_count(p_deptid integer)
 RETURNS integer 
AS $$
DECLARE
  ln_count integer;
BEGIN
  select count(*) into ln_count
  from employees
  where department_id = p_deptid;
  
  return ln_count;
END; $$
LANGUAGE plpgsql;

創建該函數之後,可以像內置函數一樣在 SQL 語句中進行調用:

select department_id,department_name,get_emp_count(department_id)
from departments d;
department_id|department_name     |get_emp_count|
-------------|--------------------|-------------|
           10|Administration      |            1|
           20|Marketing           |            2|
           30|Purchasing          |            6|
...

PL/pgSQL 函數支持重載(Overloading),也就是相同的函數名具有不同的函數參數。例如,以下語句創建一個重載的函數 get_emp_count,返回指定部門指定日期之後入職的員工數量:

CREATE OR REPLACE FUNCTION get_emp_count(p_deptid integer, p_hiredate date)
 RETURNS integer 
AS $$
DECLARE
  ln_count integer;
BEGIN
  select count(*) into ln_count
  from employees
  where department_id = p_deptid and hire_date >= p_hiredate;
  
  return ln_count;
END; $$
LANGUAGE plpgsql;

查詢每個部門 2005 年之後入職的員工數量:

select department_id,department_name,get_emp_count(department_id),get_emp_count(department_id, '2005-01-01')
from departments d;
department_id|department_name     |get_emp_count|get_emp_count|
-------------|--------------------|-------------|-------------|
           10|Administration      |            1|            0|
           20|Marketing           |            2|            1|
           30|Purchasing          |            6|            4|
...

我們再來看一個 VARIADIC 參數的示例:

CREATE OR REPLACE FUNCTION sum_num(
  VARIADIC nums numeric[])
  RETURNS numeric
AS $$
DECLARE ln_total numeric;
BEGIN
  SELECT SUM(nums[i]) INTO ln_total
  FROM generate_subscripts(nums, 1) t(i);
  
  RETURN ln_total;
END; $$
LANGUAGE plpgsql;

參數 nums 是一個數組,可以傳入任意多個參數;然後計算它們的和值。例如:

SELECT sum_num(1,2), sum_num(1,2,3);
sum_num|sum_num|
-------|-------|
      3|      6|

如果函數不需要返回結果,可以返回 void 類型;或者直接使用存儲過程。

存儲過程

PostgreSQL 11 增加了存儲過程,使用 CREATE PROCEDURE 語句創建:

CREATE [ OR REPLACE ] PROCEDURE
  name ( [ [ argmode ] [ argname ] argtype [ { DEFAULT | = } default_expr ] [, ...] ] )
AS $$
DECLARE
  declarations
BEGIN
  statements;
  ...
END; $$
LANGUAGE plpgsql;

存儲過程的定義和函數主要的區別在於沒有返回值,其他內容都類似。以下示例創建了一個存儲過程 update_emp,用於修改員工的信息:

CREATE OR REPLACE PROCEDURE update_emp(
  p_empid in integer,
  p_salary in numeric,
  p_phone in varchar)
AS $$
BEGIN
  update employees 
  set salary = p_salary,
      phone_number = p_phone
  where employee_id = p_empid;
END; $$
LANGUAGE plpgsql;

調用存儲過程使用 CALL 語句:

call update_emp(100, 25000, '515.123.4560');

事務管理

在存儲過程內部,可以使用 COMMIT 或者 ROLLBACK 語句提交或者回滾事務。例如:

create table test(a int);

CREATE PROCEDURE transaction_test()
LANGUAGE plpgsql
AS $$
BEGIN
    FOR i IN 0..9 LOOP
        INSERT INTO test (a) VALUES (i);
        IF i % 2 = 0 THEN
            COMMIT;
        ELSE
            ROLLBACK;
        END IF;
    END LOOP;
END
$$;

CALL transaction_test();
select * from test;
a|
-|
0|
2|
4|
6|
8|

只有偶數纔會被最終提交。

歡迎點贊👍、評論📝、收藏❤️!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章