深入淺出openGauss的執(zhí)行器基礎(chǔ)
火山模型
執(zhí)行器各個(gè)算子解耦合的基礎(chǔ)。對(duì)于每個(gè)算子來說,只有三步:
向自己的孩子拿一個(gè) tuple。即調(diào)用孩子節(jié)點(diǎn)的 Next 函數(shù);
執(zhí)行計(jì)算;
向上層返回一個(gè) tuple。即當(dāng)前節(jié)點(diǎn) Next 函數(shù)的返回結(jié)果。

所以整個(gè)執(zhí)行器的內(nèi)核可以用下面這個(gè)偽代碼來表達(dá)。
這種模型的好處是:
設(shè)計(jì)簡(jiǎn)單,算子解耦,互相不依賴;
內(nèi)存使用量小,沒有物化的情況下,每次只消耗一個(gè) tuple 的內(nèi)存。
Tuple 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
兩個(gè)算子之間的傳遞的都是 tuple。所以 Tuple 數(shù)據(jù)結(jié)構(gòu)是整個(gè)執(zhí)行器的核心,也是執(zhí)行器和存儲(chǔ)引擎交互的數(shù)據(jù)結(jié)構(gòu)。
先看下幾個(gè)具體數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系(附上關(guān)鍵的變量,非全部)
以上圖為例,最下層的 seqscan,會(huì)調(diào)用存儲(chǔ)引擎的?heap_getnext?函數(shù)(astore)。
以上就是存儲(chǔ)引擎和執(zhí)行引擎之間 tuple 怎么交互的。總結(jié)下就是存儲(chǔ)引擎會(huì)把實(shí)際數(shù)據(jù)所在的地址傳給上層,最后執(zhí)行引擎拿到的結(jié)構(gòu)體為TupleTableSlot。?
所以,執(zhí)行引擎只關(guān)心?TupleTableSlot?這一個(gè)結(jié)構(gòu)體即可。
一個(gè)很自然的問題就是,如果算子之間的 Tuple 都是深拷貝傳遞,對(duì)于較大的 tuple 來說(包含 varchar 類型),性能很差。因此,PG 中的 tuple 分了4類(詳見頭文件tuptable.h):
第一類 tuple 是 Disk buffer page 上的 physical tuple,就是前文的 HeapTuple。Buffer 一定要 pin 住。這種 Tuple 可以直接根據(jù)頭地址進(jìn)行訪問。
內(nèi)存中的組裝的tuple,格式和文件上 tuple 完全一樣,也是進(jìn)行過壓縮。這種也算是 physical tuple,可以直接用地址。?
Minimal physical tuple,也是內(nèi)存中的,唯一區(qū)別在于沒有系統(tǒng)列(xmin、xmax 等)。
?virtual tuple,只記錄每一個(gè)屬性數(shù)據(jù)的地址,并沒有深拷貝,而是直接通過地址來訪問?,F(xiàn)在約定的是,當(dāng)一個(gè)算子向上層吐一個(gè) tuple,直到它下次被調(diào)用時(shí),該tuple所在的內(nèi)存不會(huì)被釋放。
對(duì)于查詢來說,第一類和第四類最為常見。2、3兩類會(huì)在物化的時(shí)候使用,比如 CTEScan、HashJoin 建立 hash 表的時(shí)候,相當(dāng)于深拷貝。性能比較敏感的場(chǎng)合,盡量避免2、3類 tuple的使用。
slot 的創(chuàng)建一般通過?ExecAllocTableSlot、ExecSetSlotDescriptor?兩個(gè)函數(shù)來分配內(nèi)存和初始化信息。在執(zhí)行器初始化階段,每個(gè)算子會(huì)分配相應(yīng)的 slot。
以上圖為例,
SeqScan 算子有一個(gè) tupleTableSlot,是一個(gè) physical tuple,指向的是 Buffer 中的地址。
向上層返回自己的 tuple slot;
HashJoin 一個(gè)一個(gè)拿到 內(nèi)表的 tuple slot,需要建立 hash 表,所以創(chuàng)建了一個(gè) minimal physical tuple,復(fù)制內(nèi)表的 tuple 內(nèi)容;
Hash表建立后,HashJoin 算子然后一個(gè)一個(gè)拿到外表的 tuple slot,做 join 計(jì)算。
HashJoin 自己有一個(gè) tuple slot,如果碰到匹配,則把自己的 tuple slot 設(shè)置成 virtual tuple,其中的 tts_values 指向的是孩子節(jié)點(diǎn)的 tuple 中的地址。
再向上返回。
其中,外表的內(nèi)容指向的是 Buffer 上的physical tuple, 內(nèi)表的內(nèi)容指向的是 hash 表中的 minimal physical tuple;
當(dāng) HashJoin 被再次調(diào)用時(shí),它會(huì)重置 tuple slot。因?yàn)槭?virtual tuple,所以沒做任何事情。然后 HashJoin 會(huì)調(diào)用 SeqScan 拿下一條tuple;
SeqScan 被再次調(diào)用時(shí),也會(huì)重置 tuple slot。
因?yàn)槭?physical tuple,它需要釋放之前的 Buffer。
(當(dāng)然,如果一直是同一個(gè) Buffer 不會(huì)反復(fù) pin/unpin,這是存儲(chǔ)引擎的優(yōu)化)。
條件計(jì)算
Expr 和 Var
執(zhí)行器每個(gè)算子會(huì)對(duì)底下傳上來的 tuple 進(jìn)行計(jì)算和過濾。比如 NestLoop 需要計(jì)算內(nèi)外表傳上來的 tuple 是否滿足 join 條件。
這時(shí)候需要引出第二個(gè)重要的概念,表達(dá)式的抽象。個(gè)人理解,任何對(duì)數(shù)據(jù)的獲取操作都可以認(rèn)為是表達(dá)式。
Var/Const 也是表達(dá)式的一種。Var 表示直接從 tuple 中獲取數(shù)據(jù),Const 表示的是直接獲取一個(gè)常數(shù)。
每一個(gè)表達(dá)式會(huì)對(duì)應(yīng)一個(gè) ExprContext,ExprContext 中會(huì)記錄計(jì)算所需要的所有數(shù)據(jù),一般是孩子節(jié)點(diǎn)返回的 tuple。
表達(dá)式本身,在執(zhí)行器中用 ExprState 來表示。里面重點(diǎn)是表達(dá)式的計(jì)算函數(shù)
總結(jié)下,ExprState?結(jié)構(gòu)體表示表達(dá)式計(jì)算的邏輯,ExprContext結(jié)構(gòu)體表示的是表達(dá)式計(jì)算要用到的數(shù)據(jù)。
?從 OpExpr 可以看出,ExprState 本身也是一棵樹。一直遞歸調(diào)用 ExecEvalExpr 來獲取最終的結(jié)果。
需要注意的是,執(zhí)行樹中除了葉子節(jié)點(diǎn)上的掃描節(jié)點(diǎn),其它節(jié)點(diǎn)的數(shù)據(jù)都來源于孩子節(jié)點(diǎn)。
所以,這些計(jì)算節(jié)點(diǎn)上的 Var,不能直接指向某個(gè) table,而是需要指向的是內(nèi)表還是外表的 tuple slot。
因此,在優(yōu)化器最后的階段,set_plan_refs?函數(shù)中,會(huì)把中間節(jié)點(diǎn)的 Var 的 varno 改寫成特定的值。
而 Var 的表達(dá)式處理函數(shù)?ExecEvalScalarVar?也是根據(jù)這個(gè)信息決定去找 ExprContext 中的 哪個(gè) tuple slot。

