跳至主要內容
論文精煉 · Data model

A Relational Model of Data for Large Shared Data Banks(大型共享資料庫的關聯式資料模型)

把資料視為隨時間變動的 n 元關聯,並以述詞演算查詢,讓程式不再因儲存方式、索引或存取路徑改變而失效。

作者E. F. Codd,IBM Research Laboratory,San Jose, California 發表於CACM 13(6),1970 年 6 月 年份1970
閱讀原始論文 PDF 所有論文

一口氣講完 — 整篇論文的濃縮

1960 年代末的格式化資料系統把樹狀與網狀結構直接暴露給使用者,只要儲存順序、索引或存取路徑一改,應用程式就跟著壞掉。Codd 主張把資料庫裡的全部資料看成一組隨時間變動的 n 元關聯(relation),並以「定義域無序」的 relationship 形式呈現給使用者:欄位靠定義域名稱與角色名稱辨識,而不是靠位置。他提出一套正規化(normalization)程序,把父關聯的主鍵往下複製到每個從屬關聯,藉此消去非簡單(重複群組)定義域,最後得到一組扁平陣列,裡面沒有指標、不依賴雜湊定址、也沒有索引或排序串列。在這之上他定義了關聯上的運算:排列、投影、join、合成、tie、限制,用來從既有關聯推導出新關聯,並以「可推導性」定義強冗餘與弱冗餘,進而寫出系統真正能檢查的一致性約束。因為正規化後的模型是扁平的,一階述詞演算就足以充當通用資料子語言(universal data sublanguage),Codd 把它當作衡量各種資料語言表達力的標尺,也是程式與機器表示之間達到最大獨立性的基礎。

在這篇論文之前 — 它所降落的世界

到 1970 年為止,市面上的格式化資料系統 — IMS/360、IDS、TDMS、GIS — 提供給使用者的不是階層式檔案,就是稍微一般化的網狀模型;當時被視為資料獨立性最大進展的,是「資料描述表」這種機制。在那些系統裡,零件檔的記錄可能依零件序號遞增排列,而應用程式被允許假設「呈現順序」就等於「儲存順序」。索引政策更是各行其是:TDMS 無條件替所有屬性建索引,當時釋出的 IMS 只讓你選「完全不建索引」或「只對主鍵建索引」,而 IDS 讓檔案設計者把索引以額外的 chain 織進檔案結構,程式必須指名這些 chain,一旦 chain 被移除程式就不能正確運作。Codd 還指出,包括 IMS/360 在內的多個既有系統,甚至無法表示「有兩個相同定義域」的關聯,例如零件與子零件的組成表。當時提出的補救辦法 — 在所有使用某條存取路徑的程式都作廢之前,絕不淘汰該路徑 — 根本不可行,因為整個使用者社群模型中的存取路徑數量終究會膨脹到無法收拾。

問題 — 當時真正壞掉的地方

  • 順序相依:系統把資料元素存成至少一種與硬體位址順序緊密相關的全序,程式一旦利用了這個順序,當該順序必須被換掉時就很可能無法正確運作。
  • 索引相依:索引在資訊上是冗餘的、純粹為效能而存在,因此必須隨著存取樣態改變而動態建立與刪除;但在 IDS 裡,指名了索引 chain 的程式在 chain 被移除後就失效了。
  • 存取路徑相依:同一份零件、專案與投入量資料至少可以組成五種不同的樹狀結構,針對其中一種寫成的程式,在其餘四種裡至少有三種會失敗 — 原因不是引用了不存在的檔案,就是漏掉了含有必要資訊的檔案。
  • 非簡單定義域 — 例如員工的薪資歷程這種重複群組 — 無法放進二維、欄位同質的陣列,只能逼出更複雜的儲存資料結構。
  • 系統設計者會掉進 connection trap:沿著指標從供應商走到他供應的零件、再走到使用這些零件的專案,然後誤以為得到了真正的「供應商對專案」關聯。
  • 具名關聯集合裡的冗餘從來沒有被刻畫過,系統因此沒有任何依據判斷一次插入、刪除或鍵值更新之後資料庫是否仍處於一致狀態;而以路徑為中心的觀點還逼使用者硬造出遠多於必要的名稱,因為名稱是掛在路徑(或路徑型別)上,而不是掛在關聯上。

