跳至主要內容
論文精煉 · Extensible DBMS

Looking Back at Postgres(回顧 Postgres)

回顧柏克萊 Postgres:一套以可擴展性為核心的物件關聯式設計,孕育了 PostgreSQL 與整個世代的資料庫系統。

作者Joseph M. Hellerstein,加州大學柏克萊分校(UC Berkeley) 發表於arXiv:1901.01973,2019 年 1 月;為 Stonebraker 的 Turing Award 紀念文集 Making Databases Work(Morgan & Claypool, 2019)邀稿而寫 年份1986 起
閱讀原始論文 PDF 所有論文

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

這是 Joseph Hellerstein 對 Michael Stonebraker 於 1980 年代中期至 1990 年代中期主持的柏克萊 Postgres 計畫的回憶錄。Postgres 的目標是做一套 one-size-fits-all 的資料庫:保留資料表與宣告式查詢,卻讓幾乎每一層都可擴展——使用者自訂的 abstract data type 與函式、可巢狀的複合欄位、可插拔的存取方法、主動式規則系統、把日誌本身當成資料的 no-overwrite 儲存引擎,以及平行查詢最佳化。文章逐項檢視這些賭注,並直言哪些活了下來、哪些後來被整個拿掉。兩位學生把 Postquel 語言換成 SQL 做出 Postgres95,一群與柏克萊無關的志願者接手成為 PostgreSQL,而同一套架構如今撐起全球第四受歡迎的資料庫,以及超過 26 億美元的併購金額。全文的核心主張是:Postgres 之所以能打破 Fred Brooks 的「第二系統效應」,正是因為架構核心是可擴展性,而不是對功能的自我節制。

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

Stonebraker 先前已靠柏克萊的 Ingres 研究計畫與據此創辦的 RTI 取得巨大成功,因此 Postgres 明擺著就是 Post-Ingres:延續 Ingres 能做的事,再往前推。1980 年代初,他的團隊被微電子產業的 CAD 工具需求牽引:那些應用需要多邊形、矩形、字串等新型別,需要高效的空間搜尋、複雜的完整性約束,還需要同一個實體構造的設計階層與多重表示法。這股壓力催生了 Antonin Guttman 的 R-tree,以及把 abstract data type 硬掛上 Ingres 的原型 ADT-Ingres,後者甚至允許用一段 Quel 查詢當作欄位型別。同一時期,商業廠商正大舉投資高度最佳化的 write-ahead logging 與交易吞吐量以求差異化,而 AI 社群對規則式專家系統的熱情則已近尾聲。Postgres 同時逆著這幾股潮流設計,一個都沒有跟。

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

  • 微電子 CAD 工具需要多邊形、矩形、字串這類新資料型別,需要高效的空間搜尋、複雜的完整性約束,以及同一實體構造的設計階層與多重表示法,而這些都塞不進 Codd 關聯模型的扁平列與欄。
  • 關聯式建模的教條要求把巢狀資料(例如一張採購單連同其產品、數量、價格)拆成扁平的實體表與關聯表,但對像 CAD 電路佈局引擎這種更新極少的應用而言,這種拆解非常不自然。
  • 負責解讀應用專屬型別的程式碼只能待在 DBMS 之上,系統因此被迫「把資料拉到程式碼」而非「把程式碼推到資料」,付出可觀的效能代價。
  • B-tree 與同類結構只支援等值查找與一維範圍查詢,而發明出 R-tree 這種更好的索引本身,並不能解決端到端的系統問題:查詢最佳化器、儲存層與日誌/復原機制都得認得它。
  • 教科書式的最佳化器會把 selection 推到 join 之下並任意排序,一旦 selection 裡含有昂貴的使用者自訂函式,這個假設就崩了——它甚至可能該被拉到 join 之上執行。
  • 在 Stonebraker 看來,IBM 與 Tandem 開創的 write-ahead logging 機制過於複雜,而這種只會在當機後的罕見關鍵情境才被用到的功能,複雜到不該被信賴。

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

物件關聯式,而非物件導向

面對 impedance mismatch,OODB 陣營的解法是讓程式語言物件可持久化;Postgres 反其道而行,保留資料表作為最外層型別、保留宣告式查詢作為介面,然後讓欄位可以裝下複合型別:巢狀的 tuple 或資料表,甚至在最奇特的一種形式裡,用一段查詢宣告式地定義欄位,也就是「Quel as a data type」。這行得通,是因為關聯模型從來沒有真的要求欄位必須是純量:Codd 的模型接受任何帶有述詞的 atomic type,因此擴充型別系統只需要小幅修改中繼資料目錄,而不是換一套架構。Stonebraker 把成果命名為 Object-Relational,並直接繞過 OODB 這個「零億美元市場」。時至今日,幾乎所有商用關聯式系統都是物件關聯式的,PostgreSQL 也在沒有大幅重構的情況下吸納了 XML 與 JSON。

