# 我所理解的 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 |