目錄
- 什麼是外鍵?
- 什麼是參照完整性?
- 參照動作(ON DELETE / ON UPDATE)
- CASCADE 詳解
- DDL 層級的 CASCADE
- 實戰範例
- 連帶刪除的陷阱
- 各資料庫差異
- 最佳實踐
- 常見問題
- 總結
什麼是外鍵?
外鍵(Foreign Key, FK) 是一個表中的欄位(或欄位組合),它的值必須對應到另一個表的主鍵(或唯一鍵)。外鍵用來表達兩個表之間的關聯。
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
amount DECIMAL(10,2),
FOREIGN KEY (user_id) REFERENCES users(id) -- user_id 是外鍵
);
- 父表(被參照表):
users - 子表(參照表):
orders orders.user_id的每個值,都必須是users.id裡存在的值(或 NULL)
核心特點
- 強制關聯:子表不能參照到不存在的父列
- 由資料庫保證:不靠應用程式自律,而是 DB 層級的約束
- 影響增刪改:插入子列、刪除父列、更新主鍵都會被檢查
什麼是參照完整性?
參照完整性(Referential Integrity) 指的是:資料庫中所有外鍵的參照關係永遠有效,不會出現「指向不存在資料」的孤兒列(orphan row)。
外鍵約束會在以下操作時自動檢查:
| 操作 | 檢查內容 |
|---|---|
對子表 INSERT / UPDATE |
新的外鍵值必須在父表存在 |
對父表 DELETE |
是否還有子列在參照它 |
對父表 UPDATE(改主鍵) |
子列的外鍵要不要跟著處理 |
沒有參照完整性的問題:
orders 裡有 user_id = 99,但 users 裡沒有 id = 99
→ 這筆訂單變成「孤兒」,查不到對應使用者
當父列被刪除或主鍵被更新、而仍有子列參照它時,資料庫該怎麼做?這由參照動作決定。
參照動作(ON DELETE / ON UPDATE)
定義外鍵時,可分別指定父列被「刪除」與「更新主鍵」時的行為:
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE <動作>
ON UPDATE <動作>
五種動作
| 動作 | 父列被刪/改主鍵時 | 典型用途 |
|---|---|---|
NO ACTION |
若還有子列參照就報錯(預設);在交易結束時才檢查 | 預設、最安全 |
RESTRICT |
若還有子列參照就立即報錯 | 明確禁止刪除有子資料的父列 |
CASCADE |
連帶刪除 / 更新所有子列 | 父子強綁定(如訂單與訂單明細) |
SET NULL |
子列外鍵設為 NULL |
關聯可選、子資料應保留 |
SET DEFAULT |
子列外鍵設為欄位的預設值 | 較少用,需有合理預設值 |
NO ACTION vs RESTRICT
兩者都阻止刪除,差別在檢查時機:
RESTRICT:操作當下立刻檢查、立刻拒絕NO ACTION:延後到語句 / 交易結束才檢查(可延遲約束時更有彈性)
實務上多數情況兩者表現相同。
CASCADE 詳解
CASCADE 是「連帶處理」:父列發生變化時,子列自動跟著變。
ON DELETE CASCADE
刪父列 → 自動刪掉所有參照它的子列。
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
DELETE FROM users WHERE id = 1;
-- users.id = 1 被刪
-- orders 裡所有 user_id = 1 的訂單也自動被刪
ON UPDATE CASCADE
改父列主鍵 → 子列外鍵自動跟著改。
FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE
UPDATE users SET id = 100 WHERE id = 1;
-- orders 裡 user_id = 1 全部自動變成 100
ON UPDATE CASCADE較少用,因為主鍵通常不該被更新;若使用代理鍵(自增 ID)作主鍵,幾乎用不到。
連鎖反應(多層 CASCADE)
CASCADE 會沿著外鍵鏈一路傳遞:
users ←(CASCADE) orders ←(CASCADE) order_items
DELETE FROM users WHERE id = 1;
→ 刪 user 1
→ 連帶刪 user 1 的所有 orders
→ 再連帶刪那些 orders 的所有 order_items
一個 DELETE 可能引發大量連鎖刪除——這正是 CASCADE 強大也危險之處。
DDL 層級的 CASCADE
除了外鍵的參照動作,CASCADE 也出現在 DDL 指令中,意義是「連帶處理依賴物件」。這與外鍵的 ON DELETE CASCADE 是不同概念,常被混淆。
DROP ... CASCADE(PostgreSQL)
DROP TABLE users CASCADE;
-- 連帶刪除依賴 users 的物件:指向它的外鍵約束、view 等
DROP SCHEMA app CASCADE;
-- 連帶刪除 schema 內所有物件
- PostgreSQL 預設是
RESTRICT(有依賴就不准刪) - 加
CASCADE才會連帶刪依賴物件
TRUNCATE ... CASCADE(PostgreSQL)
TRUNCATE users CASCADE;
-- 連帶清空所有「有外鍵指向 users」的表
⚠️ MySQL 的
DROP TABLE/TRUNCATE沒有CASCADE選項;它靠foreign_key_checks設定與外鍵約束本身處理依賴。
兩種 CASCADE 對照
| 場景 | 語法 | 連帶的是 |
|---|---|---|
| 外鍵參照動作 | ON DELETE CASCADE |
資料列(刪 user 連帶刪 orders) |
| DDL 依賴 | DROP TABLE ... CASCADE |
結構物件(刪表連帶刪相關約束 / view) |
實戰範例
範例 1:訂單系統(強綁定用 CASCADE)
訂單明細離開訂單沒有意義 → 用 CASCADE。
CREATE TABLE orders (
id INT PRIMARY KEY
);
CREATE TABLE order_items (
id INT PRIMARY KEY,
order_id INT NOT NULL,
product VARCHAR(100),
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
);
-- 刪訂單,明細自動清掉,不留孤兒
DELETE FROM orders WHERE id = 5;
範例 2:文章與作者(弱關聯用 SET NULL)
作者刪帳號,文章想保留為「匿名」→ 用 SET NULL。
CREATE TABLE posts (
id INT PRIMARY KEY,
author_id INT,
title VARCHAR(200),
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL
);
DELETE FROM users WHERE id = 7;
-- posts.author_id = 7 全變成 NULL,文章保留
範例 3:防止誤刪(用 RESTRICT)
有未結訂單的客戶不准刪 → 用 RESTRICT。
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_id INT,
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT
);
DELETE FROM customers WHERE id = 3;
-- 若該客戶還有訂單 → 報錯,刪除被擋下
連帶刪除的陷阱
ON DELETE CASCADE 方便,但有實際風險:
1. 意外大規模刪除
一個看似單純的 DELETE,可能因連鎖而刪掉成千上萬列。刪父列前要清楚下游有多少子資料。
2. 效能問題
連帶刪除要逐表處理子列;若子表沒有在外鍵欄位上建索引,每次連帶刪除都可能全表掃描,造成嚴重效能問題。
-- 子表的外鍵欄位通常要自己加索引
CREATE INDEX idx_orders_user_id ON orders(user_id);
重點:許多資料庫(如 MySQL InnoDB 會自動為外鍵建索引,但 PostgreSQL 不會自動建子表索引),需手動確認。
3. 難以稽核與還原
資料「自動消失」不易追蹤。被 CASCADE 刪掉的資料沒有刪除紀錄,事後難以得知刪了什麼。
4. 循環參照
表 A 參照 B、B 又參照 A 時,CASCADE 的行為會變複雜,部分資料庫會拒絕這類設計。
軟刪除替代方案
對重要資料,常改用**軟刪除(soft delete)**而非實體 CASCADE 刪除:
-- 不真的刪,只標記
UPDATE users SET deleted_at = NOW() WHERE id = 1;
保留稽核軌跡、可還原,但查詢時都要記得過濾 deleted_at IS NULL。
各資料庫差異
| 項目 | PostgreSQL | MySQL (InnoDB) | SQLite |
|---|---|---|---|
| 外鍵預設啟用 | 是 | 是 | 否(需 PRAGMA foreign_keys=ON) |
| 子表外鍵自動建索引 | 否(要手動) | 是(自動) | 否 |
DROP TABLE ... CASCADE |
支援 | 不支援 | 不支援 |
TRUNCATE ... CASCADE |
支援 | 不支援 | 無 TRUNCATE |
| 延遲約束檢查 | 支援(DEFERRABLE) |
不支援 | 有限支援 |
SQLite 的外鍵約束預設關閉,每個連線都要先執行
PRAGMA foreign_keys = ON;才會生效,這是很常見的踩雷點。
最佳實踐
1. 依關聯強度選動作
父子強綁定(明細、子資源) → ON DELETE CASCADE
弱關聯(可選作者、分類) → ON DELETE SET NULL
重要主檔(客戶、帳號) → ON DELETE RESTRICT 或軟刪除
2. 一定要為子表外鍵建索引
尤其 PostgreSQL,否則連帶刪除 / 父表刪除檢查會很慢。
3. CASCADE 用在「擁有關係」而非「參照關係」
- ✅ 訂單 → 訂單明細(明細屬於訂單)
- ❌ 文章 → 作者(文章不該因作者沒了就消失,用 SET NULL)
4. 對關鍵資料優先考慮軟刪除
需要稽核、可還原的資料,別讓它被 CASCADE 無聲刪除。
常見問題
問題 1:CASCADE 是刪資料還是刪表?
看語法。ON DELETE CASCADE(外鍵)刪的是資料列;DROP TABLE ... CASCADE(DDL)連帶刪的是依賴的結構物件。兩者同名但用途不同。
問題 2:為什麼我設了外鍵,SQLite 卻沒擋住孤兒資料?
SQLite 外鍵預設關閉。每個連線都要先:
PRAGMA foreign_keys = ON;
問題 3:CASCADE 刪除為什麼很慢?
子表外鍵欄位沒建索引時,每次連帶刪除都要掃整張子表。PostgreSQL 不會自動建這個索引,需手動加。
問題 4:ON DELETE 和 ON UPDATE 一定要一起設嗎?
不用。可只設其中之一,未指定者用預設 NO ACTION。實務上 ON DELETE 較常用,ON UPDATE CASCADE 因主鍵通常不變而少用。
問題 5:NO ACTION 和 RESTRICT 到底差在哪?
行為幾乎相同(都阻止刪除),差別在檢查時機:RESTRICT 當下立即檢查,NO ACTION 延後到語句 / 交易結束。配合 DEFERRABLE 延遲約束時差異才明顯。
總結
核心要點
- 外鍵讓子表的值必須對應到父表,維護參照完整性,避免孤兒資料
- 父列被刪 / 改主鍵時,靠參照動作決定行為:
CASCADE/SET NULL/SET DEFAULT/RESTRICT/NO ACTION CASCADE= 連帶處理;強大但會連鎖、可能大量刪除,務必為子表外鍵建索引- DDL 的
DROP ... CASCADE連帶刪的是結構物件,與外鍵的 CASCADE 不同 - 各 DB 差異大:SQLite 外鍵預設關、PostgreSQL 子表索引要手動建、
DROP CASCADE只有 PostgreSQL 有
快速參考
| 動作 | 父列被刪時的行為 |
|---|---|
CASCADE |
連帶刪 / 改子列 |
SET NULL |
子列外鍵設 NULL |
SET DEFAULT |
子列外鍵設預設值 |
RESTRICT |
立即擋下 |
NO ACTION |
延後檢查後擋下(預設) |
| CASCADE 場景 | 連帶對象 |
|---|---|
ON DELETE CASCADE |
資料列 |
DROP TABLE ... CASCADE |
結構物件(約束 / view) |
建立日期:2026-06-17