核心概念 — 主要貢獻,以及它們為何成立

資料即隨時間變動的 n 元關聯

Codd 直接採用數學上 relation 的定義:給定集合 S1 到 Sn,R 是一組 n 元組(n-tuple),第 j 個元素取自第 j 個定義域,等價於 Cartesian product 的子集,n 稱為它的 degree。資料庫的全部資料因此就是一堆不同 degree 的關聯,隨時間接受元組的插入、刪除與修改。這之所以成立,是因為關聯只承載資料本身的自然結構:沒有順序、沒有指標、沒有存取路徑被夾帶進描述裡。所有儲存表示需要的東西都被推到使用者模型之下,系統於是有了自由更動它的空間。

relationship:拿掉欄位順序

論文指出 degree 高達 30 的關聯一點也不罕見,沒有使用者應該被迫記住這種關聯的定義域排列順序。因此 Codd 主張使用者面對的不該是 relation,而是它的定義域無序版本 relationship — 形式上是「在定義域排列下等價」的關聯所構成的等價類。要做到這點,只需要讓定義域在同一關聯內不靠位置就能唯一識別:重複出現的定義域用一個具區別性的角色名稱來限定,於是關聯 component 的兩個 part 欄位變成 sub.part 與 super.part。使用者對一個 relationship 只需知道它的名稱與各定義域名稱,甚至這些資訊都可以由系統以選單方式提供。

以正規化取得正規形式

有些定義域的元素本身就是關聯,而這些關聯又可能定義在非簡單定義域上,形成 employee 內含 jobhistory、jobhistory 內含 salaryhistory 這樣的巢狀樹。Codd 給出一套機械化的消去程序 — 正規化 — 把父關聯的主鍵往下複製到從屬關聯,再從父關聯中刪掉所有非簡單定義域,藉此把樹壓平。好處是每個關聯都能存成單純的二維、欄位同質陣列;正因如此,一階述詞演算才足以當查詢語言,壓縮後的陣列也才能成為跨系統的資料交換格式。這套程序成立的條件是:非簡單定義域的相互關係圖是一組樹,且沒有任何主鍵包含非簡單的成分定義域。

用主鍵與外鍵取代指標

能唯一識別關聯中每個元組的定義域(或定義域組合)稱為主鍵;若其中沒有任何簡單定義域是多餘的,就稱為非冗餘主鍵,而當一個關聯有多個非冗餘主鍵時,任意挑一個作為它的主鍵。若 R 的某個定義域組合不是 R 的主鍵,但其元素是某關聯 S 主鍵的值,就稱為外鍵,而 S 也可以就是 R 自己。元組之間的交互參照因此以使用者看得見、寫得出的「值」來表達,而不是以使用者必須走訪的機器位址表達。Codd 順勢推論:傳統上把資料切成「實體描述」與「實體間關係」兩部分的做法已難以維持,在使用者模型中也沒有意義 — 全部都只是關聯。

建立在述詞演算上的通用資料子語言

由於正規化後的關聯是扁平的,一個應用型一階述詞演算就足以描述資料庫的任意子集;Codd 因此提出資料子語言 R,可嵌入任何宿主語言 H。R 負責宣告關聯與其主鍵並登錄到系統 catalog(受權限控管),也負責指定擷取、插入與刪除,且完全不理會機器表示中可能存在的任何順序;H 則提供儲存表示的宣告,以及 R 需要呼叫的算術函式。這裡主張的「通用」是描述能力上的通用,而不是計算能力:限定條件式必須具備該演算之合式公式(well-formed formula)的表達力,不過只實作 prenex normal form 這類子集就足以保留這份表達力。Codd 把這個語言當成一把標尺,用來衡量其他任何被提出的資料語言。

關聯的對稱式利用

