Skip to content

把資料放在該放的地方

另有 28 種語言版本

English · Español · 日本語 · Português (Brasil) · Français · Deutsch · Italiano · 한국어 · 简体中文 · Português (Portugal) · Bahasa Indonesia · Türkçe · Polski · Tiếng Việt · ไทย · Nederlands · العربية · עברית · Українська · हिन्दी · Русский · Svenska · Dansk · Norsk bokmål · Suomi · Español (México) · Français (Canada) · Deutsch (Schweiz)

Two identical drawers on a workbench, seen from above: one jammed with everything mixed together, one divided so each kind of thing has its own compartment.
圖片由 AI 生成

第 4 個模組,共 6 個,屬於為含義建模資料一章,即 Data Architect 課程的第 2 章。每個模組都可以獨立閱讀。 上一篇:構建邏輯模型 · 下一篇:設計到第三範式,然後適度回退

**本模組回答的問題:**規範化如何幫助我弄清資料真正屬於哪裡?

我們已經有了一個邏輯模型:實體、關係、屬性和鍵。現在我想問資料建模中最重要的問題之一:這些資料真的屬於這裡嗎?

這聽起來很簡單,其實不然。我見過很多系統,其中每個欄位在有人建立它的時候都說得通。問題不在於欄位荒唐,而在於事實存錯了地方。一條配送記錄裡存著其實是在描述配送中心的資訊。同一個地址出現在幾百條記錄上。一個欄位裡存著用逗號隔開的三個值。改動一個業務事實,就意味著要更新 10,000 條記錄。這正是規範化幫我們解決的問題。在我們被資料庫術語淹沒之前,先把整個思路說清楚:**把事實放在它該在的地方。**本模組的其餘內容,都是幫我們更系統地做到這一點。

規範化就是組織資料,讓每個事實只儲存一次,存在它該在的地方。當一張表承擔了太多職責時,我們就把它拆成幾張表,每張表只描述一樣東西,再用鍵把它們連線起來。

何必這麼麻煩?因為只存一次的事實,也只需要改一次。規範化能防止冗餘,也就是同一個事實被複制到許多記錄上,同時也防止一份副本更新了、其他副本沒更新時悄悄出現的不一致。這就是為什麼它適合 Salesforce 這樣的交易系統:記錄整天都在被建立和更新。

我們分步驟進行規範化,這些步驟叫作範式,每一步只問一個問題:

  • **第一範式(1NF):**每個欄位一個值,並且有某樣東西能唯一標識每一行。
  • **第二範式(2NF):**每個事實都描述標識這一行的全部內容,而不只是其中一部分。
  • **第三範式(3NF):**每個事實都描述這樣東西本身,而不是與它相關的別的東西。

第三範式之後還有更多範式。我稍後會簡單談談它們,以及為什麼我們通常止步於第三範式。首先,我們來看看這三個步驟是怎麼發生的。

我在現場授課時,不從配送講起,而是從狗狗講起。每個人都懂狗,所以沒人會被業務分心,我們可以看著資料努力找到自己的家。然後,我們再把同樣的步驟用到 Steve Trucking 上。

**起點:原始電子表格。**我會在螢幕上放幾隻狗,然後全班同學再加上他們自己的。幾分鐘之內,我們就有了一張大清單:狗的名字、出生年份、主人是誰、主人住在哪裡、品種、繁育者是誰,以及它會哪些技能。

狗狗出生年份主人郵編城市品種繁育者技能
Bella2017Marcus80202Denver拉布拉多Maple Ridge坐下、別動、握手
Milo2019Marcus80202Denver貴賓犬Golden Meadow坐下、打滾
Max2022Ana64105Kansas City混種坐下、裝死
Luna2020Kenji98101Seattle哈士奇Cascade坐下、別動、轉圈
Rocky2024Priya30301Atlanta比格犬Maple Ridge坐下
Coco2014Ana64105Kansas City吉娃娃Little Paws握手、轉圈

它能用。每張電子表格在第一天都能用。但看看已經出了什麼問題。我要怎麼找出所有會「握手」的狗?Ana 的郵編出現在兩行上,Marcus 的也是:如果有人只更新了其中一行,會怎樣?Maple Ridge 出現了兩次,Kansas City 也是。還有,Max 是從收容所領養的,所以根本沒有繁育者。

