外鍵是資料庫層面的引用完整性保障,但也帶來鎖競爭、死鎖風險、複制複雜度、DDL 受限等副作用。本文系統講解建立語法、級聯選項、InnoDB 鎖機制、效能代價、遷移/分庫分表時的處理策略,並給出生產環境「慎用外鍵,應用層保證」的工程建議。
-- 父表
CREATE TABLE departments (
dept_id INT PRIMARY KEY,
name VARCHAR(50)
) ENGINE=InnoDB;
-- 子表建立外鍵
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
dept_id INT NOT NULL,
name VARCHAR(50),
INDEX idx_dept (dept_id), -- 外鍵欄位必須有索引
CONSTRAINT fk_emp_dept
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB;
-- 既有表添加外鍵
ALTER TABLE employees
ADD CONSTRAINT fk_emp_dept
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
ON DELETE SET NULL ON UPDATE CASCADE;
| 選項 | 父表刪除/更新時子表行為 | 適用場景 |
|---|---|---|
| RESTRICT / NO ACTION | 阻止操作,報錯 ERROR 1451 | 核心主數據,嚴禁孤兒行(預設) |
| CASCADE | 同步刪除/更新子表匹配行 | 明確從屬關係(訂單-明細、部門-員工) |
| SET NULL | 子表外鍵欄位置 NULL(欄位必須允許 NULL) | 可選關聯(文章-作者、任務-負責人) |
| SET DEFAULT | 子表外鍵欄位置預設值(罕用) | 有明確預設歸屬 |
S 鎖(共享鎖),防止父行被刪除/更新。高併發插入同一部門員工 → 熱點主鍵鎖競爭。X 鎖執行級聯。大量子行 → 長事務、鎖升級、阻塞其他事務。mysqldump 導出需按父→子順序,導入需先建父表、再建子表、最後加外鍵;分庫分表時外鍵跨庫失效。--alter-foreign-keys-method),線上變更極其複雜。# 應用層模式:先查父表存在性,再寫子表(單向依賴,無鎖競爭)
def create_employee(dept_id, name):
# 1. 輕量級快取檢查(Redis 本地快取,容忍毫秒級不一致)
if not dept_cache.exists(dept_id):
# 2. 回源資料庫確認
if not db.execute("SELECT 1 FROM departments WHERE dept_id=%s", dept_id):
raise ValueError("Department not exist")
# 3. 寫入子表(無外鍵鎖)
db.execute("INSERT INTO employees (dept_id, name) VALUES (%s, %s)", dept_id, name)
# 刪除父表時:軟刪除 + 異步清理子表
def delete_department(dept_id):
db.execute("UPDATE departments SET deleted=1 WHERE dept_id=%s", dept_id)
# 異步任務:軟刪除子表或標記孤兒
async_task.cleanup_orphan_employees(dept_id)
SHOW CREATE TABLE 可見關係,但生產環境不建立實際約束(FOREIGN KEY ... REFERENCES ... 寫在註釋或單獨文檔)。--allow-on-master 配合 --foreign-keys-method=drop 線上遷移(風險較高,需充分測試)。外鍵是「資料庫層的安全帶」,但在高併發、分佈式、敏捷迭代的現代架構中,往往成為「手銬」。建議:Schema 文檔記錄關係,應用層代碼保證完整性,資料庫層不建立實體外鍵,以換取寫入效能、DDL 自由度與架構演進靈活性。