把自訂型別與函式登記進目錄

Postgres 是第一個全面支援這組能力的資料庫系統:不透明的 abstract data type 存在資料庫裡但核心引擎不去解讀它,同時允許查詢呼叫針對這些型別的 user-defined function 與 user-defined aggregate。好處是經典的「把程式碼推到資料」,而它之所以便宜,是因為查詢語法、語意與系統架構全都不用動——只多了關聯式中繼資料目錄的擴充,以及呼叫外部程式碼的機制。Postgres 在壞的一面也超前太多:當時資料庫研究界並不把「把不安全的程式碼上傳到伺服器」當成議題,後來 Oracle 針對 Informix 執行未受保護的自訂 C 程式碼大打負面行銷,造成了實質的商業傷害。作者主張 MapReduce 正是這個想法的再實現:Postgres 的軟體工程觀念,加上 Gamma 與 Teradata 那套平行化。

可擴展的存取方法

Postgres 讓新的索引結構透過一份抽象描述登記進系統,於是 R-tree 不必修改引擎就能加進來;同時它教會查詢最佳化器辨認抽象的 selection 述詞(例如一個範圍選擇),並把它對應到那個被抽象描述的存取方法。這也正是為什麼此處的可擴展性是架構層級而非局部的:這項改動從最佳化器一路貫穿到儲存層,再到日誌與復原機制。這套設計原封不動地活了下來——PostgreSQL 至今仍在同一框架下提供 B-tree、GiST、SP-GiST 與 Gin 索引,其中 GiST 撐起 PostGIS 地理資訊系統,Gin 撐起 PostgreSQL 內建的全文索引。

昂貴述詞的查詢最佳化

一旦 selection 裡可以放進任意昂貴的使用者自訂函式,「把所有 selection 都推到 join 之下」這條經典規則就不再正確:UDF 的執行順序可能左右效能,而夠昂貴的述詞甚至該放在 join 之後執行,也就是 selection pullup。Postgres 是第一個把 UDF 的成本與選擇率記進資料庫目錄的 DBMS,這才讓上述決策變成可計算的問題。最佳化器先求出 selection 的最佳排序,再把這個序列最佳地穿插進計畫搜尋中所考慮的每棵 join tree 的分支上,因此仍完整保留 System R 那套教科書級的動態規劃架構,只多付一點排序成本。

把規則做成資料庫的一等功能

規則式程式設計的理論支線是 Datalog,而 Stonebraker 公開地不喜歡它;Postgres 走的是務實的那一支,也就是後來的 Active Database 與資料庫 trigger。Eric Hanson 與 Spyros Potamianos 的工作產出了 PRS2,它刻意保留兩種實作:一種把規則當成查詢改寫,延續 Stonebraker 在 Ingres 開創的 view 改寫思路;另一種在資料庫內部用鎖在單列層級檢查條件。最後沒有任何一種被宣告為贏家,釋出的系統把兩種都留著。PostgreSQL 今天仍存在的 per-statement 與 per-row trigger 之分,正是這個未決選擇的直系後裔。

no-overwrite 儲存:日誌就是資料

Stonebraker 拒絕再實作一次 write-ahead log,改而把主要儲存與歷史日誌統一成單一、簡單的磁碟表示:每筆記錄是一條以 transaction ID 標記的版本鏈結串列,額外需要的中繼資料只有一份已提交 transaction ID 清單與對應的實際時間。復原因此大幅簡化,因為不需要把日誌表示「翻譯」回主要表示——已提交的版本本來就躺在資料該在的地方。同一結構順帶白送 time travel:查詢可以指定「as of 某個時間點」,看到當時已提交的版本。1991 年的設計又補上以非揮發性記憶體保存 commit 狀態。

The Wei Hong Optimizer

原則上,平行化會把計畫空間炸開:傳統的每一項選擇(資料存取、join 演算法、join 順序)都要再乘上每一種可能的平行化方式。XPRS 把問題一刀切成兩半:先用 System R 風格的單機最佳化器跑出計畫,再依資料配置與系統組態,替該計畫中每個運算子安排平行度與擺放位置,藉此「平行化」它。這個做法是啟發式的,可能錯過整合式搜尋才找得到的計畫,但它讓平行化成為傳統查詢最佳化的加法成本而非乘法成本,並成為業界許多平行查詢最佳化器的標準做法。