**第 1 步:第一範式。兩條規則。每個欄位只存一個值,所以用逗號隔開的技能清單必須拿掉。並且每一行都需要某樣能唯一標識它的東西。**不是狗的名字,叫 Max 的狗多得是。而是一個 ID:一個自動生成的 ID,或者一個 GUID。技能被拆出來,放進它們自己的行裡,每隻狗的每項技能一行。而一旦一項技能有了自己的行,就有地方記錄逗號清單裡永遠說不出來的東西:這項技能有多難,這隻狗掌握得怎麼樣,以及它是什麼時候學會的。

狗狗(按狗狗 ID 標識)

狗狗 ID名字出生年份主人郵編城市品種繁育者
D-001Bella2017Marcus80202Denver拉布拉多Maple Ridge
D-002Milo2019Marcus80202Denver貴賓犬Golden Meadow
D-003Max2022Ana64105Kansas City混種
D-004Luna2020Kenji98101Seattle哈士奇Cascade
D-005Rocky2024Priya30301Atlanta比格犬Maple Ridge
D-006Coco2014Ana64105Kansas City吉娃娃Little Paws

狗狗技能(Dog Trick)(按狗狗 ID + 技能標識)

狗狗 ID技能難度熟練程度學會時間
D-001坐下簡單精通2017-11
D-001別動簡單精通2018-01
D-001握手中等良好2018-06
D-002坐下簡單精通2020-02
D-002打滾困難學習中2023-05
D-003坐下簡單良好2022-06
D-003裝死困難學習中2024-03
D-004坐下簡單精通2020-11
D-004別動簡單良好2021-01
D-004轉圈中等精通2021-06
D-005坐下簡單學習中2025-02
D-006握手中等精通2015-06
D-006轉圈中等學習中2025-04

現在,「所有會握手的狗」就是一個簡單的問題了。但請注意狗狗技能是靠什麼標識的:不是一樣東西,而是兩樣東西合在一起,狗和技能。先記住這一點。

**第 2 步:第二範式。**只有當一行是由一個組合來標識時,第二範式才有意義,而狗狗技能正是如此。所以我對每一列都問:**它描述的是整個組合,還是隻是其中一部分?**熟練程度描述的是這隻狗做這項技能:Bella 的「坐下」已經精通,Rocky 還在學。學會時間也一樣。但難度呢?不管哪隻狗做,「坐下」都很簡單。它描述的是技能,只是鍵的一部分,這就是為什麼「簡單」在每一行「坐下」上都重複出現。所以技能有了自己的表,剩下的就是這一對組合。

技能

技能 ID技能難度
T-01坐下簡單
T-02別動簡單
T-03握手中等
T-04打滾困難
T-05裝死困難
T-06轉圈中等

狗狗技能(狗狗 ID + 技能 ID)

狗狗 ID技能 ID熟練程度學會時間
D-001T-01精通2017-11
D-001T-02精通2018-01
D-001T-03良好2018-06
D-002T-01精通2020-02
D-002T-04學習中2023-05
D-003T-01良好2022-06
D-003T-05學習中2024-03
D-004T-01精通2020-11
D-004T-02良好2021-01
D-004T-06精通2021-06
D-005T-01學習中2025-02
D-006T-03精通2015-06
D-006T-06學習中2025-04

這張狗狗技能表,就是我們在模組 2 中發現的那張,它在資料建模中有個名字:聯接,或者照我的叫法,邏輯交叉物件。一隻狗會很多項技能;一項技能也有很多隻狗會。交叉物件位於兩者之間,而且它不是空洞的管道:它存放著屬於這段關係本身的事實,也就是熟練程度和日期。我們會在模組 6 中再次遇到它;在第 3 章中我們還會看到,Salesforce 對「交叉物件」的用法更窄:**Salesforce 交叉物件(Junction Object)**是一個帶有兩個主要-詳細資料關係的自訂物件(第 3 章,模組 7)。

**第 3 步:第三範式。**現在回到狗狗表。表上的一切都由狗狗 ID 標識,所以第二範式沒什麼可說的。第三範式問的是另一個問題:**這個事實描述的是這隻狗,還是這隻狗相關的別的東西?**品種是一個獨立的東西:拉布拉多就是拉布拉多,不管是哪隻狗,而關於它的事實,比如體型,屬於品種,而不屬於 Bella。郵編和城市描述的是主人,而不是狗:那是 Ana 住的地方,不管我們看的是她的哪隻狗。繁育者的詳細資訊描述的是繁育者。這些都是關於另一個事實的事實,所以每一項都移到它自己的表裡,狗用一個外部索引鍵指向它:

  • **狗狗:**狗狗 ID、名字、出生年份、主人 ID、品種 ID、繁育者 ID
  • **主人:**主人 ID、姓名、郵編、城市
  • **品種:**品種 ID、品種、體型
  • **繁育者:**繁育者 ID、名稱、城市
  • 技能和狗狗技能,來自第 2 步

