跳至主要內容
論文精煉 · Query processing

Retrospection on a Database System(一套資料庫系統的回顧)

一份坦白的工程回顧:打造能真正跑起來的關聯式 DBMS 究竟付出多少代價,又有哪些設計根本是錯的。

作者Michael Stonebraker(加州大學柏克萊分校) 發表於ACM Transactions on Database Systems 5(2),1980 年 6 月,225-240 頁 年份1972–1976
閱讀原始論文 PDF 所有論文

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

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 上自行實作檔案系統。

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

  • PDP-11 的 16 位元位址空間放不下整個系統,INGRES 只好拆成五個互相合作的 UNIX 行程(柏克萊實驗版是六個),連一個什麼都不做的互動都得依序穿過八個行程。
  • 每一次互動都在執行期才做剖析、查系統目錄驗證、決定執行策略;即使是透過 EQUEL 嵌在 C 程式裡、在迴圈中重複執行的同一句簡單更新,也照樣重來一遍。
  • 專案目標一再擴張——並行控制、當機復原、分散式資料庫在最初設計裡根本不存在——每擴張一次就逼著他們重寫系統的一大塊。
  • 程式碼是好幾個人各自隨性堆出來的,每人有自己的風格、變數命名習慣與慣用函式庫,結果除了原作者以外沒人維護得動;又因為預期程式活不久,文件幾乎完全沒寫。
  • UNIX 對其檔案系統既無並行控制也無當機復原,檔案上限 16 MB、頁面只有 512 位元組,而且邏輯頁到實體頁的對應效率不佳。
  • 資料庫管理者(DBA)並不可靠:系統目錄被頻繁且可預測地存取,理應在大小穩定後改為 hash,但有些使用者始終沒做,系統不會壞,只會愈跑愈慢。

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

系統目錄就是一般的關聯

團隊最初考慮替資料字典設計專用的非關聯式結構,因為那樣比較快。但他們很快發現:替目錄開一條專用存取路徑會造成程式碼重複,更糟的是讓 QUEL 無法查詢目錄。把 metadata 存成一般關聯,等於免費得到一套資料字典系統,也讓所有資料共用同一層存取方法。代價在文章後段浮現:執行期查目錄約佔每次互動固定開銷中的 100 毫秒,而從不把目錄改成 hash 的使用者要一直付這筆帳。

查詢改寫(query modification)

保護、完整性控制與 view 全都以「執行前改寫使用者查詢」實作,而不是事後過濾結果。view 定義、授權規則、完整性約束本質上都只是要併進來的限定條件,因此剖析器裡的單一機制就同時撐起三項功能,而查詢處理器看到的仍是一個可正常最佳化的完整查詢。文章也承認機制掛錯了對象:像 System R 授權工作那樣保護 view 比保護基底關聯乾淨得多,而作者把自己沒有改過來歸咎於純粹的固執。

以 tuple substitution 做分解

多變數的 QUEL 查詢,是靠改寫成 QUEL 本身的更簡單命令來處理,主要手法是把某個變數代換成實際的 tuple,使剩下的查詢少一個變數。遞迴的終點是單變數查詢,這也正是系統裡存在「單變數查詢處理器」這一層的唯一理由——是演算法決定了分層,而不是反過來。因為一切都留在同一種語言裡,這個策略既好實作也好最佳化。它的盲點文章講得很直白:無法表達「把兩個關聯都排序再合併」這種對兩變數 equijoin 有時最好的策略。

可互換的存取方法

設計之初就假設同一套存取方法介面會有多種實作,呼叫慣例一致、可任意互換;到 1975 年展示時已有五種(heap、hash、compressed hash、index、compressed index)。使用者完全看不到底層儲存結構,而且不像某些同期系統,INGRES 刻意不提供任何往下層鑽的途徑——這才讓實體資料獨立性名副其實。代價是介面必須在還沒想清楚「限制條件如何最佳化掃描」之前就定案,事後證明並不是他們真正需要的;而像 ISAM 載入器這類工具程式,為了效能仍得穿透這層抽象。

以單一 QUEL 敘述為交易

團隊在 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,再寫一支反向常式在接收端把樹重建回來——若在同一行程內,這原本只是傳一個指標的函式呼叫而已。

建在 UNIX 檔案系統之上的存取方法

他們沒有像 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;後兩者都認真評估過,但被認為對這個環境而言機制太重。當時這個選擇只是直覺,後來由鎖定粒度的模擬實驗加以驗證。實務上使用者也沒有反彈:文章指出,對多數使用者來說,鎖住整個資料庫都是可以接受的替代方案。

deferred update 與工具程式的當機復原

所有 QUEL 敘述都要走過 deferred update 機制,因此在敘述執行途中發生軟性當機(磁碟還完好)時,可以乾淨地回復或完成。真正困難的是工具程式執行到一半當機,因為每個工具除了本身的工作外,還會自行改動系統目錄,半途而廢就會讓目錄陷入不一致。復原設計是一支程式:掃過系統目錄一次(最多兩次),不論當時執行的是哪個命令,都能找出所有不一致,然後決定要把該命令倒回去還是往前推完。要讓這種掃描可行,必須替每個工具訂下滴水不漏的目錄操作協定,而把這些協定裝進龐大又乏味的工具程式碼裡,本身就是一大工程。

EQUEL 路徑與 400 毫秒的去向

