說明關聯模型(relational model)為何能把「資料怎麼存」與「資料怎麼問」分離,並解釋 Codd 的資料獨立性。
精確使用關聯、元組、屬性、鍵、外鍵與完整性限制等術語。
利用函數相依(functional dependency)判斷正規化程度,並完成無損分解。
以關聯代數(\(\sigma\)、\(\pi\)、\(\bowtie\)、\(\cup\)、\(-\)、\(\rho\) 等)改寫 SQL 查詢,為第 6 章的查詢最佳化打底。
區分關聯代數(程序式)與關聯計算(宣告式),並說明 SQL 的邏輯處理順序。
比較 OLTP 與 OLAP 工作負載,理解它們為何導致截然不同的儲存與執行設計。
從檔案到關聯模型
1970 年 E. F. Codd 在〈A Relational Model of Data for Large Shared Data Banks〉中提出: 應用程式不應該知道資料在機器內部怎麼排列。當時主流的階層式(IMS)與網狀式(CODASYL)資料庫, 程式必須沿著指標「導航」才能取得資料,一旦實體佈局改變,程式就得重寫。 Codd 的答案是把資料抽象成數學上的關聯(集合論中的 relation), 讓使用者只描述「要什麼」,由系統決定「怎麼拿」——這個分離正是今天查詢最佳化器存在的理由。
邏輯資料獨立性:改變邏輯綱要(新增欄位、拆表)不影響既有查詢(透過 view 隔離)。
實體資料獨立性:改變實體儲存(加索引、換儲存引擎、分割表)不改變查詢結果,只改變速度。
本教材第 2–6 章討論的一切(頁面、B+Tree、緩衝池、執行計畫)都藏在實體層; 關聯模型的威力在於你不必知道它們也能寫出正確的查詢——但要寫出快的查詢,就必須知道。
關聯模型的基本元素
關聯、元組與屬性
:在數學上是若干定義域(domain)笛卡兒積的一個子集, \(r \subseteq D_1 \times D_2 \times \cdots \times D_n\);在 SQL 中對應表(table)。
:關聯中的一個元素 \((v_1, \ldots, v_n)\);對應列(row)。
:關聯綱要中的一個命名欄位及其定義域;對應欄(column)。
:\(R(A_1\!:\!D_1, \ldots, A_n\!:\!D_n)\),是「型別」; 是某一時刻的實際內容,是「值」。
:屬性個數;:元組個數——後者是最佳化器估算成本的核心數字。
純數學的關聯是集合:沒有重複元組、沒有順序。SQL 的表則是多重集合(bag),允許重複、 且 不加 時順序未定義。這個差異在第 6 章討論等價規則時很重要: 集合代數的某些等價式在 bag 語意下並不成立。
鍵
:能唯一識別元組的屬性集合。
:極小的超鍵(拿掉任何屬性就不再唯一)。
:從候選鍵中選出的一個,作為預設識別方式;在實務上常是人工代理鍵(surrogate key,如 )。
:關聯 \(R\) 的屬性集合,其值必須出現在關聯 \(S\) 的候選鍵中——這就是參照完整性。
在 MySQL/InnoDB 中,表資料就是依主鍵排序的 B+Tree(叢集索引,見第 3 章); 以隨機 UUIDv4 當主鍵會讓插入散佈到整棵樹,造成大量頁分裂與緩衝池抖動。 PostgreSQL 的表是 heap,主鍵只是「唯一索引+NOT NULL 限制」,隨機主鍵的代價較小,但索引本身一樣會膨脹。 第 2、3 章會解釋背後的機制。
完整性限制
llP7.2cm 限制 & SQL 寫法 & 意義
定義域限制 & 型別、 & 屬性值必須屬於定義域
鍵限制 & 、 & 候選鍵唯一
實體完整性 & 主鍵隱含 & 主鍵不可為 NULL
參照完整性 & & 外鍵值必須存在於被參照表(可搭配 )
一般斷言 & 、觸發器、 & 跨列或跨表的商業規則
CREATE TABLE departments (
dept_id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE employees (
emp_id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
age INT CHECK (age BETWEEN 16 AND 100),
salary NUMERIC(12,2) NOT NULL,
dept_id INT NOT NULL REFERENCES departments(dept_id) ON DELETE RESTRICT
);
函數相依與正規化
函數相依
屬性集合 \(X\) 函數決定 \(Y\)(記作 \(X \to Y\)),意思是:在任何合法實例中, 兩個元組若在 \(X\) 上相等,則在 \(Y\) 上也相等。候選鍵 \(K\) 的定義就是 \(K \to R\)(\(K\) 決定所有屬性)且極小。
Armstrong 公理(完備且正確):
自反律(reflexivity):若 \(Y \subseteq X\),則 \(X \to Y\)。
增廣律(augmentation):若 \(X \to Y\),則 \(XZ \to YZ\)。
遞移律(transitivity):若 \(X \to Y\) 且 \(Y \to Z\),則 \(X \to Z\)。
屬性集合 \(X\) 的閉包 \(X^+\) 是由 \(F\) 推得、所有 \(X\) 能決定的屬性。計算閉包是判斷鍵與正規式的基本工具:
closure(X, F):
result = X
repeat
for each (A -> B) in F:
if A ⊆ result then result = result ∪ B
until result 不再改變
return result
正規式
正規化的目的,是消除因冗餘造成的更新異常(update / insertion / deletion anomaly)。
lP9.8cm 正規式 & 條件(在 1NF 的基礎上)
1NF & 每個屬性值都是不可分割的原子值(無重複群、無陣列欄位——雖然現代 SQL 允許 array/JSON,但那是刻意的反正規化)。
2NF & 非主屬性不能部分相依於候選鍵(只在複合鍵時有意義)。
3NF & 對每個 FD \(X \to A\):\(X\) 是超鍵,或 \(A\) 是主屬性(屬於某候選鍵)。
BCNF & 對每個非平凡 FD \(X \to A\):\(X\) 必須是超鍵。比 3NF 嚴格,但可能無法保持相依性。
4NF / 5NF & 處理多值相依與連接相依,實務上較少刻意追求。
分解一個關聯 \(R\) 為 \(R_1, R_2\) 時要檢查兩件事:
無損連接(lossless join):\(R_1 \bowtie R_2 = R\)。充分條件:\((R_1 \cap R_2) \to R_1\) 或 \((R_1 \cap R_2) \to R_2\)。
相依保持(dependency preserving):原本的每個 FD 都能在單一分解後的表上檢查,不必 join。
假設 , 且 \(\mathit{customer\_id} \to \mathit{customer\_email}\)、\(\mathit{product\_id} \to \mathit{product\_name}\)。 一位客戶改了 email,就得更新他所有訂單列(更新異常);刪掉某產品的最後一筆訂單,產品名稱也消失了(刪除異常)。 分解為 、、 後,三張表皆為 BCNF,且無損、相依保持。
正規化減少冗餘、保證一致性,代價是查詢需要更多 join。OLTP 系統通常維持 3NF/BCNF; OLAP 的星狀綱要(star schema)則刻意把維度反正規化成寬表,以換取掃描效率(第 13 章)。 「先正規化,再有憑有據地反正規化」是通則。
關聯代數
關聯代數是一組以關聯為輸入、關聯為輸出的運算子。它是程序式的——運算的順序就是執行的順序—— 因此查詢最佳化器實際上就是在「同一個關聯代數表達式的所有等價寫法中,挑最便宜的那個」。
基本運算子
clP5.0cmP3.6cm 符號 & 名稱 & 語意 & SQL 對應
\(\sigma_{p}(R)\) & 選擇 selection & 保留滿足述詞 \(p\) 的元組(水平切割) &
\(\pi_{A_1,\ldots}(R)\) & 投影 projection & 只保留指定屬性(垂直切割),集合語意下去重 &
\(R \times S\) & 笛卡兒積 & 所有元組配對 & (無條件)
\(R \bowtie_{\theta} S\) & 連接 theta-join & \(\sigma_{\theta}(R \times S)\) &
\(R \bowtie S\) & 自然連接 & 以同名屬性相等為條件並去掉重複欄 &
\(R \cup S\)、\(R \cap S\)、\(R - S\) & 集合運算 & 需綱要相容 &
\(\rho_{S(B_1,\ldots)}(R)\) & 更名 rename & 更改關聯或屬性名稱(自我連接必備) &
\(R \div S\) & 除法 division & 「對所有 \(S\) 都有配對」的 \(R\) 元組 & 以雙重 表達
擴充運算子
外部連接:\(R \lojoin S\)(左)、\(R \rojoin S\)(右)、\(R \fojoin S\)(全),無配對的一側補 NULL。
半連接 \(R \sjoin S\):保留 \(R\) 中「在 \(S\) 有配對」的元組(對應 ); 反連接(anti-join)對應 。分散式查詢常用半連接減少資料搬移(第 14 章)。
分組與聚合 \(\gamma_{G;\,f(A)}(R)\):對應 搭配 。
排序 \(\tau_{A}(R)\) 與 去重 \(\delta(R)\):在 bag 代數中才需要。
賦值 \(T \leftarrow E\):把子表達式存成暫存關聯,對應 CTE()。
從 SQL 到關聯代數
SELECT e.name, d.name AS dept
FROM employees e JOIN departments d ON e.dept_id = d.dept_id
WHERE e.age > 30 AND d.name = 'Engineering';
直接翻譯(先積再選再投影): \[\pi_{e.name,\, d.name}\Big(\sigma_{e.age>30 \,\wedge\, d.name='Engineering' \,\wedge\, e.dept\_id=d.dept\_id}(E \times D)\Big)\] 最佳化器會改寫成「先把選擇推到各表、再連接」的等價形式(述詞下推,第 6 章): \[\pi_{e.name,\, d.name}\Big(\sigma_{age>30}(E) \;\bowtie_{dept\_id}\; \sigma_{name='Engineering'}(D)\Big)\] 兩者結果相同,但第二式在連接前就把 \(D\) 縮到可能只剩一列——這就是為什麼學好關聯代數對理解最佳化器至關重要。
常用等價規則(最佳化器的工具箱)
選擇可拆分、可交換:\(\sigma_{p_1 \wedge p_2}(R) = \sigma_{p_1}(\sigma_{p_2}(R)) = \sigma_{p_2}(\sigma_{p_1}(R))\)。
投影串接只留最外層:\(\pi_{A}(\pi_{B}(R)) = \pi_{A}(R)\),若 \(A \subseteq B\)。
選擇與連接交換(述詞只涉及一側時):\(\sigma_{p_R}(R \bowtie S) = \sigma_{p_R}(R) \bowtie S\)。
連接的交換律與結合律:\(R \bowtie S = S \bowtie R\);\((R \bowtie S) \bowtie T = R \bowtie (S \bowtie T)\) ——這讓 \(n\) 個表有 \(O(n!)\) 種以上的連接順序,是第 6 章「連接重排」的搜尋空間。
投影下推:\(\pi_{A}(R \bowtie_{\theta} S) = \pi_{A}(\pi_{A_R \cup \theta_R}(R) \bowtie_{\theta} \pi_{A_S \cup \theta_S}(S))\),提早丟掉不需要的欄位。
外部連接不滿足一般的交換與結合律;選擇只有在「NULL 拒絕」(null-rejecting)述詞下才能推過外部連接。
關聯計算
關聯計算(relational calculus)是宣告式的:只描述結果元組必須滿足的條件。
元組關聯計算(TRC):\(\{\, t \mid \exists e \in \mathit{Employees}\ (e.age > 30 \wedge t.name = e.name) \,\}\)。
定義域關聯計算(DRC):變數代表屬性值而非元組:\(\{\, \langle n \rangle \mid \exists a, s, d\ (\langle \_, n, a, s, d\rangle \in \mathit{Employees} \wedge a > 30) \,\}\)。
Codd 定理:安全的(結果有限的)關聯計算與關聯代數表達能力相同。 SQL 在語法上更接近 TRC( 就是 \(\exists\)), 而資料庫內部先把 SQL 轉成關聯代數樹(邏輯計畫),再做最佳化——這正是第 5 章的起點。
SQL 的執行模型
雖然 寫在最前面,SQL 的邏輯處理順序其實是:
這解釋了幾個常見困惑:為什麼 不能用聚合函數(還沒分組)、 為什麼 的別名不能在 使用(還沒投影), 以及為什麼 可以用別名(已經投影)。注意這只是邏輯順序; 實體執行順序由最佳化器決定,只要結果等價即可。
SQL 使用三值邏輯(TRUE / FALSE / UNKNOWN)。 是 UNKNOWN 而非 TRUE; 只保留 TRUE 的列; 永遠不會回傳任何列; 聚合函數忽略 NULL 但 不會。這些語意最佳化器都必須保留,因此許多等價規則都附帶「NULL 拒絕」的條件。
OLTP 與 OLAP
lP5.4cmP5.4cm & OLTP(線上交易處理) & OLAP(線上分析處理)
典型查詢 & 依鍵讀寫少量列:下單、轉帳、登入 & 掃描數百萬列做聚合:報表、趨勢分析
每次觸及資料 & 幾 KB & 幾 GB 到 TB
存取模式 & 隨機、點查詢、高並行寫入 & 循序、全欄掃描、少量寫入(批次載入)
重視指標 & 延遲、每秒交易數(TPS)、ACID & 吞吐量、壓縮率、掃描速度
綱要 & 正規化(3NF) & 星狀/雪花(反正規化)
儲存佈局 & 列式(row store)+B+Tree & 行式(column store)+壓縮+向量化
代表系統 & PostgreSQL、MySQL、Oracle、SQL Server & ClickHouse、DuckDB、BigQuery、Snowflake、Redshift
基準測試 & TPC-C、TPC-E & TPC-H、TPC-DS
兩種工作負載對儲存引擎的要求幾乎相反:OLTP 需要快速找到並修改單一列(第 2–4 章), OLAP 需要以最少 I/O 掃過單一欄位(第 13 章)。試圖同時服務兩者的系統稱為 HTAP(Hybrid Transactional/Analytical Processing),通常以「列式主儲存+行式副本」實現(如 TiDB 的 TiFlash、SingleStore)。
PostgreSQL 把「關聯」這個概念實作得非常徹底:表、索引、序列、view、物化 view 都是 裡的一列 ( 區分 表、 索引、 序列、 view、 物化 view、 TOAST 表), 屬性存在 ,限制存在 ,統計資訊存在 。 系統目錄本身也是普通的 heap 表,可以直接查詢:
SELECT c.relname, c.relkind, c.relpages, c.reltuples
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public';
與 就是第 6 章最佳化器估算掃描成本的起點。
第 19 章的 MiniDB v1 需要一個最小的系統目錄:(屬性名稱、型別、位移)與 (表名 \(\to\) 首頁 page id)。把它設計成「存在 page 0 的特殊表」,就能體會 PostgreSQL「系統目錄也是表」的自舉(bootstrap)哲學。
給定 \(R(A,B,C,D)\) 與 \(F=\{A\to B,\ B\to C,\ C\to D\}\),求 \(A^+\) 與所有候選鍵;\(R\) 是否為 BCNF?若否,給出無損且相依保持的 3NF 分解。
以關聯代數寫出:「找出所有部門都有員工年齡大於 30 的部門名稱」,並說明為何需要除法或雙重否定。
證明或舉反例:\(\sigma_{p}(R \lojoin S) = \sigma_{p}(R) \lojoin S\) 在 \(p\) 只涉及 \(R\) 屬性時是否恆成立?若 \(p\) 只涉及 \(S\) 屬性呢?
在 bag 語意下,\(\pi_{A}(R \cup S) = \pi_{A}(R) \cup \pi_{A}(S)\) 是否成立?\(\pi_{A}(R \cap S)\) 呢?
(動手)在 PostgreSQL 建立本章的 /,執行 觀察最佳化器輸出的計畫樹,對照你手寫的關聯代數樹,找出被下推的述詞。
說明三值邏輯如何導致 在 含 NULL 時回傳空結果,並改寫成 。
E. F. Codd, , CACM 1970. 原文 PDF: https://cs-people.bu.edu/mathan/reading-groups/papers-classics/codd.pdf
CMU 15-445 Fall 2025, Lecture #01 Relational Model & Algebra、#02 Modern SQL: https://15445.courses.cs.cmu.edu/fall2025/schedule.html
Database System Concepts 7th ed., Ch. 2(關聯模型)、Ch. 6(形式化查詢語言)、Ch. 7(正規化):https://db-book.com/
RelaX 線上關聯代數計算器(可輸入代數式並看到結果與樹):https://dbis-uibk.github.io/relax/
PostgreSQL 文件 Ch. 5 Data Definition(限制、繼承、分割)與 Ch. 7 Queries: https://www.postgresql.org/docs/current/ddl.html