# 我所理解的 PostgreSQL 05: PL/pgSQL 入門 {%hackmd @moment89/tXJIcGqmSI6_fJQp2hnJUA %} ## PL/pgSQL 是什麼? 和 SQL 的差異? PL/pgSQL (Procedural Language/PostgreSQL) 是一種 [Procedural Language](https://www.postgresql.org/docs/current/xplang.html),是 PostgreSQL 內建的語言,用來撰寫具有邏輯控制能力的 Function、Stored Procedure 與 Trigger Function 等。 相比於一般 SQL 指令,PL/pgSQL 更接近程式語言一些,它支援以下功能,彌補了 SQL 在流程控制與彈性邏輯處理上的不足。 * 變數宣告與賦值 * 條件判斷 * 迴圈控制 * 錯誤處理 * 複雜流程控制與內部邏輯封裝 ## 寫 PL/pgSQL,你可能會想知道的小細節 撰寫 Function、Stored Procedure、Trigger Function 的結構都很類似,大致如下。 ```sql= CREATE {物件類型} {物件名稱} ( {輸入變數} ) RETURNS {回傳內容} -------------------- Function 可依需求回傳資料值或 Table;Trigger Function 一律回傳 Trigger;Stored Procedure 沒有 RETURNS LANGUAGE '{Procedural Language}' ----- 宣告所使用的 Procedural Language SECURITY {INVOKER/DEFINER} ----------- 預設是 SECURITY INVOKER,以執行者的身份執行邏輯。改為SECURITY DEFINER則是以建立者的身份執行邏輯 AS $BODY$ DECLARE ------------------------------ 宣告內部變數區塊,但 PL/pgSQL 才有此區塊 {宣告變數} BEGIN {處理邏輯} ------------------------ 用所選擇的 Procedural Language 撰寫處理邏輯 END; $BODY$; ``` 結構中的 `$BODY$` 到 `$BODY$` 區塊就是 PL/pgSQL Block Structure (區塊結構)。 ### `$BODY$` 是什麼? 在 PostgreSQL 中,要建立一個 Function、Stored Procedure 與 Trigger Function 時,需要在 SQL 裡面寫==一整段 PL/pgSQL 程式碼==,但因為這段程式碼勢必會包含許多特殊字如分號、引號等,會讓 SQL 的語法解讀混亂。 所以 PostgreSQL 用自訂分隔符 (Delimited String Constant) 的方式讓我們能包裹整段程式碼,避免字串中出現的符號被 SQL 誤會。 PostgreSQL 提供的自訂分隔符是用美元符號包裝 (Dollar-Quoted String Constant),方法就是用美元符號命名一個==標籤==,可以直接用 `$$`,也可以替它命名如 `$BODY$`、`$body$`,但務必要前後一致,起訖標籤必須完全相同 (大小寫敏感)。 ``` $$ ... $$ $function$ ... $function$ $abc123$ ... $abc123$ ``` 美元符號包裝和傳統的單引號字串包裝不同,它==不需要跳脫字元,而且支援嵌套==。 PostgreSQL 在語法層面,會把遇到的第一個「起始標籤」到下一個完全相同的「結束標籤」視為同一個字串常數。而我們可以利用不同的標籤名稱,在外層程式區塊內再寫字串常數,內層用另一個標籤包起來,就會看起來就像巢狀一樣。 ``` $AAA$ ...    $aaa$ ...    $aaa$ ... $AAA$ ``` #### 小結 * 何時需要用自訂分隔符方式?當需要在 `CREATE FUNCTION` / `CREATE PROCEDURE` / `DO` 區塊裡面寫一大段 Procedural Languages 邏輯時 (尤其是多段邏輯時),就建議用自訂分隔符包起來。  * 起訖的自訂分隔符必須完全相同 (大小寫敏感,``$BODY$`` ≠ ``$body$``)。 * 外層自訂分隔符與內層自訂分隔符只要命名不同,就不會互相干擾。 * 自訂分隔符方式建議不要直接用 `$$`,若資料處理邏輯中剛好有需要寫到 '$$' 字串,會導致邏輯意外被提前結束。 ### PL/pgSQL Block Structure (區塊結構) 結構很單純,就只有兩個區塊。 `DECLARE` 是選擇性的,只有需要使用變數時才需要這個區塊。 `BEGIN` ... `END` 是必要的主邏輯區塊,裡面可以撰寫流程控制邏輯、條件判斷、查詢、錯誤處理等。 ``` [DECLARE ------------ 變數宣告區塊 (Optional) ] BEGIN ------------ 主邏輯區塊 END; ``` #### 📌DECLARE 變數宣告區塊 PL/pgSQL 可使用 `DECLARE` 區塊來宣告變數,這是與 SQL 最大差異之一。 宣告變數的方法很簡單,以下範例是宣告一個 `integer` 型別的變數 `i_counter`,賦予初始值 0,以及一個 `varchar` 型別的變數 `vc_prefix`,沒有初始值。 賦值的方式,用 `:=` 和 `=` 是一樣的,都可以運作,但習慣上會使用前者。 ```sql= DECLARE i_counter integer := 0; vc_prefix varchar; ``` #### 📌BEGIN ~ END 主邏輯區塊 此區塊用於撰寫邏輯,我們接著剛剛的範例繼續寫。 ```sql= DECLARE i_counter integer := 0; vc_prefix varchar; BEGIN RAISE INFO 'Week of year = %', extract(week from current_date); RAISE INFO 'Day of week = %', extract(dow from current_date); vc_prefix := concat('W', extract(week from current_date), '_'); WHILE i_counter < extract(dow from current_date) LOOP RAISE INFO '%Day % is %', vc_prefix, i_counter + 1, current_date - extract(dow from current_date)::int + i_counter + 1; i_counter := i_counter + 1; END LOOP; END; ``` 好,寫完邏輯了! 但你可能會發現,這段邏輯不能直接在 PostgreSQL 查詢中執行,會顯示語法錯誤。 ![image](https://hackmd.io/_uploads/rkTx_w__lx.png) 就如同前面介紹的,這是一段 PL/pgSQL 區塊程式碼,但我們直接拿去執行的話 PostgreSQL 不知道你是要執行哪種語言。 我們需要先將它用前面提到的自訂分隔符包裹,並利用 `DO` 語句來執行它,`DO` 語句預設會用 PL/pgSQL 去解析所執行的 Anonymous Code Block。 ```sql DO $BODY$ DECLARE i_counter integer := 0; vc_prefix varchar; BEGIN RAISE INFO 'Week of year = %', extract(week from current_date); RAISE INFO 'Day of week = %', extract(dow from current_date); vc_prefix := concat('W', extract(week from current_date), '_'); WHILE i_counter < extract(dow from current_date) LOOP RAISE INFO '%Day % is %', vc_prefix, i_counter + 1, current_date - extract(dow from current_date)::int + i_counter + 1; i_counter := i_counter + 1; END LOOP; END; $BODY$; ``` ![image](https://hackmd.io/_uploads/ryEEuDOOxg.png) ### 如何測試、Debug? 剛剛介紹的 `DO` 指令是拿來測試 Function、 Procedure 的好工具! [`DO` 的用途](https://www.postgresql.org/docs/current/sql-do.html)是執行一段 Anonymous Code Block。 >[!NOTE] Anonymous Code Block >指一段沒有名稱、沒有被包裝成 Function 或 Procedure 的 PL/pgSQL 區塊邏輯。 >這種區塊==沒有函式名稱、也沒有輸入參數或回傳值,就是單純執行一段邏輯==。 Debug 時,就可以利用 `DO` 語法搭配前一章節所介紹的 Temp Table,可以幫助我們確認過程中每一段 SQL 產生的資料。 或搭配 `RAISE` 語法,可以幫助我們確認處理過程中的一些資訊。(可以類比為 Python 的 Print、C# 的 Console.WriteLine 用法) 我們以上一章的測試資料為基礎,建立一個簡單的 Function 來 Demo。 ```sql= CREATE OR REPLACE FUNCTION test.get_product_distribution( in_i_customer_level int ) RETURNS TABLE ( product_id bigint, product_name varchar(100), total_quantity bigint ) LANGUAGE plpgsql AS $BODY$ BEGIN DROP TABLE IF EXISTS tt_super_customer_orders; CREATE TEMP TABLE tt_super_customer_orders AS SELECT order_id FROM test.orders WHERE customer_id IN ( SELECT customer_id FROM test.customers WHERE customer_level = in_i_customer_level ); DROP TABLE IF EXISTS tt_total_quantity; CREATE TEMP TABLE tt_total_quantity AS SELECT oi.product_id, sum(oi.quantity) AS total_quantity FROM test.order_info AS oi JOIN tt_super_customer_orders AS o ON oi.order_id = o.order_id GROUP BY oi.product_id ORDER BY product_id; RETURN QUERY SELECT p.product_id, p.product_name, t.total_quantity FROM tt_total_quantity AS t JOIN test.products AS p ON t.product_id = p.product_id; END; $BODY$; ``` <center class="half"> <img src=https://hackmd.io/_uploads/SkdWnvOuxl.png width="500"/> </center> #### 不需要使用 `DO` 的情況 先來操作比較簡單的情況。 如果是測試 Function,因為不會異動資料庫,可以直接執行 Function 之後查詢其中使用的 Temp Table。 1. 直接查詢 Function。 <center class="half"> <img src=https://hackmd.io/_uploads/BJIEnDOdxx.png width="500"/> </center> 2. 切換到 Messages Tab,會看到此 Function 過程中建立了哪些 Temp Table。 <center class="half"> <img src=https://hackmd.io/_uploads/ryYD3vuugl.png width="500"/> </center> 3. 查詢該 Temp Table,可以看到其內容用於驗算、Debug。 <center class="half"> <img src=https://hackmd.io/_uploads/HkI52DOdex.png width="500"/> </center> #### 使用 `DO` 的情況 1. 把 Function SQL 先複製到自己慣用的文字編輯器裡。 <center class="half"> <img src=https://hackmd.io/_uploads/SJo76vd_lx.png width="500"/> </center> 2. 把 input 參數搬到 `DECLARE` 區塊,並賦予測試數值;把 `$BODY$ DECLARE` 以前的內容都移除,改成 `DO`。 <center class="half"> <img src=https://hackmd.io/_uploads/B1wcpwO_ex.png width="500"/> </center> 3. 畫面拉到最下方,將 `RETURN QUERY` 改為建立一張名為 `tt_return` 的 Temp Table。 ```sql DROP TABLE IF EXISTS tt_return; CREATE TEMP TABLE tt_return AS ``` <center class="half"> <img src=https://hackmd.io/_uploads/rJDATDu_eg.png width="500"/> </center> 5. 拿這段 SQL 去執行。 <center class="half"> <img src=https://hackmd.io/_uploads/B1T7CP_Oeg.png width="500"/> </center> 6. 可以用 Temp Table 做資料驗證。 <center class="half"> <img src=https://hackmd.io/_uploads/HkKwRPuulg.png width="500"/> </center> #### 嘗試紀錄每一個階段的完成時間並輸出 1. 在想要紀錄時間點的地方加上 `RAISE` 語句。 ```sql RAISE INFO 'Do something at %', clock_timestamp(); ``` 注意,此處建議使用 `clock_timestamp()` 而非 `CURRENT_TIMESTAMP`、`now()`。 >[!Note] 差異? > * `CURRENT_TIMESTAMP` 和 `now()` 相等,是當前 Transaction 的開始時間。 > * `clock_timestamp()` 是真正的當前時間。 > > 可以嘗試看看改成用 `CURRENT_TIMESTAMP` 和 `now()`,會發現在每一行輸出的時間都一模一樣。 <center class="half"> <img src=https://hackmd.io/_uploads/B1haAD_uex.png width="500"/> </center> 2. 執行它,就可以在 Messages Tab 看到每一個時間點,知道效能瓶頸在哪囉。 <center class="half"> <img src=https://hackmd.io/_uploads/HkkgJuuulg.png width="500"/> </center> --- 本篇介紹了 PL/pgSQL 的基礎概念,並附上了我自己當初在認識 PL/pgSQL 時有的疑問 (ex. `$BODY$`) 以及如何搭配前一篇介紹的 Temp Table 做測試驗證,希望能幫助同樣有疑問的讀者~