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 支援檢查資料庫限制