外鍵與參照完整性完全指南

理解外鍵如何維護資料間的參照完整性,以及 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

🔗相關文章