一旦使用者知道某個關聯被儲存了,他就會期待能以任意的引數組合當作已知、其餘當作未知來利用它(查詢、更新、刪除),因為資訊就擺在那裡。Codd 把這稱為(邏輯上的)對稱式利用,並指出以路徑為基礎的系統付不起這個代價:單一個二元關聯就需要兩條有向路徑,而 degree 為 n 的關聯需要 n! 條路徑要命名與管理。他也很誠實地承認:效能上的對稱性是不能期待的 — 這等於把整個存取路徑選擇問題悄悄交給了系統。若改成用巢狀二元關聯取代直接的 n 元記法,情況更糟:名稱數從 n+1 變成 2n-1,而且某些查詢基礎會變得極難表達。

可推導性、冗餘與一致性

Codd 定義:若存在一串取自運算集合 O 的運算,能「在所有時刻」都從集合 S 的成員產生出 R,則 R 是從 S 可 O-推導的;其中只允許結果唯一的運算 — natural join 合格,join 不合格。若某關聯的某個投影可從其他關聯的投影推導出來,該關聯集合就是強冗餘的,而且能寫成一條等式;若該投影無法被推導,但在所有時刻都是其他投影之某個 join 的投影,則是弱冗餘。強冗餘是有用且可移除的 — 它換來使用者方便,也讓半退役的 relationship 能留在具名集合裡,好讓舊程式繼續正確執行;弱冗餘則內生於使用者社群的邏輯需求,無法移除。由於系統缺乏語意知識、無法推導出這些事實,冗餘必須以約束敘述的方式宣告出來,而一致性就定義為「瞬時值滿足該約束集合」。

運作方式 — 具體的機制

陣列表示法與它的五條規則

關聯以二維陣列呈現:每一列是一個 n 元組,列的順序無關緊要,所有列彼此相異,欄的順序有意義(因為它對應定義域的排列順序),而每一欄以其定義域名稱標示。Codd 明講這個陣列只是說明用,並非關聯式觀點的必要成分 — 這正是把邏輯模型與任何儲存表示切開的那條線。欄序在這一層仍然重要,因為兩欄可能標頭相同卻意義不同:在三元關聯 component(part, part, quantity) 中,component(x, y, z) 表示零件 x 是零件 y 的直接組件,且組裝一單位 y 需要 z 單位的 x。角色名稱 sub 與 super 用來限定重複的定義域,使用者於是能以名稱定址欄位,徹底忘掉位置。

正規化程序