表達(dá)式如下:

示例1?filter
以 SeqScan 為例,在優(yōu)化器階段, SeqScan 上會(huì)有一個(gè) qual,表示過濾條件。在執(zhí)行器階段會(huì)生成一個(gè)對(duì)應(yīng)的 ExprState,用于計(jì)算。
示例2??join
以 Nestloop 為例,優(yōu)化器結(jié)束的時(shí)候,join 節(jié)點(diǎn)會(huì)有一個(gè) joinqual 表示 join 條件。
示例3??index scan & index only scan
之前很多人搞不清楚這里面 index cond/ filter 是什么關(guān)系。但是,通過執(zhí)行器源碼很容易得知它們的用處。先看 IndexOnlyScan
總結(jié):
Index Cond 是用來做 btree 掃描的 key,定位到第一個(gè) IndexTuple。存儲(chǔ)引擎中用
之后索引掃描會(huì)順著 btree 的鏈表掃描所有的葉子頁面,對(duì)葉子頁面上的每一個(gè) tuple 用 ScanKey 檢查是否滿足條件,滿足再返回
Filter 是在 執(zhí)行器層用,針對(duì) HeapTuple 再做一次過濾
IndexOnlyScan 和 Index Scan的唯一區(qū)別是,IndexOnlyScan 的HeapTuple是根據(jù)IndexTuple直接構(gòu)建的,不需要回表,其它邏輯是一樣。
所以理論上講 IndexOnlyScan 不應(yīng)該出現(xiàn) filter,上述場(chǎng)景可能有改進(jìn)空間。