運作方式 — 具體的機制

把型別登記進目錄

程式設計者定義一個 abstract data type,方式是提供輸入/輸出轉換函式,以及作用在該型別上的自訂函式與運算子,這些登記資訊就和內建型別並列存放在關聯式中繼資料目錄裡。執行器透過一套通用的呼叫機制去觸發這些外部程式碼,因此 ADT 欄位對引擎而言始終是不透明的——它只是存放一串自己從不解讀的位元組。此外,Postgres 還把每個自訂函式的成本與選擇率寫進目錄,供最佳化器日後讀取。因為一切都掛在目錄上,新增一個型別完全不必更動剖析器、計畫表示法或儲存格式。

插入一個存取方法

像 R-tree 這樣的存取方法是以抽象方式登記的:描述它能回答哪些述詞,而非揭露內部結構。最佳化過程中,規劃器抽象地檢視 selection 述詞(例如辨認出一個範圍選擇),再與宣稱支援該類述詞的已登記存取方法比對,於是新索引不必動最佳化器手術就能被使用。原始工作留下的缺口是並行控制:鍵值缺乏一維排序,B-tree 式的鎖定策略無法套用,計畫大致把這個問題擱置了。Marcel Kornacker 後來的論文工作,正是為這個介面在 GiST 上提供了樣板化的並行控制與復原機制。

把昂貴的 selection 擺進計畫

最佳化器先依目錄中記錄的成本與選擇率,導出查詢中各 selection 述詞的最佳排序,得到單一條排好序的序列。接著它把這個序列穿插進 System R 動態規劃搜尋所列舉的每棵 join tree 的分支上,這正是讓某個述詞在成本划算時得以在 join 之後才被求值的關鍵。由於排序只算一次、在所有列舉出的樹之間重複使用,額外開銷只是一點排序成本,而不是搜尋空間的爆炸。這項功能有進到 Illustra,但很早就在 PostgreSQL 原始碼樹中被關閉,主因是當時並沒有具說服力的昂貴 UDF 使用情境。

PRS2:兩套規則引擎並存

在改寫路徑上,「on condition then action」形式的規則被重寫成「on query then 改寫成另一個查詢並改為執行它」,直接沿用 Stonebraker 在 Ingres 為 view 改寫打造的機制,因此「在 Mike 的獲獎清單加一列」可以被改寫成「把 Mike 的薪水調高 10%」這種完全不同的更新。在實體路徑上,條件是用資料庫內部的鎖在單列層級檢查;當查詢碰到這種鎖時,系統不像傳統並行控制那樣等待,而是直接執行對應的動作。兩條路徑都出現在釋出的系統中。Postgres 3.1(約 1991 年)留存至今、勸退後人別碰 tuple 層級規則系統的原始碼註解,忠實記錄了那條單列路徑有多難維護。

版本化的 no-overwrite tuple

更新從不覆寫記錄,而是在該記錄的版本鏈結串列上追加一個新版本,並蓋上寫入交易的 transaction ID。可見性靠比對已提交 transaction ID 清單來判定,而隨附的實際時間讓查詢能索取「某一時刻」的狀態,time travel 就是這樣實作出來的。因為已提交的資料本來就位於最終位置,當機復原不需要任何把日誌記錄翻譯回主要儲存的階段。代價是被取代的舊版本會不斷累積、需要昂貴的背景重組,而且廠商在 write-ahead log 成熟後所發展的 log shipping 式交易複寫,在這套方案裡會非常難做。

把已完成的計畫平行化

XPRS 讓一般的循序最佳化器完整跑完,產出唯一一份最佳單機計畫,之後才對這棵計畫樹做一次平行化處理。這一趟會依資料的實體配置與這台共享記憶體機器的組態,替每個運算子指定平行度與擺放位置。搜尋階段本身完全不變,因此總計畫空間仍是「循序計畫空間 + 一個後處理」,這正是成本維持加法而非乘法的原因。

向下延伸到 tertiary storage

