天天看點

Implementation of Global Temp Table

一、工作介紹

開發特性過程以及實作特性技術細節:

第一個和Postgre SQL 相關的雲産品是RDS PostgreSQL,是把主備流複制架構的PostgreSQL 放到雲上,做自動售賣。

Implementation of Global Temp Table

核心層面,定期更新PostgreSQL 的大版本和小版本,根據使用者需求支援各類的插件以及開發雲上獨有的插件。比如打通PostgreSQL 和其他雲産品資料通路的插件(Oss_fdw)。

這個版本對PostgreSQL 的核心改動并不大,但和PostgreSQL 的社群交流較多。當使用者提出的新的需求時,首先會從社群中尋找靈感,如果社群在某個版本中支援了該應用,則會建議使用者直接使用這個版本。如果沒有,将會做定制化的開發。

2018 年,開始自研了雲原生資料庫PolarDB,而PolarDB 也有支援PostgreSQL引擎的版本。

Implementation of Global Temp Table

該版本有兩個主要的特點:

首先,它基于計算存儲分離的架構,可以同時支援很多的企業級存儲。在公共雲上,支援PolarStore 分布式的存儲,線上下也支援傳統的塊存儲。這使得存儲上的擴充性有了一個大幅的提高;另一方面,計算節點也支援多讀一個架構,它可以實作擴充讀的能力,使計算能力可擴充,相較于RDS PostgreSQL 有了比較大的提高。

其次,自研了很多進階的SQL 特性,使PolarDB for PostgreSQL 在企業級的應用場景裡,或者傳統的資料庫使用場景有一個高度的Oracle 相容性,更加易于在複雜場景中替換Oracle 應用。

SQL 功能

SQL 大緻分為以下幾個類型:

第一部分的工作相對簡單,比如支援某類驅動,支援功能性的函數和視圖,比如數學函數、字元串處理函數等。

第二部分是SQL 層的一些特性例如聚集函數,或PL/SQL 裡的特性。

相對複雜的第三部分是表級的特性以及SQL 優化相關的特性。

Implementation of Global Temp Table

二、Global Temp table 的實作分享

三個部分

1. 站在Postgre SQL 的肩膀上

2. Global Temp Table 的設計

3. Global Temp Table 在社群Review

什麼是全局臨時表?

Implementation of Global Temp Table

用普通的表做類比,對資料庫中的普通表a 而言,不同的application 連接配接到資料庫中檢視表a 的時候,能夠看到表a 中的所有的資料。

而對于全局臨時表而言,不同的連接配接到資料庫中,都能看到臨時表b。但是,對于臨時表b 中的資料,每個連接配接都隻能看到自己的這一份資料,看不到其他連接配接中的資料,那麼這就是臨時表的定義。

對于會話中的資料生命周期而言,可以選擇綁定事物或者會話,換一種說法也就是我們可以選擇這張表中的會話資料,使得在事務送出的時候被自動清理,或者是在會話退出的時候被自動清理。這就是全局臨時表的定義。

全局臨時表的使用場景?

一般情況下我們使用多表join 再疊加子查詢的方式來實作對複雜資料場景的資料處理。這種做法業務的開發難度會随着SQL 的複雜程度的提高而急劇的提高,最終業務SQL 将轉變得不可運維。

自然的,使用存儲過程加臨時表處理資料的方案,就成為了一個更加合适的選擇。

Implementation of Global Temp Table

具體來說是把複雜SQL 中的邏輯進行拆分,拆分成存入過程中的多個步驟,進而分步驟完成,每個步驟之間,使用臨時表來做資料的交換。

這樣可以使它發揮出臨時表的很多的優勢。

比如,臨時表可以使用輔助索引來做資料處理的加速,由于臨時表在會話之間不共享資料,也很容易開發出并行資料處理的邏輯來進一步的加速業務,整體的開發難度不會随着業務的複雜而急劇的提高。

最後,由于臨時表中的資料支援在特定的時機被自動化清理,那麼開發者不用花費額外的精力去維護臨時表的定義資訊,維護臨時表達索引資訊,以及主動的清理臨時表中的臨時資料。