現在 Ana 只儲存一次,她的兩隻狗都指向她。Maple Ridge 只儲存一次,而品種表為每個品種的事實(比如體型)提供了一個家,這些事實在電子表格里根本沒地方放。外部索引鍵可以為空:Max 沒有繁育者 ID,這是一個誠實的答案,而不是缺漏。然後我會給全班出一道陷阱題:主人是什麼時候得到這隻狗的,這個日期放在哪裡?放在狗上。在這個模型裡,一隻狗同一時間只有一個主人,所以這個日期描述的是這隻狗與它主人之間的關係。

你還可以再進一步,把城市從郵編中拆出來;教科書就是這麼做的。但在實踐中,一個地址會保持在一起,況且 Kansas City 本來就橫跨兩個州,這很好地提醒我們:現實世界可不讀我們的教科書。

這就是整個過程:一張電子表格,然後三個問題,每個事實最後都落在一個地方。退後一步,看看我們建了什麼。一張雜亂的清單變成了六張表,每張描述一樣東西,用鍵連線起來:

第三範式之後的狗狗模型 六個用連線相連的方框。一位主人擁有一隻或多隻狗。一隻狗有一位主人,屬於一個品種,由零個或一個繁育者繁育(一個繁育者繁育一隻或多隻狗)。狗和技能之間是狗狗技能:一隻狗會零項或多項狗狗技能,一項技能見於零項或多項狗狗技能,每項狗狗技能恰好屬於一隻狗和一項技能。 擁有 繁育 屬於 會 見於 主人 繁育者 狗 品種 狗狗技能 技能 恰好一個 零個或一個 一個或多個 零個或多個 © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

跟上這個過程,不需要任何一個 Salesforce 術語。它只是狗、狗身邊的人,以及它們會什麼。

狗狗讓這些步驟一目瞭然。而真實的業務讓它們變得要緊,因為同樣的錯誤在規模化之後要花真金白銀。所以,我們把同樣的三個問題帶到 Steve Trucking。假設它是這樣記錄配送的:

配送零售合作夥伴配送中心配送中心城市配送小組卡車
1001KeystoneKeystone Phoenix DCPhoenix配送小組 12117
1002KeystoneKeystone Phoenix DCPhoenix配送小組 8122
1003KeystoneKeystone Phoenix DCPhoenix配送小組 12117

乍一看,這裡沒什麼複雜的。但看看配送中心。我們在每一條從它出發的配送上重複寫著「Keystone Phoenix DC, Phoenix」:Steve Trucking 每天在九個配送中心之間大約完成 3,300 次配送,所以每個配送中心每天有幾百次。如果那個配送中心搬遷了怎麼辦?我們要改多少條記錄?如果漏掉一條又會怎樣?現在資料說這個配送中心同時位於兩個城市。這就是更新異常。如果我們想在一個新配送中心的第一次配送之前就把它建立起來,怎麼辦?如果我們刪除了某個配送中心的最後一次配送:我們是不是剛剛把唯一存放著它的資訊的地方也刪掉了?

這些是同一個問題的不同症狀:**我們把關於不同事物的事實混在了一起。**配送中心的事實屬於配送中心。配送的事實屬於配送。配送小組的事實屬於配送小組。這就是實踐中的規範化。

我們從第一範式,也就是 1NF 開始。人們有時會在這裡把規範化弄得比實際需要更難。

假設一次配送有個叫「項目」的欄位:「冰箱、製冰機套件、舊機拉走。」我們把三條資訊放進了一個欄位。這看起來很方便,直到有人問:「上個月我們做了多少次舊機拉走?」或者想像有叫項目 1、項目 2 和項目 3 的欄位。這也能用,直到出現有四個項目的配送。問題不在於我們選錯了項目欄位的數量,而在於我們沒有認出這個重複結構。一次配送可以有好幾個項目。這是我們應該建模的資料:也就是我們在模組 2 中發現的配送項目。

