概述
在 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 遊標允許我們封裝一個查詢,然後每次處理結果集中的一條記錄。遊標可以將大結果集拆分成許多小的記錄,避免內存溢出;另外,我們可以定義一個返回遊標引用的函數,然後調用程序可以基於這個引用處理返回的結果集。
使用遊標的步驟大體如下:
- 聲明遊標變量;
- 打開遊標;
- 從遊標中獲取結果;
- 判斷是否存在更多結果。如果存在,執行第 3 步;否則,執行第 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_messages 和 log_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|
只有偶數纔會被最終提交。
歡迎點贊👍、評論📝、收藏❤️!