大量的使用者使用Global Temp Table 處理自己的業務資料。我們調查發現,高達80%的Oracle 資料庫使用者使用了全局臨時表特性,有超過50%的客戶,使用了存儲過程加臨時表結合的方式來完成自己的業務邏輯。在調研Global Temp Table 的現狀時發現,該特性還屬于SQL 标準。幾乎所有的商業資料庫都支援這個特性。

在PostgreSQL 社群裡面,從2007 年到2015 年都陸續有多個核心開發者,嘗試實作該特性,但終究沒有成功。這個特性目前放在PostgreSQL 的TODO LIST 裡。

Implementation of Global Temp Table

如何支援Global Temp Table 特性?

達成一緻的結論:

1. 相對于社群已經支援的臨時表,我們可以叫它Local Temp Table,Global Temp Table 通過持久化表定義的方式,減少系統表的膨脹。

2. Global Temp Table 的一些狀态的資訊,不用存放在系統表中,可以存放在每個會話的記憶體機構中。

Implementation of Global Temp Table

未能達成一緻的問題:

1. 對于這個特性在Global Temp Table 的每個會話中的資料,是否應該有一份為它支援的獨立的統計資訊?

2. 對于資料和資料的事務資訊應該怎麼樣存儲和處理?是否應該為臨時表這麼一種特相對特殊的表開發一種全新的更加簡潔的存儲方案來存放它?

這些問題都暫未達成一緻意見。

Implementation of Global Temp Table

這裡總結了相對于社群已經實作的臨時表Local Temp Table 和我們開發的Global Temp Table 在需求層面的一些差異點。

可以看到Local Temp Table 的表定義和資料,它的生存的周期基本上是一緻的,但是Global Temp Table 的表定義和本地資料則不一緻,GTT 的表定義是持久化的,但是每個會話中的資料各有自己生命周期,這一點帶來了一系列的問題,即是Global Temp Table 最大的實作難點。

要實作Global Time Table 需要解決4 個次元的難題:

1. 存儲層面

2. 中繼資料層面

3. 統計資訊層面

4. 事務層面

Implementation of Global Temp Table

解決難度從低到高,這4 個次元分别又可以展開細化成一些子問題,例如

1. DDL 語句的實作、鎖問題、Cache 問題。

2. 統計資訊和優化器中的統計資訊處理問題。

3. 事務資訊的處理問題和資料的可見性問題、vacuum 問題等等。

這4 個方面的問題,前兩點存儲層面和原資料層面社群已經有了一個比較明确的方向,實作思路都已經非常明确,但是表中資料的統計資訊的處理以及事務資訊的處理還沒有一個明确的結論。

以四個方面具體的闡述一下。( 以下,使用GTT 來簡稱global temp table,用LTT來簡稱社群與已經支援的特性local temp table)

Implementation of Global Temp Table

設計的第一部分:存儲

這部分涉及GTT 的資料存儲的檔案格式和用于暫存資料的Buffer d 的設計。

在這兩部分的設計上大體都複用LTT,但是不同之處是在于同一張GTT,在不同的會話中都可能有一份獨立的存儲和Buffer,而LTT 隻會有一個會話使用。

即在那些已經使用過的會話中有一份存儲和Buffer,沒有使用的會話中則沒有,那麼這就需要使用一套DDL 語句來管理每個會話中的資料。為了達到使用DDL 語句來管理GTT 資料的目的,我設計了一個全局的哈希表,放在了共享記憶體上,用于追蹤和管理GTT 在每個會話中的存儲資訊。當使用者使用DDL 管理GTT 或在每個會話中發生第GTT的第一次DML 時,都會維護這個全局的哈希表。有了哈希表,普通表所支援的DML 和DDL 這些語句都可以在GTT 上全部實作。

極端的情況下,在一個DB 中,我們同時管理1 萬個GTT 和同時建立1 萬個連接配接,這會産生1 萬個會話,這樣的場景最多也隻會使用到12 兆的實體記憶體,是一個可以接受的開銷。

這樣的設計保證了用最低的資源實作了GTT 的整套DDL。

而為了維護GTT 這一份中繼資料和多個會話中的資料的一緻性,增加了一個限制,也就是說當多個會話都在使用這張GTT 的時候,那麼它無法對GTT 做Drop Table 和Alter Table。

設計的第二部分:中繼資料的管理

