MapleCheng

在浩瀚的網路世界中無限潛水欸少年郎!

0%

我的資料庫不用 Foreign Key——一個技術主管的實戰心得

前幾天在整理一個專案的資料庫 Schema 文件,突然想到一件事:我們團隊的所有專案,資料庫裡沒有任何一條 Foreign Key。

不是忘了加。是刻意不加的。

「你瘋了嗎?」

我知道,這句話大概是很多人看到標題的第一反應。資料庫課本第三章就教你正規化、建立關聯、加上 FK 確保參照完整性。這是基本功,怎麼能不用?

好,讓我先承認一件事:課本說的沒錯。 FK 在理論上是保護資料完整性的好工具。但理論跟實務之間,隔著一道叫做「部署」的鬼門關。

踩坑的開始

故事要從幾年前說起。那時候我還很乖,每張表都規規矩矩地建 FK、加 Index、設 Unique Constraint。資料庫設計得漂漂亮亮,ER Diagram 拿出來簡直是藝術品。

然後災難就開始了。

第一次部署更新的時候,我需要改一張表的結構。但這張表被三張子表用 FK 指著,其中一張子表又被另外兩張表指著。結果呢?一個欄位的異動,我得按照特定順序先解除 FK、改完再重建,整個 migration script 寫了快一百行,就為了加一個欄位。

更慘的是,客戶端環境。我們的系統跑在客戶的 Windows Server 上,每家客戶的資料庫狀態都不一樣。有人手動改過表、有人塞過髒資料、有人的 FK 早就因為某次意外被 disable 了但沒人知道。每次部署都像拆炸彈。

全世界都在禁用 FK

後來我開始研究,發現這不只是我的問題。很多大規模系統早就不用 FK 了:

  • 微服務架構:資料分散在不同服務的資料庫裡,跨庫 FK 本來就不可能
  • 高流量系統:FK 的檢查在大量寫入時會變成效能瓶頸
  • 頻繁部署的產品:Schema migration 被 FK 綁住,部署速度直接砍半
  • 多租戶 SaaS:每個租戶的資料狀態不同,FK 約束反而製造更多部署問題

這不是什麼前衛的想法。這是被現實打臉之後的集體共識。

我的「純表設計」原則

經過幾年的演化,我們團隊現在的資料庫設計原則很簡單,我叫它「純表設計」:

不用 FK。 關聯邏輯全部在程式端處理。應用層本來就要驗證資料,多一層 FK 檢查只是多一個出問題的地方。

不建 Index。 先別急著罵我。我的意思是:不在建表時預設加 Index。等系統上線、有了真實的查詢模式,再根據實際的慢查詢去加。盲目加 Index 不但浪費空間,還會拖慢寫入速度。

不設 Unique Constraint。 唯一性檢查同樣由程式處理。原因跟 FK 一樣——部署時少一個要處理的東西。

不用 Default Value。 框架會處理預設值。string 類型永遠非 nullable,框架自動帶空字串。所有欄位一律 NOT NULL

decimal 一律 DECIMAL(19,9) 不想每次都在猜這個欄位要幾位小數。統一規格,省心。

時間用 DATETIME 不是 DATETIME2。夠用就好,不需要奈秒級精度。

「那資料完整性怎麼辦?」

這是最常被問的問題。我的回答是:資料完整性本來就不該只靠資料庫。

想想看,你的 API 會不會在收到 request 的時候,不做任何驗證就直接塞進資料庫?不會吧。你一定會在 Controller 層或 Service 層檢查:這筆訂單的客戶存不存在?這個料號有沒有在主檔裡?數量是不是正數?

既然程式端已經在做這些檢查了,FK 就變成了雙重保險。雙重保險聽起來很好,但它的代價是:

  • 每次 INSERT/UPDATE 都多一次查詢
  • Migration 複雜度暴增
  • 部署風險提高
  • 除錯時要同時考慮程式邏輯和 DB 約束

對我們這種要在幾十個客戶端部署的產品來說,簡單就是可靠

但我不是說 FK 沒用

這裡要說清楚:我不是在宣揚「FK 是邪惡的」。

如果你在做一個內部系統,資料庫只有一套,團隊對 Schema 有完全的掌控權,部署頻率不高——用 FK 完全沒問題,甚至應該用。FK 能在最後一道防線幫你擋住程式 bug 造成的髒資料。

但如果你跟我一樣,面對的是:

  • 產品要部署到幾十個客戶端
  • 每個客戶的環境都不一樣
  • 需要頻繁更新 Schema
  • 團隊規模不大,沒有專職 DBA

那「純表設計」可能值得你認真考慮。

關聯怎麼追蹤?

沒有 FK 不代表沒有關聯。我們的做法是:

文件先行。 每個專案都有完整的 Schema 文件,每張表的每個欄位都標注它關聯到哪張表。格式大概長這樣:

1
2
3
4
🔑 PK: TA001 + TA002
- TA001: 單別(→ CMSMQ.MQ001
- TA002: 單號
- TA003: 客戶代號(→ COPMA.MA001

文件跟程式碼一起版控,改了表就改文件,PR review 時一起看。

命名慣例。 我們的欄位命名有固定規則,看到欄位名就知道它對應哪張表。這比隱藏在 DB metadata 裡的 FK 關係直觀多了。

ERD 用 Mermaid。 關聯圖直接寫在 Markdown 裡,跟文件一起維護。不需要額外的工具,git diff 就能看出改了什麼。

            
            erDiagram
    ORDER ||--o{ ORDER_DETAIL : contains
    ORDER }o--|| CUSTOMER : belongs_to
    ORDER_DETAIL }o--|| PRODUCT : references
          

實際的效果

這套做法我們跑了幾年,結果是:

部署速度快了很多。 Migration script 從動輒上百行變成幾行 ALTER TABLE,部署一個新版本從半小時縮短到幾分鐘。

除錯更單純。 問題不是在程式就是在資料,不會有「FK violation 但我不知道是哪個 constraint」的鬼打牆。

新人上手更快。 不用先搞懂複雜的 FK 關係圖,看文件就知道表跟表之間的關聯。

代價呢? 偶爾會有髒資料跑進去。但說實話,有 FK 的時候也會有——只是錯誤訊息從「FK violation」變成業務邏輯上的不一致。處理方式都一樣:找到 bug、修掉、清資料。

結語

資料庫設計沒有銀彈。FK 不是萬能的,不用 FK 也不是萬能的。

我的建議是:根據你的部署模式來決定。如果你的系統是「build once, deploy once」,FK 是好朋友。如果你的系統是「build once, deploy everywhere」,認真考慮把約束邏輯搬到程式端。

這不是偷懶,這是務實。

如果你也在多客戶端部署的路上掙扎過,希望這篇能給你一點不同的思路。畢竟,我們的血淚不該白流 😂