一次配送的項目,三種存法 三張卡片展示同一次配送的項目的三種存法。一個欄位:配送 1001 有一個「項目」欄位,內容是「冰箱、製冰機套件、舊機拉走」,它答不出上個月有多少次舊機拉走。重複欄位:項目 1 冰箱、項目 2 製冰機套件、項目 3 舊機拉走,它裝不下有四個項目的配送。子記錄(醒目標示):配送 1001 有三條配送項目,冰箱、製冰機套件和舊機拉走,下一個項目是一筆新記錄,而不是一個新欄位。下方的註記:如果某件事可以發生很多次,可能有另一個實體正想冒出來。 一個欄位 三個事實藏在一個值裡 配送 1001 項目:冰箱、製冰機套件、舊機拉走 它答不出的問題 上個月有多少次舊機拉走? 重複欄位 項目 1、項目 2、項目 3 配送 1001 項目 1:冰箱 項目 2:製冰機套件 項目 3:舊機拉走 它裝不下的配送 有四個項目的那次 子記錄 每項一筆配送項目 配送 1001 配送項目:冰箱 配送項目:製冰機套件 配送項目:舊機拉走 下一個項目 新記錄,而不是新欄位 如果某件事可以發生很多次,可能有另一個實體正想冒出來。 © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

1NF 的實踐經驗是:**不要把重複結構藏在欄位裡。**如果業務告訴你某件事可以發生很多次,就要認真聽。可能有另一個實體正想冒出來。

在真實的 org 裡,你會經常看到這種情況。有人需要幾種語言的描述,於是建立了一個叫 Description_English 的欄位,然後是 Description_French,再然後是 Description_German。或者一項審批需要幾個人,於是我們就有了審批人 1、審批人 2、審批人 3。我在評審中發現這種情況時,就是打一下手,去修你的資料模型。如果業務確實永遠只有一兩個,一兩個欄位也許還行。超過這個數,就應該是一個子物件:一個帶有語言的翻譯(Translation),一個帶有審批人的審批步驟(Approval Step)。下一種語言,或者第四位審批人,就變成一條新記錄,而不是一個新欄位外加一次部署。

第二範式,也就是 2NF,在一條記錄由多樣東西的組合來標識時才有意義。以配送項目為例。它有自己的記錄 ID,但每一條都按配送 + 產品唯一。假設我們儲存配送、產品、數量和產品的描述。

數量在這裡說得通:它描述的是這次配送上的這個產品。但產品描述描述的並不是這個組合,而是產品本身。把它放在每一條配送項目上,那麼這個型號的冰箱每次裝上另一輛卡車,我們都要重複一遍。這就是線索:這個事實屬於別的地方。

所以 2NF 的問題是:**這個屬性描述的是我正在標識的整個東西,還是隻是其中一部分?**如果它只描述產品,它就歸產品。如果它描述的是產品在這次特定配送中的情況(數量、是否需要安裝),它就屬於配送項目。很多管理員永遠不會有意識地說:「我現在正在應用第二範式。」這沒關係。我更在意的是,你學會問:這個事實實際上描述的是什麼?

第三範式:不要儲存關於其他事實的事實

Section titled “第三範式:不要儲存關於其他事實的事實 ”

現在說說日常建模中我最看重的那個:第三範式,也就是 3NF。

每個配送中心在零售合作夥伴那邊都有一位經理,這個人會使用合作夥伴入口網站。假設我們把配送中心經理的姓名直接放在每一次配送上。配送中心換了新經理時會怎樣?我們要更新從那個配送中心出發的每一次配送。又是那股味道:我們在重複一個屬於別處的事實。配送從一個配送中心出發;配送中心有一位經理。這是兩段關係,經理的姓名屬於配送中心。

經典的教科書例子是地址:郵編、城市和州一起儲存在每條記錄上,而城市和州通常可以由郵編推出來。同樣的形狀,同樣的問題。

關於另一個事實的事實:配送中心經理 兩個面板。每次配送上都有:一筆配送記錄,帶有記錄 ID、配送日期、配送中心,以及配送中心的城市和經理,這兩項都標為重複。只存一次,放在配送中心:配送保留記錄 ID、配送日期和配送中心 ID,並從一個配送中心出發,而配送中心存有自己的記錄 ID、名稱、城市和經理。下方的註記:配送中心換新經理,左邊要更新幾百次,右邊只要一次。 每次配送上都有 只存一次,放在配送中心 出發自 配送 配送 ID 記錄 ID 配送日期 配送中心 配送中心城市 重複 配送中心經理 重複 配送 配送 ID 記錄 ID 配送日期 配送中心 ID 配送中心 配送中心 ID 記錄 ID 名稱 城市 經理 配送中心換新經理:左邊要更新幾百次,右邊只要一次。 © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