EQUEL 是把 QUEL 嵌進 C 的前置處理器;C 是唯一可行的宿主語言,因為只有它支援 INGRES 賴以運作的行程間通訊。直譯器當初是為終端上的臨時查詢設計的,因此 EQUEL 程式中的敘述被當成隨手輸入的敘述看待:執行期才剖析,再查系統目錄確認關聯存在、欄位存在、常數型別正確或能正確轉換。光這段驗證每次互動約耗 100 毫秒,行程穿越再吃掉 150 到 175 毫秒,於是形成 400 毫秒的地板與每秒約 2.5 次簡單互動的天花板。直譯還很吃空間:一支 EQUEL 程式的 working set 約 150 KB 再加上程式本身,對小型機器是沉重負擔;文章給出的解法是讓 INGRES 改成可編譯、可直譯兩路並行。

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

  • 原型每次互動有約 400 毫秒(40 萬條指令)的固定開銷,使簡單敘述的吞吐量被壓在每秒約 2.5 次互動,無論敘述多麼瑣碎。
  • 這段開銷可拆解為:執行期對系統目錄做有效性驗證約 100 毫秒,以及八段行程控制路徑上的行程間通訊約 150 到 175 毫秒。
  • 一支 EQUEL 程式的 working set 約為 150 KB 加上程式本身;又因為行程之間無法共用程式碼,存取方法必須在每個需要的行程裡重複存放。
  • 設計假設 INGRES 處理一頁 512 位元組不會超過 30 毫秒(約等於 UNIX 取一頁的時間),因此在單磁碟控制器的 PDP-11 上理應永遠是 I/O bound;實際上有相當多情況是 CPU bound。
  • 系統約有 50 萬位元組原始碼,除了程式內註解幾乎沒有文件;撰稿時約有 100 位使用者(5 到 10 位重度使用者,加上約 90 位輕度使用者,多為大學),文中另一處則提到 106 位使用者,其中不少人用的是 PDP-11/34 與 11/40。
  • 四年之間,系統的每個部分都經歷過二到五個版本,大致每年重寫一大塊;其中相當於一次完整重寫的工作量,被歸因於一開始沒有訂定程式撰寫規範,而使用者則回報應用程式的開發速度可達原先預期的十倍。

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

  • 作者自承:讓 hash bucket 恰好等於一頁 UNIX 頁面,代表就算只找一個 tuple 也要搜完整整 512 位元組;相對於已修正的「記憶體中多搬一次 tuple」與「操作整個 tuple 而非需要的欄位」,這一項被稱為設計上根本性的錯誤。
  • 作者自承:採用載入時建好、之後不再更動的靜態索引目錄(而非 B-tree 式動態目錄),使 DBA 得定期重建目錄並自行猜測資料頁初始裝載率;而 overflow page 會讓緩衝需求無法預測,偏偏位址空間小到根本容納不下。
  • 作者自承:分解法無法表達兩變數 equijoin 的 sort-merge 策略,要補上就得重畫兩個查詢處理行程之間的介面;同樣地,保護掛在基底關聯而非 view 上,作者歸咎於自己的固執;更新則是「無聲」的,只回一句 done,不說改了哪些或幾筆 tuple。
  • 作者自承:整個系統都是直譯執行,因此宿主語言程式重複送出的參數化敘述,每次都得完整付一次剖析、驗證與最佳化成本;文章表示專案當時才剛開始把 INGRES 改成可編譯、可直譯兩路並行。
  • 後續發展揭露的問題:「資料模型的影響遠小於作業系統與實作投入」這個主張,在每秒 2.5 筆交易的 PDP-11 系統上成立,卻無法一般化——System R 的編譯式執行計畫與成本式查詢最佳化之所以成為業界常態,正是因為它們直接攻擊本文視為環境宿命的那些開銷;而且本文的證據基礎只有單一專案且使用者經驗有限,作者自己也點明「真正的陪審團——真實使用者——尚未做出完整判決」。

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

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.”

§3.2

“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.”

§5.3

術語 — 依本篇論文的用法

QUEL
INGRES 的資料子語言,其查詢部分鬆散地以 Codd 的 DSL/Alpha 為基礎,但沒有量詞(quantifier)。原本以標點符號為主的語法在 1975 年被改成關鍵字導向,這也是使用者語言最後一次重大改動。
EQUEL
把 QUEL 敘述嵌入 C 程式的前置處理器。C 是唯一可行的宿主語言,因為只有它支援 INGRES 運作所必需的行程間通訊。
Decomposition(分解)
把多變數的 QUEL 命令改寫成一連串以 QUEL 本身表達的較簡單命令的查詢處理策略。它優雅且易於最佳化,但無法表達 sort-merge equijoin。
Tuple substitution(tuple 代換)
分解法的核心步驟:把查詢中某個變數代換成實際的 tuple,使剩餘查詢少一個變數。反覆代換可將任何查詢化約成單變數查詢。
OVQP(單變數查詢處理器)
負責透過關聯當下使用的存取方法,執行單一關聯查詢的模組。它之所以成為系統中獨立的一層,純粹是因為分解法需要這樣一層。
Query modification(查詢改寫)
在剖析階段改寫使用者查詢,把 view 定義、保護限定條件與完整性約束併進去。一套機制因此同時實作三項功能,而最佳化器看到的仍是一個普通查詢。
System catalogs(系統目錄)
INGRES 的資料字典,以一般關聯形式儲存,因此可用 QUEL 查詢、由同一套存取方法管理。目錄一開始是 heap,大小穩定後應改成 hash。
Deferred update(延後更新)
所有 QUEL 敘述都必須經過的機制,使得敘述執行途中若發生軟性當機(磁碟仍完好),能夠乾淨地復原。
Static directory(靜態目錄)
在載入時一次建好、之後不再修改的索引目錄,如 ISAM。文章將它與 B-tree 這類動態目錄對比,並認為當初該選動態的那一邊。

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

在時間軸上查看