如何給一個GTT 建立索引?

假設會話1 在GTT A 上正在建立一個索引,而其他的會話也可能同時使用表A,如果會話1 完成了本地索引的建立并把索引的中繼資料更新到系統表中,那麼其他的會話也能馬上看到A 上的索引。

在會話3,這時候會話3 的GTT 有的一份獨立的資料,但這個資料并沒有被建立的索引所維護,那麼我們應該在觸發索引掃描的語句中去新Build 索引A 嗎?這樣DML 中Build 索引會存在問題嗎?

Implementation of Global Temp Table

事實上對于這一個問題,我覺得是有問題的。于是在第一版的設計中就禁止了在多個會話同時使用到表A 的時候,為表A 建立索引。

最新的設計:在會話1 中,當表A 的索引完成建立後,索引資訊更新到catalog 中,其他的會話會看到索引資訊,這時候其他會話會根據自己目前的情況,選擇是否立即使用A 上的索引。

舉例說明

1. 如果會話2 中表A 沒有存放資料,這時激活這個表上的索引是安全的,在這之後表A又來了新的資料,索引是被維護起來的,那麼索引是有效的和可用的。

2. 如果會話3 中表A 已經有資料了,不可能在一個查詢語句或DML 語句觸發Build 一個索引這樣的動作,這是就要失效會話3 中的索引,當會話3 中的GTT 的資料被清理過後,即就是觸發了on commit 子句,在事物送出的時,索引又重新被啟用了,再來新的資料,也能及時被索引記錄,索也能用于GTT 的查詢加速。

這個設計相對就是簡單粗暴的禁止掉索引建立,是一個比較完善的設計。

設計的第三個部分:統計資訊

由于每個會話中資料的彼此獨立,資料的存取又使用了會話内的local buffer,這樣的設計使得沒有一個會話有能力看到同樣一張GTT 表的全局資料。同時我們知道PostgreSQL 的查詢使用代價模型,好的查詢性能基于準确的資料統計資訊,這就使得GTT 需要為每個會話内的資料提供一份的獨立的資料統計資訊。

同時這些本地資料的統計資訊不需要放到全局的系統表中。

Implementation of Global Temp Table

基于這個設計思路,我們設計了一個會話級的本地哈希表,用于儲存這些GTT 表的統計資訊,提供給本地會話内的優化器使用,幫助優化器做GTT 的查詢優化,這就使得GTT 相關的SQL 有了一個優秀的查詢執行路徑,保證了GTT 相關SQL 的高性能。使GTT 的查詢性能和普通的表抹平了差異。

設計的第四個部分:GTT 事物資訊的處理

按照我的計劃,GTT 和普通表都将使用相同的存儲設計,不需要實作一個新的存儲的方式。我們知道PostgreSQL 的傳統行存資料的設計,每一行資料的頭部都儲存了資料的事務資訊。

每個事務号,即XID 占用32Bit 存儲空間,XID 是一類系統資源,一個執行個體中隻能同時存在2 的32 次方減一個XID。

Implementation of Global Temp Table

事務資源的回收通過Vacuum 機制完成,這是PostgreSQL 獨有的機制。

基于前面講到的存儲部分的設計,GTT 在每一個會話中的資料是獨立的,它們自己獨立的事務資訊,自然需要被維護起來。

資料的可見性,依賴資料内的事務資訊,對于具體某個事務的狀态(也就是這個事務是否送出或復原),存儲在CLOG(commit log)中。

如果事務狀态資訊,也就是CLOG,一旦被Vacuum,GTT 在會話中的資料,由于丢失了事務狀态資訊,這份資料則不用了,也等于是造成了資料的丢失。

那麼,我們将會話中資料的事務資訊(這份資料最老的XID relfrozenxid)也同樣儲存在本地的哈希表中,同時維護會話級的GTT 的Oldest relfrozenxid,并且把它放到全局的共享區中。

這樣Vacuum 在清理CLog 時,就有機會考慮到GTT 的事務資訊了。我們用這個設計保證GTT 資料的完整性。

另一個方面,從全局的角度也能實時看到某個會話中GTT 中的資料是否包含了老舊的事務資訊,需要被清理,也利于做事務資訊的全局管理和清理。