---
# System prepended metadata

title: '我所理解的 PostgreSQL 05: PL/pgSQL 入門'
tags: [PostgreSQL, 我所理解的 PostgreSQL]

---

# 我所理解的 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 做測試驗證，希望能幫助同樣有疑問的讀者～

