原創(chuàng)文章,首發(fā)自作者個(gè)人博客Jason's Blog。
轉(zhuǎn)載請(qǐng)務(wù)必在文章開(kāi)頭處注明出自Jason's Blog,并給出原文鏈接
存儲(chǔ)過(guò)程簡(jiǎn)介
什么是存儲(chǔ)過(guò)程
百度百科是這么描述存儲(chǔ)過(guò)程的:存儲(chǔ)過(guò)程(Stored Procedure)是在大型數(shù)據(jù)庫(kù)系統(tǒng)中,一組為了完成特定功能的SQL語(yǔ)句集,存儲(chǔ)在數(shù)據(jù)庫(kù)中,首次編譯后再次調(diào)用不需要再次編譯,用戶(hù)通過(guò)指定存儲(chǔ)過(guò)程的名字并給出參數(shù)(如果有)來(lái)執(zhí)行它。它是數(shù)據(jù)庫(kù)中的一個(gè)重要對(duì)象,任何一個(gè)設(shè)計(jì)良好的數(shù)據(jù)庫(kù)應(yīng)用程序都應(yīng)該用到存儲(chǔ)過(guò)程。
維基百科是這樣定義的:A stored procedure (also termed proc, storp, sproc, StoPro, StoredProc, StoreProc, sp, or SP) is a subroutine available to applications that access a relational database management system (RDMS). Such procedures are stored in the database data dictionary。
PostgreSQL對(duì)存儲(chǔ)過(guò)程的描述是:存儲(chǔ)過(guò)程和用戶(hù)自定義函數(shù)(UDF)是SQL和過(guò)程語(yǔ)句的集合,它存儲(chǔ)于數(shù)據(jù)庫(kù)服務(wù)器并能被SQL接口調(diào)用。
總結(jié)下來(lái)存儲(chǔ)過(guò)程有如下特性:
- 存儲(chǔ)于數(shù)據(jù)庫(kù)服務(wù)器
- 一次編譯后可多次調(diào)用
- 設(shè)計(jì)良好的數(shù)據(jù)庫(kù)應(yīng)用程序很可能會(huì)用到它
- 由SQL和過(guò)程語(yǔ)句來(lái)定義
- 應(yīng)用程序通過(guò)SQL接口來(lái)調(diào)用
使用存儲(chǔ)過(guò)程的優(yōu)勢(shì)及劣勢(shì)
首先看看使用存儲(chǔ)過(guò)程的優(yōu)勢(shì)
- 減少應(yīng)用與數(shù)據(jù)庫(kù)服務(wù)器的通信開(kāi)銷(xiāo),從而提升整體性能。筆者在項(xiàng)目中使用的存儲(chǔ)過(guò)程,少則幾十行,多則幾百行甚至上千行(假設(shè)一行10個(gè)字節(jié),一千行即相當(dāng)于10KB),如果不使用存儲(chǔ)過(guò)程而直接通過(guò)應(yīng)用程序?qū)⑾鄳?yīng)SQL請(qǐng)求發(fā)送到數(shù)據(jù)庫(kù)服務(wù)器,會(huì)增大網(wǎng)絡(luò)通信開(kāi)銷(xiāo)。相反,使用存儲(chǔ)過(guò)程能降低該開(kāi)銷(xiāo),從而提升整體性能。尤其在一些BI系統(tǒng)中,一個(gè)頁(yè)面往往要使用多個(gè)存儲(chǔ)過(guò)程,此時(shí)存儲(chǔ)過(guò)程降低網(wǎng)絡(luò)通信開(kāi)銷(xiāo)的優(yōu)勢(shì)非常明顯
- 一次編譯多次調(diào)用,提高性能。存儲(chǔ)過(guò)程存于數(shù)據(jù)庫(kù)服務(wù)器中,第一次被調(diào)用后即被編譯,之后再調(diào)用時(shí)無(wú)需再次編譯,直接執(zhí)行,提高了性能
- 同一套業(yè)務(wù)邏輯可被不同應(yīng)用程序共用,減少了應(yīng)用程序的開(kāi)發(fā)復(fù)雜度,同時(shí)也保證了不同應(yīng)用程序使用的一致性
- 保護(hù)數(shù)據(jù)庫(kù)元信息。如果應(yīng)用程序直接使用SQL語(yǔ)句查詢(xún)數(shù)據(jù)庫(kù),會(huì)將數(shù)據(jù)庫(kù)表結(jié)構(gòu)暴露給應(yīng)用程序,而使用存儲(chǔ)過(guò)程是應(yīng)用程序并不知道數(shù)據(jù)庫(kù)表結(jié)構(gòu)
- 更細(xì)粒度的數(shù)據(jù)庫(kù)權(quán)限管理。直接從表讀取數(shù)據(jù)時(shí),對(duì)應(yīng)用程序只能實(shí)現(xiàn)表級(jí)別的權(quán)限管理,而使用存儲(chǔ)過(guò)程是,可在存儲(chǔ)過(guò)程中將應(yīng)用程序無(wú)權(quán)訪問(wèn)的數(shù)據(jù)屏蔽
- 將業(yè)務(wù)實(shí)現(xiàn)與應(yīng)用程序解耦。當(dāng)業(yè)務(wù)需求更新時(shí),只需更改存儲(chǔ)過(guò)程的定義,而不需要更改應(yīng)用程序
- 可以通過(guò)其它語(yǔ)言并可及其它系統(tǒng)交互。比如可以使用PL/Java與Kafka交互,將存儲(chǔ)過(guò)程的參數(shù)Push到Kafka或者將從Kafka獲取的數(shù)據(jù)作為存儲(chǔ)過(guò)程的結(jié)果返回給調(diào)用方
當(dāng)然,使用存儲(chǔ)過(guò)程也有它的劣勢(shì)
- 不便于調(diào)試。尤其在做性能調(diào)優(yōu)時(shí),以PostgreSQL為例,可使用EXPLAIN ANALYZE檢查SQL查詢(xún)計(jì)劃,從而方便的進(jìn)行性能調(diào)優(yōu)。而使用存儲(chǔ)過(guò)程時(shí),EXPLAIN ANALYZE無(wú)法顯示其內(nèi)部查詢(xún)計(jì)劃
- 不便于移植到其它數(shù)據(jù)庫(kù)。直接使用SQL時(shí),SQL存于應(yīng)用程序中,對(duì)大部分標(biāo)準(zhǔn)SQL而言,換用其它數(shù)據(jù)庫(kù)并不影響應(yīng)用程序的使用。而使用存儲(chǔ)過(guò)程時(shí),由于不同數(shù)據(jù)庫(kù)的存儲(chǔ)過(guò)程定義方式不同,支持的語(yǔ)言及語(yǔ)法不同,移植成本較高
存儲(chǔ)過(guò)程在PostgreSQL中的使用
PostgreSQL支持的過(guò)程語(yǔ)言
PostgreSQL官方支持PL/pgSQL,PL/Tcl,PL/Perl和PL/Python這幾種過(guò)程語(yǔ)言。同時(shí)還支持一些第三方提供的過(guò)程語(yǔ)言,如PL/Java,PL/PHP,PL/Py,PL/R,PL/Ruby,PL/Scheme,PL/sh。
基于SQL的存儲(chǔ)過(guò)程定義
CREATE OR REPLACE FUNCTION add(a INTEGER, b NUMERIC)
RETURNS NUMERIC
AS $$
SELECT a+b;
$$ LANGUAGE SQL;
調(diào)用方法
SELECT add(1,2);
add
-----
3
(1 row)
SELECT * FROM add(1,2);
add
-----
3
(1 row)
上面這種方式參數(shù)列表只包含函數(shù)輸入?yún)?shù),不包含輸出參數(shù)。下面這個(gè)例子將同時(shí)包含輸入?yún)?shù)和輸出參數(shù)
CREATE OR REPLACE FUNCTION plus_and_minus
(IN a INTEGER, IN b NUMERIC, OUT c NUMERIC, OUT d NUMERIC)
AS $$
SELECT a+b, a-b;
$$ LANGUAGE SQL;
調(diào)用方式
SELECT plus_and_minus(3,2);
add_and_minute
----------------
(5,1)
(1 row)
SELECT * FROM plus_and_minus(3,2);
c | d
---+---
5 | 1
(1 row)
該例中,IN代表輸入?yún)?shù),OUT代表輸出參數(shù)。這個(gè)帶輸出參數(shù)的函數(shù)和之前的add
函數(shù)并無(wú)本質(zhì)區(qū)別。事實(shí)上,輸出參數(shù)的最大價(jià)值在于它為函數(shù)提供了返回多個(gè)字段的途徑。
在函數(shù)定義中,可以寫(xiě)多個(gè)SQL語(yǔ)句,不一定是SELECT語(yǔ)句,可以是其它任意合法的SQL。但最后一條SQL必須是SELECT語(yǔ)句,并且該SQL的結(jié)果將作為該函數(shù)的輸出結(jié)果。
CREATE OR REPLACE FUNCTION plus_and_minus
(IN a INTEGER, IN b NUMERIC, OUT c NUMERIC, OUT d NUMERIC)
AS $$
SELECT a+b, a-b;
INSERT INTO test VALUES('test1');
SELECT a-b, a+b;
$$ LANGUAGE SQL;
其效果如下
SELECT * FROM plus_and_minus(5,3);
c | d
---+---
2 | 8
(1 row)
SELECT * FROM test;
a
-------
test1
(1 row)
基于PL/PgSQL的存儲(chǔ)過(guò)程定義
PL/pgSQL是一個(gè)塊結(jié)構(gòu)語(yǔ)言。函數(shù)定義的所有文本都必須是一個(gè)塊。一個(gè)塊用下面的方法定義:
[ <<label>> ]
[DECLARE
declarations]
BEGIN
statements
END [ label ];
- 中括號(hào)部分為可選部分
- 塊中的每一個(gè)declaration和每一條statement都由一個(gè)分號(hào)終止
- 塊支持嵌套,嵌套時(shí)子塊的END后面必須跟一個(gè)分號(hào),最外層的塊END后可不跟分號(hào)
- BEGIN后面不必也不能跟分號(hào)
- END后跟的label名必須和塊開(kāi)始時(shí)的標(biāo)簽名一致
- 所有關(guān)鍵字都不區(qū)分大小寫(xiě)。標(biāo)識(shí)符被隱含地轉(zhuǎn)換成小寫(xiě)字符,除非被雙引號(hào)包圍
- 聲明的變量在當(dāng)前塊及其子塊中有效,子塊開(kāi)始前可聲明并覆蓋(只在子塊內(nèi)覆蓋)外部塊的同名變量
- 變量被子塊中聲明的變量覆蓋時(shí),子塊可以通過(guò)外部塊的label訪問(wèn)外部塊的變量
聲明一個(gè)變量的語(yǔ)法如下:
name [ CONSTANT ] type [ NOT NULL ] [ { DEFAULT | := } expression ];
使用PL/PgSQL語(yǔ)言的函數(shù)定義如下:
CREATE FUNCTION somefunc() RETURNS integer AS $$
DECLARE
quantity integer := 30;
BEGIN
-- Prints 30
RAISE NOTICE 'Quantity here is %', quantity;
quantity := 50;
-- Create a subblock
DECLARE
quantity integer := 80;
BEGIN
-- Prints 80
RAISE NOTICE 'Quantity here is %', quantity;
-- Prints 50
RAISE NOTICE 'Outer quantity here is %', outerblock.quantity;
END;
-- Prints 50
RAISE NOTICE 'Quantity here is %', quantity;
RETURN quantity;
END;
$$ LANGUAGE plpgsql;
聲明函數(shù)參數(shù)
如果只指定輸入?yún)?shù)類(lèi)型,不指定參數(shù)名,則函數(shù)體里一般用$1,$n這樣的標(biāo)識(shí)符來(lái)使用參數(shù)。
CREATE OR REPLACE FUNCTION discount(NUMERIC)
RETURNS NUMERIC
AS $$
BEGIN
RETURN $1 * 0.8;
END;
$$ LANGUAGE PLPGSQL;
但該方法可讀性不好,此時(shí)可以為$n參數(shù)聲明別名,然后可以在函數(shù)體內(nèi)通過(guò)別名指向該參數(shù)值。
CREATE OR REPLACE FUNCTION discount(NUMERIC)
RETURNS NUMERIC
AS $$
DECLARE
total ALIAS FOR $1;
BEGIN
RETURN total * 0.8;
END;
$$ LANGUAGE PLPGSQL;
筆者認(rèn)為上述方法仍然不夠直觀,也不夠完美。幸好PostgreSQL提供另外一種更為直接的方法來(lái)聲明函數(shù)參數(shù),即在聲明參數(shù)類(lèi)型時(shí)同時(shí)聲明相應(yīng)的參數(shù)名。
CREATE OR REPLACE FUNCTION discount(total NUMERIC)
RETURNS NUMERIC
AS $$
BEGIN
RETURN total * 0.8;
END;
$$ LANGUAGE PLPGSQL;
返回多行或多列
使用自定義復(fù)合類(lèi)型返回一行多列
PostgreSQL除了支持自帶的類(lèi)型外,還支持用戶(hù)創(chuàng)建自定義類(lèi)型。在這里可以自定義一個(gè)復(fù)合類(lèi)型,并在函數(shù)中返回一個(gè)該復(fù)合類(lèi)型的值,從而實(shí)現(xiàn)返回一行多列。
CREATE TYPE compfoo AS (col1 INTEGER, col2 TEXT);
CREATE OR REPLACE FUNCTION getCompFoo
(in_col1 INTEGER, in_col2 TEXT)
RETURNS compfoo
AS $$
DECLARE result compfoo;
BEGIN
result.col1 := in_col1 * 2;
result.col2 := in_col2 || '_result';
RETURN result;
END;
$$ LANGUAGE PLPGSQL;
SELECT * FROM getCompFoo(1,'1');
col1 | col2
------+----------
2 | 1_result
(1 row)
使用輸出參數(shù)名返回一行多列
在聲明函數(shù)時(shí),除指定輸入?yún)?shù)名及類(lèi)型外,還可同時(shí)聲明輸出參數(shù)類(lèi)型及參數(shù)名。此時(shí)函數(shù)可以輸出一行多列。
CREATE OR REPLACE FUNCTION get2Col
(IN in_col1 INTEGER,IN in_col2 TEXT,
OUT out_col1 INTEGER, OUT out_col2 TEXT)
AS $$
BEGIN
out_col1 := in_col1 * 2;
out_col2 := in_col2 || '_result';
END;
$$ LANGUAGE PLPGSQL;
SELECT * FROM get2Col(1,'1');
out_col1 | out_col2
----------+----------
2 | 1_result
(1 row)
使用SETOF返回多行記錄
實(shí)際項(xiàng)目中,存儲(chǔ)過(guò)程經(jīng)常需要返回多行記錄,可以通過(guò)SETOF實(shí)現(xiàn)。
CREATE TYPE compfoo AS (col1 INTEGER, col2 TEXT);
CREATE OR REPLACE FUNCTION getSet(rows INTEGER)
RETURNS SETOF compfoo
AS $$
BEGIN
RETURN QUERY SELECT i * 2, i || '_text'
FROM generate_series(1, rows, 1) as t(i);
END;
$$ LANGUAGE PLPGSQL;
SELECT col1, col2 FROM getSet(2);
col1 | col2
------+--------
2 | 1_text
4 | 2_text
(2 rows)
本例返回的每一行記錄是復(fù)合類(lèi)型,該方法也可返回基本類(lèi)型的結(jié)果集,即多行一列。
使用RETURN TABLE返回多行多列
CREATE OR REPLACE FUNCTION getTable(rows INTEGER)
RETURNS TABLE(col1 INTEGER, col2 TEXT)
AS $$
BEGIN
RETURN QUERY SELECT i * 2, i || '_text'
FROM generate_series(1, rows, 1) as t(i);
END;
$$ LANGUAGE PLPGSQL;
SELECT col1, col2 FROM getTable(2);
col1 | col2
------+--------
2 | 1_text
4 | 2_text
(2 rows)
此時(shí)從函數(shù)中讀取字段就和從表或視圖中取字段一樣,可以看此種類(lèi)型的函數(shù)看成是帶參數(shù)的表或者視圖。
使用EXECUTE語(yǔ)句執(zhí)行動(dòng)態(tài)命令
有時(shí)在PL/pgSQL函數(shù)中需要生成動(dòng)態(tài)命令,這個(gè)命令將包括他們每次執(zhí)行時(shí)使用不同的表或者字符。EXECUTE語(yǔ)句用法如下:
EXECUTE command-string [ INTO [STRICT] target] [USING expression [, ...]];
此時(shí)PL/plSQL將不再緩存該命令的執(zhí)行計(jì)劃。相反,在該語(yǔ)句每次被執(zhí)行的時(shí)候,命令都會(huì)編譯一次。這也讓該語(yǔ)句獲得了對(duì)各種不同的字段甚至表進(jìn)行操作的能力。
command-string包含了要執(zhí)行的命令,它可以使用參數(shù)值,在命令中通過(guò)引用如$1,$2等來(lái)引用參數(shù)值。這些符號(hào)的值是指USING字句的值。這種方法對(duì)于在命令字符串中使用參數(shù)是最好的:它能避免運(yùn)行時(shí)數(shù)值從文本來(lái)回轉(zhuǎn)換,并且不容易產(chǎn)生SQL注入,而且它不需要引用或者轉(zhuǎn)義。
CREATE TABLE testExecute
AS
SELECT
i || '' AS a,
i AS b
FROM
generate_series(1, 10, 1) AS t(i);
CREATE OR REPLACE FUNCTION execute(filter TEXT)
RETURNS TABLE (a TEXT, b INTEGER)
AS $$
BEGIN
RETURN QUERY EXECUTE
'SELECT * FROM testExecute where a = $1'
USING filter;
END;
$$ LANGUAGE PLPGSQL;
SELECT * FROM execute('3');
a | b
---+---
3 | 3
(1 row)
SELECT * FROM execute('3'' or ''c''=''c');
a | b
---+---
(0 rows)
當(dāng)然,也可以使用字符串拼接的方式在command-string中使用參數(shù),但會(huì)有SQL注入的風(fēng)險(xiǎn)。
CREATE TABLE testExecute
AS
SELECT
i || '' AS a,
i AS b
FROM
generate_series(1, 10, 1) AS t(i);
CREATE OR REPLACE FUNCTION execute(filter TEXT)
RETURNS TABLE (a TEXT, b INTEGER)
AS $$
BEGIN
RETURN QUERY EXECUTE
'SELECT * FROM testExecute where b = '''
|| filter || '''';
END;
$$ LANGUAGE PLPGSQL;
SELECT * FROM execute(3);
a | b
---+---
3 | 3
(1 row)
SELECT * FROM execute('3'' or ''c''=''c');
a | b
----+----
1 | 1
2 | 2
3 | 3
4 | 4
5 | 5
6 | 6
7 | 7
8 | 8
9 | 9
10 | 10
(10 rows)
從該例中可以看出使用字符串拼接的方式在command-string中使用參數(shù)會(huì)引入SQL注入攻擊的風(fēng)險(xiǎn),而使用USING的方式則能有效避免這一風(fēng)險(xiǎn)。
PostgreSQL中的UDF與存儲(chǔ)過(guò)程
本文中并未區(qū)分PostgreSQL中的UDF和存儲(chǔ)過(guò)程。實(shí)際上PostgreSQL創(chuàng)建存儲(chǔ)與創(chuàng)建UDF的方式一樣,并沒(méi)有專(zhuān)用于創(chuàng)建存儲(chǔ)過(guò)程的語(yǔ)法,如CREATE PRECEDURE。在PostgreSQL官方文檔中也暫未找到這二者的區(qū)別。倒是從一些資料中找對(duì)了它們的對(duì)比,如下表如示,僅供參考。

多態(tài)SQL函數(shù)
SQL函數(shù)可以聲明為接受多態(tài)類(lèi)型(anyelement和anyarray)的參數(shù)或返回多態(tài)類(lèi)型的返回值。
- 函數(shù)參數(shù)和返回值均為多態(tài)類(lèi)型。其調(diào)用方式和調(diào)用其它類(lèi)型的SQL函數(shù)完全相同,只是在傳遞字符串類(lèi)型的參數(shù)時(shí),需要顯示轉(zhuǎn)換到目標(biāo)類(lèi)型,否則將會(huì)被視為unknown類(lèi)型。
CREATE OR REPLACE FUNCTION get_array(anyelement, anyelement)
RETURNS anyarray
AS $$
SELECT ARRAY[$1, $2];
$$ LANGUAGE SQL;
SELECT get_array(1,2), get_array('a'::text,'b'::text);
get_array | get_array
-----------+-----------
{1,2} | {a,b}
(1 row)
- 函數(shù)參數(shù)為多態(tài)類(lèi)型,而返回值為基本類(lèi)型
CREATE OR REPLACE FUNCTION is_greater(anyelement, anyelement)
RETURNS BOOLEAN
AS $$
SELECT $1 > $2;
$$ LANGUAGE SQL;
SELECT is_greater(7.0, 4.5);
is_greater
------------
t
(1 row)
SELECT is_greater(2, 4);
is_greater
------------
f
(1 row)
- 輸入輸出參數(shù)均為多態(tài)類(lèi)型。這種情況與第一種情況一樣。
CREATE OR REPLACE FUNCTION get_array
(IN anyelement, IN anyelement, OUT anyelement, OUT anyarray)
AS $$
SELECT $1, ARRAY[$1, $2];
$$ LANGUAGE SQL;
SELECT get_array(4,5), get_array('c'::text, 'd'::text);
get_array | get_array
-------------+-------------
(4,"{4,5}") | (c,"{c,d}")
(1 row)
函數(shù)重載(Overwrite)
在PostgreSQL中,多個(gè)函數(shù)可共用同一個(gè)函數(shù)名,但它們的參數(shù)必須得不同。這一規(guī)則與面向?qū)ο笳Z(yǔ)言(比如Java)中的函數(shù)重載類(lèi)似。也正因如此,在PostgreSQL刪除函數(shù)時(shí),必須指定其參數(shù)列表,如:
DROP FUNCTION get_array(anyelement, anyelement);
另外,在實(shí)際項(xiàng)目中,經(jīng)常會(huì)用到CREATE OR REPLACE FUNCTION去替換已有的函數(shù)實(shí)現(xiàn)。如果同名函數(shù)已存在,但輸入?yún)?shù)列表不同,會(huì)創(chuàng)建同名的函數(shù),也即重載。如果同名函數(shù)已存在,且輸入輸出參數(shù)列表均相同,則替換。如果已有的函數(shù)輸入?yún)?shù)列表相同,但輸出參數(shù)列表不同,則會(huì)報(bào)錯(cuò),并提示需要先DROP已有的函數(shù)定義。