實用的 3NF 問題是:**這個事實描述的是這樣東西,還是與這樣東西相關的別的東西?**我希望你時刻都在問這個問題。

現在退後一步,就像我們對狗狗做的那樣。我們一開始的那張配送表變成了五樣東西,每樣都存放自己的事實:配送項目在第一範式時被拆出來,產品在第二範式時被拆出來,而配送中心的城市和經理在第三範式時移到了配送中心。

規範化之後的 Steve Trucking 五個用連線相連的方框。一個零售合作夥伴營運零個或多個配送中心。一次配送恰好從一個配送中心出發,並包含一個或多個配送項目。每個配送項目恰好對應一個產品,而一個產品出現在零個或多個配送項目上。 營運 出發自 包含 出現在 零售合作夥伴 配送 配送項目 產品 配送中心 恰好一個 零個或一個 一個或多個 零個或多個 © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

同樣的三個問題,同樣的結果:每個事實都在一個地方。

你可以花一整門資料庫課程來講規範化。我們不打算這麼做。在這門課裡,我希望你有一個在職架構師的理解:

  • **1NF:**我是不是把多個值或者重複結構藏在了一個欄位或一條記錄裡?
  • **2NF:**這個事實描述的是我正在標識的整個東西嗎?
  • **3NF:**這個事實真的在描述這個實體,還是在描述與它相關的東西?

這些不是資料庫教科書裡的定義,而是我在設計時真正能用上的問題。

是的,規範化還能走得更遠:**Boyce-Codd 範式(BCNF)**針對鍵更復雜的模型收緊了 3NF 周圍的規則,此外還有第四、第五甚至第六範式。它們處理的是更少見的重複,這類重複更多來自不尋常的鍵結構,而不是日常的業務記錄。理論很多,而根據我的經驗,對於我們在 Salesforce 上構建的這類系統,不值得為它們頭疼。在業務系統中,壓力通常朝著相反的方向:出於使用者、報表和效能的考慮,我們從第三範式適度回退的可能性,遠遠大於越過它。這是下一個模組的內容。知道還有更高的範式存在就好,但對於我們做的大多數事情,1NF 到 3NF 就給了我們所需的思考工具。

我的立場是這樣的。當我試圖理解一個業務並設計它的邏輯模型時,我想從乾淨的狀態開始:配送的事實歸配送,配送中心的事實歸配送中心,卡車的事實歸卡車,重複的概念作為概念來建模。我不想僅僅因為可能讓某個介面或報表更方便,就把一個業務事實複製得滿模型都是。至少現在還不要。

但規範化不是宗教。目標不是做出學術上最純粹的模型。我見過一些架構師太專注於純粹,結果模型讓每個必須使用它的人都很痛苦。我們不是要贏得一場規範化比賽;我們是在構建一個系統,而這個系統有使用者、報表、整合、效能需求、規模,以及越來越多讀取資料的 AI。它們可能想要不同形狀的資料。

所以我的起點是在可行的情況下設計到第三範式,然後在必要時適度回退:先弄清每個事實屬於哪裡。然後,如果我們要偏離它,我想知道為什麼。如果答案是「因為我不知道這個欄位還能放哪兒」,那就不夠好。如果答案是「我們有意複製了這個值,因為它讓合作夥伴記分卡變得簡單,而且我們有可靠的方法讓它保持同步」,那我們就是在進行一場架構對話了。

在對邏輯模型做規範化時,我會尋找:

  • 應該單獨建模的重複值或重複結構。
  • 描述的實體與它們所存放的實體不同的屬性。
  • 在許多記錄上不必要地重複的事實,以及讓它們變得不一致的依賴關係。
  • 藏在欄位裡的業務概念。
  • 一個在任何實現妥協之前就能呈現真相的乾淨模型。

這不是因為每個物理系統都必須保持完美的規範化。恰恰相反。在下一個模組中,我們會出於使用者、報表、規模、分析和 AI 的考慮,有意偏離乾淨的模型,而且我們會清楚地知道自己在用什麼做交換。