Foreign Key Issues
部分內容由 LLM 生成,尚未經過人工驗證。
外鍵(Foreign Key)設計與實務中的常見問題。
常見反模式
| 反模式 | 問題 | 最佳實踐 |
|---|---|---|
| 理論有 FK 但 DB 沒建 | ER 圖有標示,但資料庫未實際建立約束 | 明確決策:建立 FK 或文件記錄原因 |
| 只靠應用層驗證 | 資料完整性依賴應用程式邏輯 | 資料庫層應有約束作為最後防線 |
| ETL 中避開 FK | 為了 ETL 效能而不建 FK | 載入後重建 FK,或使用 deferred constraints |
為什麼需要外鍵約束
flowchart TB
subgraph WithFK["有外鍵約束"]
A1[應用層驗證] --> B1[資料庫 FK 約束]
B1 --> C1[資料完整性保證]
end
subgraph WithoutFK["無外鍵約束"]
A2[應用層驗證] --> B2[直接寫入]
B2 --> C2[可能產生孤兒資料]
end
外鍵約束的好處
1. 資料完整性
-- 有 FK:嘗試插入不存在的 customer_id 會失敗
INSERT INTO orders (customer_id, amount) VALUES (9999, 100.00);
-- Error: Foreign key violation
-- 無 FK:可以插入,產生孤兒資料
INSERT INTO orders (customer_id, amount) VALUES (9999, 100.00);
-- Success (但 customer 9999 不存在)2. 級聯操作
-- 刪除客戶時自動刪除相關訂單
ALTER TABLE orders
ADD CONSTRAINT fk_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE CASCADE;
-- 或設為 NULL
ON DELETE SET NULL;3. 查詢優化提示
- 資料庫優化器可利用 FK 關係選擇更好的執行計畫
- 某些資料庫可利用 FK 進行 JOIN 消除
不建外鍵的情境
合理不建 FK 的場景
1. 跨資料庫/微服務架構
┌─────────────┐ ┌─────────────┐
│ Service A │ │ Service B │
│ (DB-A) │ --> │ (DB-B) │
│ orders │ │ customers │
└─────────────┘ └─────────────┘- 無法在不同資料庫間建立 FK
- 需要在應用層處理一致性
2. 高寫入量 OLTP 系統
- FK 檢查會增加寫入延遲
- 需評估效能 vs 完整性的取捨
3. 分區表限制
- 某些資料庫對分區表的 FK 支援有限
- 例如:MySQL 分區表不支援 FK
4. NoSQL 或非關聯式使用場景
- 故意使用非正規化設計
- 以查詢效能為優先
不建 FK 時的替代措施
1. 文件記錄
-- 在 DDL 中註解說明關係
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
-- FK to customers.id (not enforced for performance)
-- Integrity maintained by OrderService
customer_id BIGINT NOT NULL,
amount DECIMAL(10,2)
);2. 定期資料品質檢查
-- 檢查孤兒資料
SELECT o.id, o.customer_id
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
WHERE c.id IS NULL;
-- 排程執行並警示3. 應用層強制驗證
@Transactional
public Order createOrder(Long customerId, BigDecimal amount) {
// 先驗證 customer 存在
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new EntityNotFoundException("Customer not found"));
Order order = new Order(customer.getId(), amount);
return orderRepository.save(order);
}ETL 與外鍵
ETL 處理外鍵策略
策略 1:暫時停用 FK
-- 載入前
ALTER TABLE orders NOCHECK CONSTRAINT fk_customer; -- SQL Server
-- 或
SET FOREIGN_KEY_CHECKS = 0; -- MySQL
-- 執行 ETL 載入...
-- 載入後重新啟用
ALTER TABLE orders CHECK CONSTRAINT fk_customer;
SET FOREIGN_KEY_CHECKS = 1;策略 2:使用 Deferred Constraints(PostgreSQL)
-- 建立可延遲的約束
ALTER TABLE orders
ADD CONSTRAINT fk_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
DEFERRABLE INITIALLY DEFERRED;
-- 在交易結束時才檢查
BEGIN;
INSERT INTO orders ... -- 暫時不檢查
INSERT INTO customers ... -- 之後補上
COMMIT; -- 此時才檢查 FK策略 3:載入順序控制
-- 先載入父表
LOAD DATA INTO customers ...
-- 再載入子表
LOAD DATA INTO orders ...策略 4:暫存表載入
-- 載入到暫存表(無 FK)
LOAD DATA INTO staging_orders ...
-- 驗證後才插入正式表
INSERT INTO orders
SELECT * FROM staging_orders s
WHERE EXISTS (SELECT 1 FROM customers c WHERE c.id = s.customer_id);決策指南
| 場景 | 建議 | 原因 |
|---|---|---|
| 單體應用 + OLTP | 建 FK | 資料完整性優先 |
| 微服務跨 DB | 不建 | 技術限制 |
| 高寫入 OLTP | 評估 | 測試效能影響 |
| 資料倉儲 | 可不建 | 資料已經過 ETL 驗證 |
| 分區表 | 依 DB 支援 | 檢查資料庫限制 |