---
# System prepended metadata

title: '我所理解的 PostgreSQL 02: 資料庫物件有哪些? 該在什麼情境下使用?'
tags: [PostgreSQL, 我所理解的 PostgreSQL]

---

# 我所理解的 PostgreSQL 02: 資料庫物件有哪些? 該在什麼情境下使用?

{%hackmd @moment89/tXJIcGqmSI6_fJQp2hnJUA %}

---

在 PostgreSQL 中，資料庫物件 (Database Objects) 泛指儲存在資料庫中的結構性元素，Database、Schema、User、Table ... 等所有東西都是資料庫物件，它們都有唯一的 object id。

這節旨在介紹常用的資料庫物件，希望閱讀後會知道資料庫有哪些工具可以利用!

:::success
💡在 PostgreSQL 中，物件名稱預設為大小寫不敏感，系統會自動將未加雙引號的名稱轉為小寫儲存。
也就是說，即使你使用駝峰命名（例如 `MyTable`），資料庫實際會以 `mytable` 儲存。
若想保留大小寫，必須使用雙引號括住物件名稱（如 `"MyTable"`），但這會導致查詢時也需精確使用大小寫與雙引號。
:::

## [Schema](https://www.postgresql.org/docs/current/ddl-schemas.html)

可以理解為資料庫版本的命名空間 (類比 C# 的 namespace)，用來分類與管理物件。

一個 Database 下可以有多個 Schema。
Schema 和 Database 不同的是，==Database 之間是不直接互通的，但 Database 下的 Schema 之間是直接互通的==。
也就是我對單一 Database 連線，只要是我有訪問權限的 Schema，我可以在一個連線中訪問這些不同的 Schema 下的物件。

==不同 Schema 下可以有相同名稱的資料庫物件==。
例如我可以在 A Schema 下有 table_abc，在 B Schema 也下有 table_abc，這並不會衝突，它們就是兩個定義於不同 Schema 下的獨立物件。

### public Schema 是什麼?

透過 pgAdmin 左側導覽列，會發現每個 Database 下的 Schema 清單中，一定有一個名為 public 的 Schema。

<center class="half">
    <img src=https://hackmd.io/_uploads/BJKcH8Lwgg.png width="200"/>
</center>

在資料庫預設參數中，有一項 `search_path` 參數，預設值為 `"$user", public`。

<center class="half">
    <img src=https://hackmd.io/_uploads/SJFdLL8Plg.png width="200"/>
</center>

這項參數的作用是定義「==當一個物件未提供 Schema 名稱時預設的查詢路徑。==」

而預設值 `"$user", public` 就表示，當我們查詢物件沒有指定 Schema 名稱，它會看看有沒有跟當前使用者名稱一樣的 Schema (`$user` 會自動取代為當前使用者的名稱)、在該 Schema 下查找物件，若沒有則在 public 下查找物件。

故在應用層面
1. 若在開發上有將 schema 和 user 名稱一致的習慣，而某一物件在每個 schema 下都存在，則開發時可以利用這個特性避免重複的 `SELECT * FROM {different schema name}.same_table_name;` 語句。
2. ==若有希望能在查詢時省略 schema name、提供給 Database 下所有使用者都能用的共用資料庫物件，可以考慮建立在 public Schema==。
    例如時間轉換、單純數值換算等功能性的物件。
3. 安裝 Extension，大多數 Extension 的功能函式預設都會放在 public Schema 下。
    舉例來說，新建一個 test 資料庫，原本該資料庫的 public Schema 下沒有任何 Function (左圖)；在隨意安裝了一個 Extension，會看到同時新增了很多 Function (右圖)。

<center class="half">
    <img src=https://hackmd.io/_uploads/S11NvUUvee.png height="400"/>
    <img src=https://hackmd.io/_uploads/rkUbOLLvle.png height="400"/>
</center>
    
### 什麼時候該建立 Schema?

Schema 的使用沒有絕對規則或最佳實踐，主要依據實際應用需求與團隊偏好進行規劃即可。

希望能切分不同功能模組的資料庫物件以便管理 → ✅可以依模組分配 Schema，例如人員組織功能一個 Schema、設備資訊功能一個 Schema。

希望能依資料新舊管理 → ✅可以有用於存放即時資料 Schema 搭配存放歷史資料 Schema，前後者的資料表結構一致、用使用者決定可拿取即時資料還是歷史資料。

甚至有一派別是不使用多個 Schema，而是使用多個 Database 來確保資料隔絕。

## [Table](https://www.postgresql.org/docs/current/ddl-basics.html)

這應該是每個人對資料庫的第一印象。

可以想像成 Excel 表格，有標頭也有資料，標頭通常是定義好的，資料列則是可變動的，可能會增加、減少。

:::spoiler 補充說明
資料表的實體資料會儲存在磁碟上的一種稱為 Heap 的儲存結構中，對應的檔案位於 PostgreSQL 的資料目錄中。可以透過系統目錄 (System Catalog) 查詢表格的 object ID，再對應至資料夾中的實體檔案。若檔案過大，系統可能會將其拆分為多個檔案。

舉例來說，可透過系統資訊表 pg_database 找到資料庫的 oid、透過 pg_class 找到資料表的 oid。
![image](https://hackmd.io/_uploads/BJFSYLUPle.png)
在 base 資料夾下，可看到該資料庫 oid 的資料夾。
![image](https://hackmd.io/_uploads/SyxOtIIDxl.png)
在該資料庫 oid 資料夾下可看到該物件的實體檔案。
![image](https://hackmd.io/_uploads/rJEFt8Ivxl.png)
:::


建立資料表的基本語句就是 `CREATE TABLE`，如下圖，可以建立一個沒有任何欄位的空 Table。(當然，這種 Table 無法儲存資料，對其做查詢也查不出資料。)

<center class="half">
    <img src=https://hackmd.io/_uploads/rJmG9IUvgx.png width="400"/>
    <img src=https://hackmd.io/_uploads/SJA758LPeg.png width="400"/>
</center>

我們當然不希望建立一張什麼都沒有的表，需要定義這個表的子物件。

以下介紹較常用的子物件 (指需要依附於物件如 Table、View、Materialized View 之上的物件)。

### Column

可以理解為 Excel 表格的標頭，用於定義欄位的名稱的型別。

欄位型別的定義可以==限制插入表格的資料格式，避免不符合預期的資料寫入==，例如我可以限制 A 欄位只能塞入數字、B 欄位只能塞入長度20以下的字串、C 欄位可以為空、D 欄位要有預設值 ... 等等。

PostgreSQL 支援的型別很多，除了基本的字串、數字，也支援 `json`、`jsonb`、`array` 型別，原生支援的型別清單可參考[官方文檔](https://www.postgresql.org/docs/current/datatype.html)。此外也可以[自訂型別](https://www.postgresql.org/docs/current/sql-createtype.html)，例如枚舉型別。

### [Constraint](https://www.postgresql.org/docs/current/ddl-constraints.html)

雖然 Column 的型別提供了一定程度的資料約束，但若有需要實現更細緻的約束，可以應用 Constraint。

1. Primary Key
    * 限制==某個欄位==或==某一群欄位組合==在整個資料表中必須是唯一值且不可為空。
        這相當於 Unique 約束和欄位 not null 約束的疊加。
    * 此約束會自動建立索引。
        在 pgAdmin 左側導覽列看不到，但可在 System Catalog 提供的系統資訊表 pg_index 查到。
        
    <center class="half">
        <img src=https://hackmd.io/_uploads/rkgY1lPDxe.pngg width="400"/>
    </center>

    * 一張表只能有一個 Primary Key 約束。(關聯式資料庫的標準定義)
2. Foreign Key
    * 限制==某個欄位==或==某一群欄位組合==並須存在於所參照 `REFERENCES` 的另一張表中，且該欄位或該群欄位組合必須是所參照的表的 Primary Key。
    * 這項約束主要用於確保資料一致性，可以設定當所參考的資料被刪除、更新時的行為。(我認為這是 Foreign Key 的核心價值，如果是使用預設的 `NO ACTION`，對資料一致性的幫助有限，因此使用外鍵時建議搭配適當的 `ON DELETE` 或 `ON UPDATE` 行為設定，以發揮其完整效用。)
    * 注意，這和 Primary Key、Unique 約束不同，不會自動建立索引。
3. Unique
    * 限制==某個欄位==或==某一群欄位組合==在資料表中必須是唯一值，但空值的資料列不受限制。
    * 此約束會自動建立索引。
        在 pgAdmin 左側導覽列看不到，但可在 System Catalog 提供的系統資訊表查到。
4. Check
    * 可以設定欄位必須符合某一個 boolean 條件式的判斷，例如 A 欄位必須大於 0、B 欄位必須大於 C 欄位等。

### [Trigger](https://www.postgresql.org/docs/current/sql-createtrigger.html)

設定在對資料表執行特定操作的==之前==或==之後==自動觸發執行指定的函式 (Trigger Function)。

Trigger 的觸發頻率可以設定為 `FOR EACH ROW` 或 `FOR EACH STATEMENT`。

* `FOR EACH ROW` 表示==每一筆受影響的資料列都會觸發一次 Trigger Function==。
    例如: 設定在對 A Table 每 `INSERT` 一筆資料之前，觸發 Trigger Function 去 B Table 查 C 欄位資料值一併寫入 A Table。
* `FOR EACH STATEMENT` 表示==只要操作符合條件，不論此次操作影響多少筆資料，Trigger Function 只會執行一次==。通常用於記錄操作行為或統一處理。
    例如: 設定當對 A 資料表執行 `UPDATE` 操作時，不論更新幾筆資料，只觸發一次 Trigger，在 B 表中紀錄此次異動行為。

### [Partition](https://www.postgresql.org/docs/current/ddl-partitioning.html)

資料表分區是指==將一個大型資料表依照某個條件 (稱為 Partition Key) 切分成多個子表 (Partition)==，在結構上這些 Partition 是附屬於主表 (Partitioned Table) 的子物件。

當對主資料表 (Partitioned Table) 執行 DML 操作 (如 `INSERT`、`UPDATE`、`DELETE`) 時，PostgreSQL 會根據 Partition Key 的值，將資料導向對應的 Partition 子表。

在累積大量資料的時候使用 Partition 可以加速查詢效率，特別是在條件過濾 (如 `WHERE time BETWEEN...` ) 明確時。

💡注意事項
1. 若一張 Partitioned Table 下的 Partition 數量過多，查詢規劃器在查詢規劃階段會面臨額外成本，反而造成效能瓶頸。
2. 在對 Partitioned Table 插入資料時，如果相應範圍的 Partition 不存在或者沒有配置 Default Partition，是會報錯的!
3. PostgreSQL 並未內建自動建立或清除 Partition 的機制 (例如根據時間自動建立每日分區)，需透過第三方套件如 pg_partman，或自行撰寫維護腳本來實現。

## [Index](https://www.postgresql.org/docs/current/indexes.html)

Index 可以建立在 Table 或 Materialized View 等資料結構上，為==某個欄位==或==某一群欄位組合==建立 index，用於加速查詢效率。

### Index 運作機制

一般來說，`WHERE` 條件在沒有 Index 的情況下，資料庫會將表中所有資料列依序載入記憶體，逐行比對是否符合查詢條件，這稱為「全表掃描 (Sequential Scan)」。

而 Index 則是一種==額外的資料結構，它獨立於資料表==。

該檔案紀錄了欲取得的資料位在 heap file 中的實體位置 block / index (可以理解為資料的住址，例如 A 資料位在 block 2 的 index 5)，故查詢時只要將指定 block 的資料載入到記憶體即可。

PostgreSQL 預設使用 B-tree 結構來建立 Index，會將目標欄位的值排序後儲存成一棵平衡搜尋樹，查詢時依據值大小決定是向左還是向右子節點繼續搜尋，最終導引至資料所在位置。

### Index 副作用

由於每個 Index 都是獨立儲存的檔案，因此會額外占用磁碟空間。

假設 Table 本身 1GB，除了 Primary Key 還另外建有一個 index，可能各占 0.5GB，那此資料表的 Table + Index 就佔了 2GB。

此外，每當資料表發生 `INSERT`、`UPDATE` 或 `DELETE` 操作時，系統也必須同步更新相關 Index 結構，故會增加寫入成本，若是資料寫入頻繁的應用可能需要評估是否可接受。

## [View](https://www.postgresql.org/docs/current/sql-createview.html)

==相當於把一段 SQL 包裝成捷徑==，每次查詢 View 系統都會重新執行該 SQL 查詢，並即時回傳結果。

若有需要重複使用、邏輯稍微複雜或具通用性的查詢語句，可以考慮定義為 View，提升可讀性與維護性。

## [Materialized View](https://www.postgresql.org/docs/current/sql-creatematerializedview.html)

Materialized View 類似於 View，但不同的是，它==會將查詢結果「實際儲存」為資料檔案==，因此不需每次查詢都重新執行 SQL，可提升查詢效能，且支援建立 Index。

建立 Materialized View 時，系統會立即執行查詢語句，並將結果儲存。日後若來源資料異動，需透過 `REFRESH MATERIALIZED VIEW` 指令主動更新內容。

|  | Materialized View | View |
| -------- | -------- | -------- |
|優點|料存為實體表、可以建立索引。|資料即時，無需手動更新。|
|缺點|來源資料異動不會即時反映，需手動刷新。|每次查詢都需重新執行 SQL，效能依查詢複雜度及來源資料量而定。|
|適合情境|資料更新頻率低、重複查詢同一批資料的情況。|需要即時資料，或邏輯複雜但可即時執行的查詢。|

## [Function](https://www.postgresql.org/docs/current/sql-createfunction.html)

用來封裝查詢邏輯，有 input、inout、output 介面。

通常來說，Function 不建議對資料 `INSERT`、`UPDATE`、`DELETE` 的操作，而是用來==執行查詢與邏輯判斷，並回傳計算結果==。
:::success
畢竟使用 Function 的方式是 `SELECT` 它，`SELECT` 字面意義是查詢，如果執行查詢動作卻會異動資料，對 Function 使用者來說有點反直覺。
:::

要注意設定 Function 時須評估所使用的函式易變性類型，以便查詢優化器正確評估其執行時機與結果快取可能性，進而提升查詢效能。➡️ [函式易變性類型介紹](https://hackmd.io/@moment89/FunctionVolatilityCategories)

Function 可使用 SQL 語法或 Procedural Languages (通常是 PL/pgSQL) 編寫。 

## [Stored Procedure](https://www.postgresql.org/docs/current/sql-createprocedure.html)

用來封裝查詢邏輯，差異在於 Procedure 支援執行異動資料的行為，但不支援直接回傳查詢結果 (沒有 `RETURN` 值)。

Procedure 通常是==執行會異動資料庫內容的操作==，例如定時將資料處理後存到另一張表，或者執行資料庫維護指令。

常見用途如
* 對多張資料表執行複雜異動流程。
* 排程作業中，定時處理資料。
* 執行資料清理、統計等維護操作。

Stored Procedure 可使用 SQL 語法或 Procedural Languages (通常是 PL/pgSQL) 編寫。 

## [Trigger Function](https://www.postgresql.org/docs/current/plpgsql-trigger.html)

Table 上綁定的 Trigger 本身不能直接執行邏輯，需綁定一個 Trigger Function 作為 Callback Function。

適合用於==資料表發生異動瞬間進行自動邏輯處理==，如驗證、補值等。

Trigger Function 可使用 SQL 語法或 Procedural Languages (通常是 PL/pgSQL) 編寫。

撰寫語法與一般 Function 類似，但需配合特殊變數使用，以 PL/pgSQL 舉例:
* `NEW`: 代表新資料列（適用於 `INSERT`、`UPDATE`）
* `OLD`: 代表舊資料列（適用於 `UPDATE`、`DELETE`）
* `TG_*` 系統變數：一系列 TG 開頭的變數可用於得 Trigger、來源表相關資訊。

## 個人建議

以下分享我個人在選擇使用物件的大致思路。

* 希望在資料進入資料表的瞬間進行同步加工處理 ➡️ Trigger 與 Trigger Function
* 處理邏輯需要傳遞變數或處理流程較動態 ➡️ Function 或 Stored Procedure
* 查詢重複性高且來源資料量小 ➡️ View
* 查詢重複性高且來源資料量大、即時性要求低 ➡️ Materialized View

| 資料來源量級 | 處理邏輯複雜度 | 即時性要求 | 需要輸入參數 | 是否異動資料 | 建議選項 |
| -- | -- | -- | -- | -- | -- |
| 無限制 | 高 | 無限制 | 無限制 | ==是== | Stored Procedure |
| ==少== | ==低== | 高 | 否 | 否 | View |
| 中等以上 | 低 | ==低== | 否 | 否 | Materialized View |
| 無限制 | 高 | 高 | ==是== | 否 | Function |