Project Sequoia 需要多達 100 TB 的數位衛星影像,遠超過當時磁碟合理可存的量,於是 Sunita Sarawagi 的論文工作把整條堆疊往下延伸到裝滿光碟與磁帶的機械手臂 jukebox。大型多維陣列被切成 chunk,會一起被取用的 chunk 就存在一起,並且複製 chunk 讓同一塊資料擁有多個實體「鄰居」。接著磁碟被當成 tertiary storage 的快取,而查詢最佳化與查詢排程都必須把 tertiary storage 漫長的載入時間、以及命中磁碟快取的價值一併算進去——這同時改變了選定的計畫,也改變了該計畫被排程執行的時間點。與此並行的另一項工作是 Mike Olson 的 Inversion 檔案系統,把 UNIX 檔案系統抽象疊在 RDBMS 之上;Stonebraker 稱之為「一個直截了當的練習」,結果既不直截了當,也沒能存活下來。

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

  • 這是一篇回憶錄而非實驗論文,因此它最主要的量尺是採用度:文中指出 PostgreSQL 是全世界最受歡迎的獨立開源資料庫系統,並在整體排名中位居第四,僅次於 Oracle、MySQL 與 MS SQL Server,且在 2017 與 2018 兩年都是全球成長最快的資料庫系統。
  • Postgres 系出同源公司已揭露或可估算的併購金額合計超過 26 億美元:Illustra 於 1997 年被 Informix 以估計 4 億美元收購、Netezza 被 IBM 收購時價值 17 億美元、Greenplum 於 2010 年被 EMC 以估計 3 億美元收購、Aster Data 於 2011 年被 Teradata 以 2.63 億美元收購。ParAccel 也被 Actian 收購,但金額未揭露,因此不計入上述總額。
  • 架構的延續性是本文另一個量測:25 年後,PostgreSQL 的原始碼目錄結構、行程結構與資料結構,仍與約 1991 年的 Postgres 3.1 版本相近到——熟悉現行原始碼的開發者幾乎可以毫無障礙地在舊程式碼中穿梭。
  • 可擴展存取方法這一層至今仍在生產環境承重:PostgreSQL 的 B-tree、GiST、SP-GiST 與 Gin 索引都經由它提供,其中 GiST 支撐 PostGIS 地理資訊系統,Gin 支撐 PostgreSQL 內建的全文索引。
  • Postgres 是第一個把使用者自訂函式的成本與選擇率記入資料庫目錄的 DBMS,而據此打造的昂貴述詞最佳化器完整保留了 System R 那套教科書式的動態規劃架構,只額外付出一小段排序成本來把 selection 排好。
  • XPRS 那套兩階段平行最佳化成為業界許多平行查詢最佳化器的標準做法;而以 Postgres 為基礎的新創 Greenplum 與 Aster 在 2007 年前後證明,平行化後的 Postgres 對多數客戶而言,功能與實用性都遠勝 MapReduce。

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

  • 論文自承:Postgres 的儲存系統在效能上從未出色,版本化與 time travel 後來被移出 PostgreSQL,改以 write-ahead logging 取代;而廠商在 write-ahead log 成熟後順勢發展的 log shipping 式交易複寫,在 Postgres 的方案裡會相當難做。
  • 作者在註腳中承認、並被後見之明放大的問題:PostgreSQL 至今在交易處理上仍不算快,因為它為了提供 MVCC 保留了 Postgres tuple 的大部分儲存額外開銷——而 MVCC 從來不是柏克萊計畫的目標——結果是用不少額外 I/O 去模擬 Oracle 的 snapshot isolation,卻既不支援 time travel,也沒有簡單的復原機制。
  • 論文自承:可擴展存取方法的原始工作幾乎沒有處理並行控制,因為鍵值缺少一維排序使得 B-tree 式鎖定無法套用;這些困難的並行與復原問題,要到 Kornacker 為 GiST 提出樣板化解法後才被解決。
  • 論文自承:查詢改寫與單列鎖定兩種規則實作,最後都無法被宣告為贏家;所有規則程式碼最終在 PostgreSQL 中被丟棄重寫,而 trigger 在實務上被謹慎少用,因為規則一多,彼此的交互作用就混亂到難以承受,且 trigger 本身仍相對耗時。
  • 由後續發展揭露:PostgreSQL 最關鍵的限制是無法向外擴展成平行的 shared-nothing 架構,因此每個重要的商業分支都得自己補上;作者也惋惜這件事沒有在 2000 年代初以真正的開源方式完成。同樣地,Fast Path 介面只讓 Postgres 在學術 OODB 基準測試上表現尚可,從未真正解決 impedance mismatch,而 Inversion 檔案系統在實務上並未存活。

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