從非簡單定義域相互關係樹的頂端開始。取父關聯的主鍵,把該主鍵(或主鍵組合)插入每一個直接從屬的關聯;展開後關聯的主鍵,就是它原本的主鍵再加上從父關聯複製下來的主鍵。接著把父關聯裡所有非簡單定義域刪掉、移除樹的頂點,然後對剩下的每棵子樹重複同樣的步驟。套用到 employee(man#, name, birthdate, jobhistory, children) 與 jobhistory(jobdate, title, salaryhistory),結果是 employee'(man#, name, birthdate)、jobhistory'(man#, jobdate, title)、salaryhistory'(man#, jobdate, salarydate, salary) 與 children'(man#, childname, birthyear) — 每個關聯都變扁平,而每個後代關聯都明確帶著原本靠實體巢狀關係才隱含的祖先鍵。

選取運算子:排列與投影

給定索引串列 L = i1 到 ik 與 degree 至少為 k 的 n 元關聯 R,πL(R) 是一個 k 元關聯,其第 j 欄取自 R 的第 ij 欄,並把結果中重複的列移除。純排列是 L 為全部 n 個索引之重排的情形;degree 4 的 supply 共有 4! = 24 種排列(含恆等排列)。純投影則是刪去部分欄位;因為之後要去除重複列,投影可能比來源關聯的元組更少,例如排列後的投影 π31(supply) 就是如此。單就使用者的 relationship 模型而言排列無關緊要,但對儲存表示很重要:在支援對稱式利用的系統中,一個被儲存的關聯所能回答的查詢集合,與它任一排列所能回答的完全相同 — 兩者都存在邏輯上沒必要,但基於效能考量可能是划算的。

join、natural join 與模糊點

若存在三元關聯 U 使得 π12(U) = R 且 π23(U) = S,則二元關聯 R 可與二元關聯 S 進行 join,任何這樣的 U 都稱為 R 與 S 的一個 join。充分條件是 π2(R) = π1(S);此時 natural join R*S = {(a,b,c):R(a,b) 且 S(b,c)} 必定存在,且滿足 π12(R*S) = R 與 π23(R*S) = S。join 一般並不唯一:若 join 所依據之定義域中的某個元素,在 R 之下有一個以上的關聯對象、在 S 之下也有一個以上,這個元素就稱為模糊點(point of ambiguity),正是它造成多個 join 並存。若 π1(R) 或 S 是函數(一對一或多對一,但不是一對多),就不會出現模糊點,natural join 就是 R 與 S 的唯一 join;要注意「S 與 R 的 join」是完全另一回事。

環狀 join 與 tie 運算子

三個二元關聯 R(supplier、part)、S(part、project)、T(project、supplier),在彼此滿足五項投影與蘊涵條件時,可以合成一個 cyclic 3-join,也就是滿足 π12(U) = R、π23(U) = S、π31(U) = T 的三元關聯 U;相對地,linear 3-join 得到的是四元關聯 V,滿足 π12(V) = R、π23(V) = S、π34(V) = T。為了從線性 join 導出環狀 join,Codd 引入 tie 運算子 γ:它把 n 元關聯降為 n-1 元,只保留首尾兩個成分相等的元組,因此自然環狀 3-join 就是 γ(R*S*T);R*S*T 不需要括號,因為 natural join 具結合律。環狀 3-join 出現多解的條件遠比 2-join 嚴苛:三組配對都必須有模糊點,而且這三個點要沿著環互為對象、串成一圈。至於任意 degree 的關聯,只要把定義域取 Cartesian product 分組成 A、B、C,就能當成二元關聯處理。

合成、限制與推導運算集合

若存在 R 與 S 的某個 join U 使得 T = π13(U),則 T 是 R 與 S 的一個合成(composition),因此「可合成」與「可 join」完全等價;自然合成定義為 R·S = π13(R*S)。多個不同的 join 有可能塌縮成同一個合成,因為經由其他點的明確對應把答案釘住了,所以相異合成的數量介於 1 到相異 join 的數量之間。R 被 S 限制(restriction)的結果,是以等長索引串列 L 與 M 定義的 R 的最大子集 R',滿足 πL(R') = πM(S);這是「把函數限制到其定義域子集」的推廣,只有在被比較的元素之間可以談相等時才有定義。對於非推論式系統的具名集合,Codd 主張足夠的推導運算集合是投影、natural join、tie 與限制:排列在此無關緊要,自然合成則因為等於「先 natural join 再投影」而不必納入。

宣告冗餘與檢查一致性

