系統目錄就是一般的關聯
團隊最初考慮替資料字典設計專用的非關聯式結構,因為那樣比較快。但他們很快發現:替目錄開一條專用存取路徑會造成程式碼重複,更糟的是讓 QUEL 無法查詢目錄。把 metadata 存成一般關聯,等於免費得到一套資料字典系統,也讓所有資料共用同一層存取方法。代價在文章後段浮現:執行期查目錄約佔每次互動固定開銷中的 100 毫秒,而從不把目錄改成 hash 的使用者要一直付這筆帳。
一份坦白的工程回顧:打造能真正跑起來的關聯式 DBMS 究竟付出多少代價,又有哪些設計根本是錯的。
INGRES 是最早真正跑得起來的關聯式資料庫系統之一,而這篇文章是 Stonebraker 對 1973 到 1979 年間在柏克萊打造它的完整自白。全文分三個階段推進:設計研討期、第一版實作(1975 年三月端給 Ted Codd 看的半成品),以及為了讓系統高效、可靠、可維護而進行的多年重寫;接著逐條列出作者認為根本就是錯誤的決定。技術核心是一連串以簡單為原則的選擇:系統目錄就是一般的關聯資料、以 query modification 一併實作 view/保護/完整性、以 tuple substitution 把查詢分解到單變數查詢、以單一 QUEL 敘述作為交易單位並搭配粗粒度實體鎖、以 deferred update 支撐當機復原。全文最常被引用的主張是:關於資料模型效率的爭論其實搔不到癢處——在五個 UNIX 行程加上全程直譯、每次互動固定 400 毫秒開銷的環境下,INGRES 每秒只能處理約 2.5 次互動,而同樣環境下的網狀模型系統也快不到哪裡去。
1973 年時,Codd 的關聯式模型已在紙上存在數年,而外界最常見的質疑就是它做不快。Stonebraker 與 Eugene Wong 一起讀關聯式文獻,很快就想動手實作,但兩人都坦承毫無帶領實作專案的經驗——事實上他們誰都沒寫過像樣規模的程式。他們湊到約九萬美元買硬體,代價是必須替柏克萊 Urban Economics Group 交出一套 geodata 系統;選機器時軟體環境比硬體更關鍵,於是選了 UNIX。這個選擇同時帶來 PDP-11 的 64K 位址空間、單一檔案上限 16 MB、512 位元組頁面的檔案系統,以及底層完全沒有並行控制與當機復原的現實。同一時期 IBM 的 System R 正平行開發,在好幾個關鍵點上做了相反的選擇,包括在 VM/370 上自行實作檔案系統。
團隊最初考慮替資料字典設計專用的非關聯式結構,因為那樣比較快。但他們很快發現:替目錄開一條專用存取路徑會造成程式碼重複,更糟的是讓 QUEL 無法查詢目錄。把 metadata 存成一般關聯,等於免費得到一套資料字典系統,也讓所有資料共用同一層存取方法。代價在文章後段浮現:執行期查目錄約佔每次互動固定開銷中的 100 毫秒,而從不把目錄改成 hash 的使用者要一直付這筆帳。
保護、完整性控制與 view 全都以「執行前改寫使用者查詢」實作,而不是事後過濾結果。view 定義、授權規則、完整性約束本質上都只是要併進來的限定條件,因此剖析器裡的單一機制就同時撐起三項功能,而查詢處理器看到的仍是一個可正常最佳化的完整查詢。文章也承認機制掛錯了對象:像 System R 授權工作那樣保護 view 比保護基底關聯乾淨得多,而作者把自己沒有改過來歸咎於純粹的固執。
多變數的 QUEL 查詢,是靠改寫成 QUEL 本身的更簡單命令來處理,主要手法是把某個變數代換成實際的 tuple,使剩下的查詢少一個變數。遞迴的終點是單變數查詢,這也正是系統裡存在「單變數查詢處理器」這一層的唯一理由——是演算法決定了分層,而不是反過來。因為一切都留在同一種語言裡,這個策略既好實作也好最佳化。它的盲點文章講得很直白:無法表達「把兩個關聯都排序再合併」這種對兩變數 equijoin 有時最好的策略。
設計之初就假設同一套存取方法介面會有多種實作,呼叫慣例一致、可任意互換;到 1975 年展示時已有五種(heap、hash、compressed hash、index、compressed index)。使用者完全看不到底層儲存結構,而且不像某些同期系統,INGRES 刻意不提供任何往下層鑽的途徑——這才讓實體資料獨立性名副其實。代價是介面必須在還沒想清楚「限制條件如何最佳化掃描」之前就定案,事後證明並不是他們真正需要的;而像 ISAM 載入器這類工具程式,為了效能仍得穿透這層抽象。
團隊在 System R 式的精密鎖定子系統、對檔案(或檔案集合)的粗粒度實體鎖,以及 predicate lock 之間長考,最後以簡單性而非威力定案。把單一 QUEL 敘述當成不可分割的操作,意味著所有需要的資源都能事先一次要齊,徹底避開死結;交易只要再大一點,這個策略就不成立。在這個單位之下,「粗粒度實體鎖最好」原本只是直覺,後來由鎖定粒度的模擬實驗證實。使用者經驗也支持這個選擇:對多數使用者而言,就算鎖住整個資料庫也是可以接受的。
資料庫管理者擁有存放該資料庫的所有 UNIX 實體檔案,而 INGRES 執行檔使用 UNIX 的 set user id 機制,代表任何使用者執行時都以 DBA 的有效 user id 運行。在作者看來,這是唯一能保證「除了 DBA 之外,誰都只能透過執行 INGRES 才碰得到資料庫」的做法;任何更寬鬆的方案都會讓其他程式直接竄改檔案。於是這項保證建立在作業系統上,而不是建立在資料庫程式的正確性上。它也反過來限制了架構:因為終端監控器允許使用者直接編輯檔案,它必須被推出去成為獨立行程,好讓 INGRES 其餘部分不受它影響。
文章把資料庫效能拆成兩種量法:小型交易的固定開銷,以及單一大查詢的成本。作者主張前者在 PDP-11 環境下與資料模型完全無關——它就是系統呼叫成本、環境切換與驗證成本的總和,因此一套以五個行程執行的網狀模型系統同樣只有每秒 2.5 筆交易。後者確實有一點資料模型的成分,但仍被系統呼叫成本、作業系統的緩衝與排程決策、輸出格式化,以及調校投入了多少所主宰。結論是:拿兩套不同資料模型的系統對打,測到的主要是它們的作業系統與實作者。
出貨版本以五個 UNIX 行程執行,柏克萊實驗版是六個;設計上 DECOMP 應與單變數查詢處理器平行跑,工具程式則放在 overlay 裡。即使是什麼都不做的互動,控制流仍要穿過八個行程:格式化八則訊息、呼叫八次 UNIX 排程器、動用八次 pipe,光這段就佔掉 400 毫秒固定開銷中的 150 到 175 毫秒。又因為行程之間無法共用程式碼,存取方法必須在每個需要它的行程裡各存一份。更麻煩的是,部分行程間訊息傳的是 QUEL 命令的內部樹狀結構,因此系統得寫一支常式把樹狀物件線性化後塞進 pipe,再寫一支反向常式在接收端把樹重建回來——若在同一行程內,這原本只是傳一個指標的函式呼叫而已。
他們沒有像 System R 在 VM/370 上那樣自寫檔案系統,而是直接把存取方法蓋在 UNIX 檔案之上,理由是避免重複作業系統的功能,也讓程式碼比較好外流散布。一個 hash bucket 恰好就是一頁 512 位元組的 UNIX 頁面,因此即使只要找一個 tuple,也一定得搜完整頁。原始的容量假設是:INGRES 處理一頁 512 位元組絕不會超過 30 毫秒,而 UNIX 從磁碟取一頁大約也是這個時間,所以在常見的單磁碟控制器 PDP-11 上系統永遠會是 I/O bound;實測卻發現有不少情況是 CPU bound,於是每頁、每個 tuple 的浪費全都現形。其中兩項後來修掉了——tuple 在記憶體中多搬了一次,以及操作整個 tuple 而非只取需要的欄位——但「hash bucket 等於一頁」被作者稱為設計上根本性的錯誤。
DECOMP 接手剖析後的 QUEL 查詢,用 tuple substitution 一次消去一個變數,把它化約成一連串單變數查詢;OVQP 則針對每個單一關聯的查詢,透過該關聯當下使用的存取方法執行。兩者分屬不同的 UNIX 行程,這正是新增 join 策略昂貴的原因:辨認出某個兩變數 equijoin 該用「兩邊各自排序再合併」並不難,但要實作就得重新設計兩個查詢處理行程之間的介面。第一版的分解是硬幹出來的,在重寫階段大幅改善;剖析器則因為補丁太多而頭重腳輕,最後整個打掉重寫。
既然不可分割的單位是一句 QUEL 敘述,命令就能在開始前把所有需要的資源一次要齊,於是死結根本不會發生,也不需要維護等待圖。鎖是粗粒度的實體鎖——鎖檔案或檔案集合——而不是細粒度的記錄鎖或 predicate lock;後兩者都認真評估過,但被認為對這個環境而言機制太重。當時這個選擇只是直覺,後來由鎖定粒度的模擬實驗加以驗證。實務上使用者也沒有反彈:文章指出,對多數使用者來說,鎖住整個資料庫都是可以接受的替代方案。
所有 QUEL 敘述都要走過 deferred update 機制,因此在敘述執行途中發生軟性當機(磁碟還完好)時,可以乾淨地回復或完成。真正困難的是工具程式執行到一半當機,因為每個工具除了本身的工作外,還會自行改動系統目錄,半途而廢就會讓目錄陷入不一致。復原設計是一支程式:掃過系統目錄一次(最多兩次),不論當時執行的是哪個命令,都能找出所有不一致,然後決定要把該命令倒回去還是往前推完。要讓這種掃描可行,必須替每個工具訂下滴水不漏的目錄操作協定,而把這些協定裝進龐大又乏味的工具程式碼裡,本身就是一大工程。
EQUEL 是把 QUEL 嵌進 C 的前置處理器;C 是唯一可行的宿主語言,因為只有它支援 INGRES 賴以運作的行程間通訊。直譯器當初是為終端上的臨時查詢設計的,因此 EQUEL 程式中的敘述被當成隨手輸入的敘述看待:執行期才剖析,再查系統目錄確認關聯存在、欄位存在、常數型別正確或能正確轉換。光這段驗證每次互動約耗 100 毫秒,行程穿越再吃掉 150 到 175 毫秒,於是形成 400 毫秒的地板與每秒約 2.5 次簡單互動的天花板。直譯還很吃空間:一支 EQUEL 程式的 working set 約 150 KB 再加上程式本身,對小型機器是沉重負擔;文章給出的解法是讓 INGRES 改成可編譯、可直譯兩路並行。
INGRES 與 System R 並列為關聯式系統的兩大源頭,而這篇文章正是那條路線的誠實帳本。柏克萊的程式碼由 Relational Technology Inc. 商品化為 Ingres,後來歷經 Ingres Corporation 與 Computer Associates,最終開源;專案成員也把設計理念帶了出去:曾任首席程式設計師的 Robert Epstein 先後參與創辦 Britton Lee 與 Sybase,而 Sybase 引擎後來被 Microsoft 取得授權,成為 SQL Server 的基礎;另一位首席程式設計師 Eric Allman 則在同一環境中寫出 sendmail。文章第 6 節的計畫化為 Distributed INGRES 與 MUFFIN 資料庫機器,是早期的 shared-nothing 設計,其構想一路延伸到 Gamma 與今日的 MPP 引擎。Stonebraker 在柏克萊的下一個系統 POSTGRES,正是針對本文列出的種種限制而生,後來成為 PostgreSQL——它實作 view 與 row-level security 的查詢改寫機制,血緣直接來自 INGRES 的 query modification。QUEL 輸給了 SQL,但本文的其他判斷都很經得起時間:prepared/compiled statement 已是標配,靜態 ISAM 目錄全面敗給 B-tree,而讓作者改信的自動化實體設計,如今是產品功能之一。此外,這種以數字支撐、毫不遮掩的系統事後檢討本身也成了一種文類,Stonebraker 後來的回顧文章(包括 What Goes Around Comes Around)便是它的直系後代。
“This paper describes the implementation history of the INGRES database system. It focuses on mistakes that were made in progress rather than on eventual corrections.”
“In four years there were between two and five incarnations of all pieces of the system.”
“In summary, I would allege that a comparison of two systems using different data models would result primarily in a test of the underlying operating system and the implementation skill (or man-years allowed) of the designers and only secondarily in a test of the data models.”