PostgreSQL 是最直接的後裔:Andrew Yu 與 Jolly Chen 把 Postquel 換成可擴展的 SQL 變體做出 Postgres95,一支與柏克萊無關的志願者「臨時球隊」把程式碼接手成為 PostgreSQL,自 1995 年照看至今;而 Heroku 在 2010 年把它選為平台預設資料庫,也一併把 Ruby on Rails 與 Django 帶了過來。商業支線則從 Illustra 開始——其 DataBlades 成為 Informix Universal Server——接著是 Netezza 以 FPGA 打造的平行資料倉儲、Greenplum 的 shared-nothing 分支及其 Orca 最佳化器與 MADlib 機器學習函式庫、EnterpriseDB 的 Oracle 相容版、Aster Data 的 SQL 與 MapReduce 分析、技術成為 AWS Redshift 的 ParAccel,以及自 2016 年起純以 PostgreSQL 擴充 API 提供的 CitusDB。可擴展性架構本身也成了業界常態:各大廠商如今都能在伺服器內執行使用者自訂函式,幾乎所有商用關聯式系統都是物件關聯式的,而 trigger 早已寫進 SQL 標準。文中認為「來得太早」的那些想法,也不斷換個名字回來:MapReduce 與 Big Data 堆疊本質上就是託管在查詢框架裡的使用者程式碼,XQuery 與今日的 JSON 查詢語言重演了 Postquel 的複合物件,而具體化視圖維護、Complex Event Processing 與串流查詢都是規則系統工作的延伸。連旁支的賭注也留下後代:GiST 從可擴展存取方法介面長出來,如今驅動 PostGIS;而與 Postgres 同期在柏克萊誕生的 Margo Seltzer 的 BerkeleyDB,則預示了 Dynamo、MongoDB 與 Cassandra 這些分散式 key-value 儲存。

論文原文 — 逐字引用

“Postgres 是 Michael Stonebraker 最有野心的計畫——他傾力打造一套 one-size-fits-all 資料庫系統的宏大嘗試。”

§1 Opening

“說到底,這個想法就是把資料庫中的每一筆記錄,維持成一條以 transaction ID 標記的版本鏈結串列——某種意義上,端看你的觀點,這是「把日誌當成資料」,或是「把資料當成日誌」。”

§2.3

“Postgres 是為可擴展性而設計的,而那個設計是健全的。當可擴展性成為架構核心,你就能放手發揮創意,不必那麼擔心紀律:你可以嘗試許多擴充,讓強的那些勝出。”

§4 Lessons

術語 — 依本篇論文的用法

Object-Relational(物件關聯式)
Stonebraker 為以下做法所取的名號:以使用者自訂型別、函式與巢狀欄位等物件導向特性去擴充關聯式資料模型與宣告式查詢語言,而不是像 OODB 陣營那樣讓程式語言物件可持久化。
Abstract Data Type (ADT)
由使用者提供、存放在資料庫中但核心系統不去解讀的型別;引擎只知道如何把它搬進搬出,以及有哪些已登記的函式能作用其上。
User-Defined Function (UDF)
登記到系統中、供查詢對 ADT 欄位呼叫的應用程式碼。Postgres 同時支援 user-defined aggregate,並且是第一個把每個函式的成本與選擇率記入目錄的 DBMS。
複合物件(complex object)
值本身即為巢狀結構的欄位,可以是 tuple 或資料表;在 ADT-Ingres 一脈中甚至可以是一段被當成資料型別的 Quel 查詢,讓非第一正規化的資料能住在一般的關聯式資料表裡。
可擴展存取方法(extensible access method)
透過一份「我能回答哪些述詞」的抽象描述登記進系統的索引結構,使查詢最佳化器能把抽象的 selection 述詞對應到它。R-tree 是主要推動範例,GiST 則是後來的一般化成果。
no-overwrite 儲存
Postgres 的儲存紀律:更新不就地修改資料,而是在該筆記錄以 transaction ID 標記的版本鏈上追加一個新版本,使主要資料與歷史日誌成為同一份結構。
Time travel(時光旅行查詢)
以「as of 某個過去時間點」執行查詢,做法是比對已提交 transaction ID 清單與其時間戳記,挑出當時已提交的 tuple 版本。
Fast Path
直接暴露資料庫儲存內部的 C/C++ 介面,用來讓 Postgres 跳過查詢剖析與最佳化,在 OODB 產品自家的基準測試上與之競爭。
The Wei Hong Optimizer
Stonebraker 為 XPRS 這套做法所取的名字:先把查詢當成單機查詢來最佳化,再替這份完成的計畫安排各運算子的平行度與擺放位置,藉此平行化。

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

在時間軸上查看