強冗餘可以用等式表示:在 employee(serial#, name, manager#, managername) 中,若 manager# 的作用定義域恆包含於 serial# 的、managername 的恆包含於 name 的,則投影 π14(employee) 可寫成投影 π12(employee) 與 π13(employee) 的合成,於是 managername 這個定義域是多餘的。弱冗餘沒有這樣的等式,只有像這樣的敘述:在所有時刻,π12(P) 是 π12(Q) 與 π21(R) 的某個合成 — 而那個合成在某一瞬間可能是自然合成、在另一瞬間卻是非自然的。由於系統缺乏各具名關聯的細部語意,它無法推導出這些約束,就算試圖歸納也可能出錯,所以必須在關聯集合 C 之外另外宣告約束敘述集合 Z;狀態 (C, Z, V) 為一致,當且僅當瞬時值 V 滿足 Z。檢查 R、S、T 上的合成約束可化約成三個測試:π1(T) = π1(R)、π2(T) = π2(S),以及對 π12(T) 中每一組 (a,c) 都存在 b 使 (a,b) 屬於 π12(R) 且 (b,c) 屬於 π12(S)。在每次插入、刪除或鍵值更新時檢查會拖慢這些操作;另一種做法是每日批次檢查,再透過所有狀態變更交易的日誌回溯出造成不一致的輸入。

論文證明了什麼 — 量測數據與證明

  • 同一份零件、專案與投入量資料可組成五種合理的樹狀結構;一個假設了其中一種、且不做任何結構判斷的程式 P,在其餘四種裡至少有三種會失敗:若 P 在結構 5 成功,則在其他四種全部失敗;若在結構 3 或 4 成功,則至少在 1、2、5 失敗;若在 1 或 2 成功,則至少在 3、4、5 失敗。
  • 在以路徑為基礎的模型下,對稱式利用在組合數上是不可行的:單一個二元關聯就需要兩條有向路徑,degree 為 n 的關聯需要 n! 條路徑要命名與管理,而論文指出在商業、政府與科學資料庫中,degree 30 的關聯一點也不罕見。
  • 把 n 元關聯改用巢狀二元關聯表達,需要造 2n-1 個名稱而非 n+1 個:4 元關聯 supply 在 n 元記法下只需 5 個名稱,寫成 P(supplier, Q(part, R(project, quantity))) 則需要 7 個。
  • 論文指出,只要非簡單定義域的相互關係圖是一組樹、且沒有任何主鍵含有非簡單的成分定義域,正規化就適用;Codd 並表示他不知道有哪個應用需要放寬這兩項條件。
  • 當 π2(R) = π1(S) 時 natural join 必定存在,並可證明滿足 π12(R*S) = R 與 π23(R*S) = S;進一步地,若 π1(R) 或 S 是函數,就不會出現模糊點,natural join 即為 R 與 S 的唯一 join。
  • 正規化後的關聯集合可壓縮成一種交換格式:其中沒有任何指標(無論是位址值或位移值)、完全不依賴雜湊定址機制、也不含索引或排序串列 — 因此能在內部表示差異極大的系統之間用來搬運大量資料。

限制與取捨 — 論文自承的,以及後來被發現的

  • 論文自承完全不討論支援關聯式模型的系統要如何實作,並指出「把高階資料語言的請求翻譯成對當前儲存表示既正確又高效的動作」是一個充滿挑戰、尚未解決的設計問題。(論文自承;要到 System R 與 INGRES 才補上)
  • 作為推導集合,論文只給了投影、natural join、tie 與限制,並寫明「其他正規化性質的運算是可能的,但本文不討論」— 這個缺口後來由 Codd 自己的第二、第三正規化形式以及 Boyce-Codd normal form 填補。(論文自承,後續工作補齊)
  • 對稱式利用只在邏輯層面被承諾,論文明白但書:效能上的對稱性是不能期待的;如何為任意宣告式請求選出高效執行計畫,後來成為成本式查詢最佳化問題,本文並未嘗試處理。(論文自承;由後續工作揭露其難度)
  • 一致性被定義成純粹的瞬時狀態性質,與該狀態如何形成無關,因此系統無法分辨那是使用者輸入錯誤還是一筆尚未做完的跨關聯更新,通常也只能去詢問環境(例如造成不一致的使用者)才能釐清;論文同時承認,要對可能非常龐大且變動頻繁的關聯取得瞬時快照有實務困難,並且完全沒有談併行控制與復原機制。(論文自承)
  • 弱冗餘被明確指為內生於使用者社群的邏輯需求,系統或資料庫管理者都無法移除,因此某些跨關聯的約束只能長期背著並持續檢查,無法靠設計消除。(論文自承)

它後來變成什麼 — 繼承這個想法的系統

IBM 的 System R 把這篇論文變成了可運行的系統:SEQUEL(後來的 SQL)就是 Codd 通用資料子語言的一種具體語法,而 System R 的成本式查詢最佳化器,正是對 Codd 刻意留白的效能問題所給的答案。柏克萊的 INGRES 以 QUEL 做出第二個獨立實作,這條血脈經 Postgres 延伸到 PostgreSQL;Oracle、DB2、Informix 與 Sybase 則把同一個模型商業化,Codd 也因此獲得 1981 年的 Turing Award。第 2 節定義的那些運算 — 投影、限制、natural join、合成 — 成了此後幾乎每一個查詢引擎的實體運算子詞彙,從 System R 的 nested-loop 與 sort-merge join,一路延伸到今日向量化與分散式引擎如 Spark SQL、Presto 與 BigQuery。本文所稱的 normal form 就是後來的第一正規化形式;圖 3 那套「把父鍵複製下來」的程序,是第二、第三正規化形式、Boyce-Codd normal form 以及第四、第五正規化形式的直系祖先,而主鍵與外鍵這對概念則成了 SQL 的參照完整性約束。connection trap 的警告老得非常漂亮:它正是「不能假設在階層式、物件式、文件式或圖式儲存中沿指標走訪就等於算出正確的 join」這個論證,在往後每一輪導覽式對關聯式的辯論中不斷重演。即便是放棄 SQL 語法的系統,最後也大多回到關聯式語意 — Google 的 Spanner 與 F1、Cassandra 的 CQL,以及 Hadoop 之上的各種 SQL 層,都重新引入了表格、宣告式鍵與宣告式查詢,取代導覽式的替代方案。

論文原文 — 逐字引用

“未來大型資料庫的使用者,必須被保護到不需要知道資料在機器裡是怎麼組織的(也就是內部表示)。”

Abstract

“它提供了一種只描述資料自然結構的方式 — 也就是說,不為了機器表示的目的而額外疊加任何結構。”

§1.1

“由於不了解關聯的合成,若干系統設計者掉進了可稱為 connection trap 的陷阱。”

§2.1.4

術語 — 依本篇論文的用法

relation(degree 為 n 的關聯)
取自 n 個具名定義域 S1 到 Sn 的 n 元組所成的集合,等價於這些定義域之 Cartesian product 的子集。degree 為 1、2、3 者分別稱為 unary、binary、ternary。
relationship
關聯的「定義域無序」版本,形式上是「在定義域排列下彼此等價」的關聯所構成的等價類。這才是使用者應該面對的東西,欄位位置因此從使用者模型中完全消失。
primary key(主鍵)
其值能唯一識別關聯中每個 n 元組的定義域或定義域組合。若其中沒有任何簡單定義域是多餘的,就稱為非冗餘主鍵;當一個關聯有多個非冗餘主鍵時,任意挑一個作為主鍵。
foreign key(外鍵)
關聯 R 中不是 R 主鍵、但其元素為某關聯 S 之主鍵值的定義域或定義域組合,而 S 也可以就是 R 自己。在關聯 supply 中,supplier、part、project 分開來看各自都是外鍵。
nonsimple domain(非簡單定義域)
元素本身就是關聯、而非不可分解原子值的定義域,大致對應當時術語中的 repeating group。Codd 的例子是 employee 關聯中的 salary history 這個定義域。
normal form(正規形式)
每個關聯都只定義在簡單定義域上的狀態,因此每個關聯都能表示成二維、欄位同質的陣列。它由「把父關聯主鍵複製到從屬關聯」的正規化程序達成。
natural join
對滿足 π2(R) = π1(S) 的二元關聯 R 與 S,由所有滿足 R(a,b) 且 S(b,c) 的 (a,b,c) 組成的三元關聯。它是此情形下必定存在的那個 join,具結合律,且其投影能原封不動還原 R 與 S。
point of ambiguity(模糊點)
join 所依據之定義域中的某個元素,它在 R 之下有一個以上的關聯對象,在 S 之下也有一個以上。正是這種元素讓 R 與 S 之間存在多個相異的 join。
connection trap
誤以為「沿著所有指標路徑從供應商經零件走到專案」就能得到該供應商所供應專案的正確集合。只有當目標關聯在所有時刻都恰好是另外兩個關聯的自然合成時,這個結論才成立。

在時間軸上的位置 — 這篇論文在整段故事中的座標

在時間軸上查看