跟我一塊兒讀postgresql源碼(十五)——Executor(查詢執行模塊之——control節點(上))

控制節點

控制節點用於完成一些特殊的流程執行方式。因爲PostgreSQL爲査詢語句生成二叉樹狀的査詢計劃,其中大部分節點的執行過程須要兩個之內的輸入和一個輸出。但有一些特殊的功能爲了優化的須要,會含有特殊的執行方式和輸人需求(例如對於update、INSERT和DELETE,在普通的SELECT基礎上有一個ModifyTable節點,UNION操做在一個計劃節點就執行多個表(大於2)的合併,Append節點並未把UNION涉及的多個表放在孩子節點中,而是將這些表組成一個鏈表放在Append節點的appendplans字段中,處理時依次處理該鏈表中的節點獲取輸入),這一類的節點被稱爲控制節點。下面給出了PostgreSQL中的控制節點列表。數組

T_ResultState,
    T_ModifyTableState,
    T_AppendState,
    T_RecursiveUnionState,
    T_BitmapAndState,
    T_BitmapOrState,

1.Result節點

Result節點主有兩種用途。緩存

咱們先討論比較簡單的一種狀況。針對那些不掃描表的查詢,例如:數據結構

postgres=# explain select 1+2;
                QUERY PLAN
------------------------------------------
 Result  (cost=0.00..0.01 rows=1 width=0)
(1 row)

postgres=# explain insert into zxc values (1,'ddff');
                   QUERY PLAN
-------------------------------------------------
 Insert on zxc  (cost=0.00..0.01 rows=1 width=0)
   ->  Result  (cost=0.00..0.01 rows=1 width=0)
(2 rows)

當SELECT査詢中沒有FROM子句時或者INSERT語句只有一個VALUES子句(這種狀況咱們在前面介紹ValueScan節點時提到過)時,執行査詢計劃不須要掃描表,執行器會直接計算SELECT的投影屬性或者使用VALUES子句構造元組。app

Result計劃節點的另外一種用途相對複雜點,是用來優化包含僅需計算一次的過濾條件,例如:函數

postgres=# explain select * from zxc where 1>2;
                        QUERY PLAN
-----------------------------------------------------------
 Result  (cost=0.00..22.70 rows=1 width=36)
   One-Time Filter: false
   ->  Seq Scan on zxc  (cost=0.00..22.70 rows=1 width=36)
(3 rows)

因爲PostgreSQL採用了一次一元組的方式執行査詢計劃樹,而WHERE子句中的條件表達式結果是常量,只需計算一次便可。所以,Result節點可針對此種狀況進行優化,避免重複計算這類表達式。這時,Result節點僅有一個子節點(左子節點)。post

爲了可以處理以上兩種狀況,Result節點被定義成以下所示的樣子,除了繼承Plan節點的基
本屬性外,還擴展定義了 resconstantqual字段。顧名思義,該字段保存只需計算一次的常量表達式。優化

typedef struct Result
{
    Plan        plan;
    Node       *resconstantqual;
} Result;

Result節點初始化過程(ExecInitResult函數)會初始化ResultSlate結構,該結構以下:指針

typedef struct ResultState
{
    PlanState   ps;             /* its first field is NodeTag */
    ExprState  *resconstantqual;
    bool        rs_done;        /* are we done? */
    bool        rs_checkqual;   /* do we need to check the qual? */
} ResultState;

若是有常量表達式,則放置在resconstantqual字段中,而re_checkqual則用於標記是否須要計算常量表達式,re_done表示Result節點是否已經處理完全部元組。Result初始化過程在計劃節點的標準初始化過程以外增長了幾項檢査:rest

  • 1)保證無右子節點。code

  • 2)檢査是否有常量表達式,有則將執行狀態節點的rs_checkqual設置爲真,不然設置爲假。

Result節點的初始化函數ExecInitResult除了一些基礎的初始化外,主要的就是初始化ResultState的rs_done、rs_checkqual和resconstantqual字段。後兩個字段都是根據Result節點的resconstantqual字段來進行初始化的。

Result節點的執行函數ExecResult中,首先判斷rs_checkqual是否爲真,爲真表示須要進行常量表達式計算,計算完成後將rs_checkqual設置爲假,下次執行不會再進行計算。若是表達式計算結果爲假,表示沒有知足條件的結果,則Result節點直接返回NULL。若表達式計算結果爲真,則會檢査是否有左子樹,有則從其中獲取元組;沒有左子樹表示爲VALUES子句,則直接設置rs_done爲真,由於只有一個元組須要輸出(這裏的VALUES子句中只容許有一個元組)。最後對結果元組執行投影操做並返回。

對於Result節點的清理過程(ExecEndResult函數),因爲不存在右子節點,因此只需調用左子節點的清理過程。


2.Append節點

Append節點用於處理包含一個或多個子計劃的鏈表(以下圖)。

...
                   /
                Append -------+------+------+--- nil
                /   \         |      |      |
              nil   nil      ...    ...    ...
                                 subplans

落實到查詢計劃裏,咱們看到的是這樣的:

postgres=# explain select * from zxc union all select * from user_define_table ;
                                 QUERY PLAN
----------------------------------------------------------------------------
 Append  (cost=0.00..45.40 rows=2540 width=36)
   ->  Seq Scan on zxc  (cost=0.00..22.70 rows=1270 width=36)
   ->  Seq Scan on user_define_table  (cost=0.00..22.70 rows=1270 width=36)
(3 rows)

Append處理過程會逐個處理這些子計劃(forwards or backwards),當一個子計劃返回了全部的結果後,會接着執行鏈表中的下一個子計劃,直到鏈表中的全部子計劃都被執行完。從這種執行方式可知,Append節點不使用左右孩子節點。其特色就在於依次執行子計劃鏈表(見下圖)。該節點可用於處理包含多個子査詢的UNION操做,以及支持繼承查詢(在含有繼承表的查詢中,查詢須要scan父表和子表)。

這裏student表和employee繼承自person表,student-emp繼承自student表和employee表,
那麼對於查詢: select name from person;

查詢計劃相似以下:


                  |
                Append -------+-------+--------+--------+
                /   \         |       |        |        |
              nil   nil      Scan    Scan     Scan     Scan
                              |       |        |        |
                            person employee student student-emp

Append節點的定義以下所示,很簡單。它在Plan節點的基礎上擴展定義了 appendplans字段,其中appendplans用於存儲子計劃鏈表。

typedef struct Append
{
    Plan        plan;
    List       *appendplans;
} Append;

Append節點的初始化過程(ExecInitAppend函數)會初始化AppendState節點,而後初始化子計劃鏈表中的每個子計劃,並將它們的狀態記錄節點組織成一個數組,使AppendState節點的appendplans 字段指向該數組。 as_nplans 記錄 Append 節點中子計劃鏈表的長度, as_whichplan 則用於在執行中指示當前處理的子計劃在鏈表中的偏移量.

typedef struct AppendState
{
    PlanState   ps;             /* its first field is NodeTag */
    PlanState **appendplans;    /* array of PlanStates for my inputs */
    int         as_nplans;      /* how many plans are in the array */
    int         as_whichplan;   /* which plan is being executed (0 .. n-1) */
} AppendState;

Append節點的執行過程也很簡單。該過程由ExecAppend函數完成。它會首先從as_whichplan標記的子計劃開始執行,若是返回的元組非空,則直接返冋結果。若是當前子計劃返回的是空元組,那麼會將as_whichPlan移動到下一個子計劃(由於執行過程能夠向前和向後掃描,向前掃描爲加1,反之減1),並繼續執行當前子計劃。假若已經沒有下一個子計劃能夠處理,則直接返回空元組。

因爲沒有左右孩子節點,Append節點清理工做(ExecEndAppend函數)不須要遞歸調用左右子節點的清理過程,而是掃描整個子計劃鏈表,依次調用子計劃的清理函數。


3.BitmapAnd/BitmapOr節點

BitmapAnd和BitmapOr都是位圖(Bitmap)類型節點,用於位圖計算。

先上例子:

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;  
  
                                     QUERY PLAN  
-------------------------------------------------------------------------------------  
 Bitmap Heap Scan on tenk1  (cost=25.08..60.21 rows=10 width=244)  
   Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))  
   ->  BitmapAnd  (cost=25.08..25.08 rows=10 width=0)  
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=101 width=0)  
               Index Cond: (unique1 < 100)  
         ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0)  
               Index Cond: (unique2 > 9000)

首先介紹一個位圖運算的例子。在使用索引掃描表時,可用位圖來標識知足掃描條件的元組(Bitmap Index Scan),即將知足條件的元組的對應位置1,這樣就可使用位圖來表示整個表中知足掃描條件的元組。當一個表上有多個屬性約束且都有索引時,能夠利用各個索引分別掃描獲得知足對應屬性約束的結果位圖,而後根據多個屬性約束上的邏輯關係對位圖進行與或運算獲得最終的結果位圖。

以下圖所示,假設表有屬性attrA和attrB,且分別創建了索引。若是須要進行的査詢中有條件「(涉及attrA的條件)AND (涉及attrB的條件)」,則能夠首先利用第一個條件掃描屬性attrA上的索引並構建位圖Bitmap A,其中每一位對應表中的一個元組,若是元組對應的位爲1表示該元組知足條件。用一樣的方法利用attrB上的索引構建位圖Bitmap B。因爲兩個屬性的約束間是「與」關係,所以能夠對兩個位圖進行AND操做,從而獲得Bitmap AB,其中值爲1的位表示對應的元組同時知足A、B兩屬性上約束。

BitmapAnd和BitmapOr節點實現了兩個或多個位圖的「與」和「或」運算。這兩個節點的數據組織相似於Append節點,將產生每個位圖的子計劃放在一個鏈表裏,在執行過程當中先執行子計劃節點獲取位圖,而後進行「與」(「或」)操做。下面給出了 BitmapAnd節點(BitmapOr節點的也相似)的數據結構,它們都有一個子計劃的鏈表(bitmapplans字段)。但和Append節點不一樣之處在於這兩個節點的子計劃返回的是位圖,而不是元組,所以,該節點不出如今ExecProcNode函數的判斷條件中(該函數中只涉及返回元組的計劃節點)。

typedef struct BitmapAnd
{
    Plan        plan;
    List       *bitmapplans;
} BitmapAnd;
typedef struct BitmapAndState
{
    PlanState   ps;             /* its first field is NodeTag */
    PlanState **bitmapplans;    /* array of PlanStates for my inputs */
    int         nplans;         /* number of input plans */
} BitmapAndState;

Bitmap類節點的初始化過程(ExecInitBitmapAnd和ExecInitBitmapOr函數)將初始化BitmapAndState/BitmapOrState節點,其過程與AppendState節點相似,狀態節點定義中也擴展了子計劃鏈表對應的狀態節點指針數組bitmapplans以及子計劃個數nplans。Bitmap類節點執行時也是依次獲取每一個子計劃的位圖,進行號/或操做,並將結果位圖返回。其清理過程也是依次調用每一個子計劃的清理過程,而不會處理未使用的左右子節點。

總之,該節點和Append很相似,我就很少說了。


4.RecursiveUnion節點

RecursiveUnion節點用於處理遞歸定義的UNION語句。下面給出一個例子,使用SQL語句定義從1到100求和,査詢語句以下:

postgres=# explain WITH RECURSIVE t(n)AS(
VALUES (1)
UNION ALL
SELECT n +1 FROM t WHERE n < 100)
select sum(n) FROM t;
                               QUERY PLAN
-------------------------------------------------------------------------
 Aggregate  (cost=3.65..3.66 rows=1 width=4)
   CTE t
     ->  Recursive Union  (cost=0.00..2.95 rows=31 width=4)
           ->  Result  (cost=0.00..0.01 rows=1 width=0)
           ->  WorkTable Scan on t t_1  (cost=0.00..0.23 rows=3 width=4)
                 Filter: (n < 100)
   ->  CTE Scan on t  (cost=0.00..0.62 rows=31 width=4)
(7 rows)

該査詢定義了一個臨時表並將VALUES子句給出的元組做爲t的初始值,「SELECT n + 1 FROM t WHERE n < 100」將t中屬性n小於100的元組的n值加1並返回,UNION ALL操做將SELECT 語句產生的新元組集合合併到臨時表 t 中。 以上的 SELECT 和 UNION 操做將遞歸地執行下去,直到SELECT語句沒有新元組輸出爲止。最後執行 「SELECT sum (n) FROM t」 對 t 中元組的n值作求和操做。

上述査詢由RecursiveUnion節點處理。有一個初始輸入集做爲遞歸過程的初始數據(如上例中「VALUES(1)」),而後進行遞歸部分(上例中的「SELECTn + 1 FROM t WHERE n < 100」)的處理獲得輸出,並將新獲得的輸出與初始輸入合併後做爲下次遞歸的輸入(例如第一次遞歸處理時新的輸出集爲{2},與初始輸入合併後爲{1, 2})。

RecursiveUnion節點的數據結構以下所示,它在Plan的基礎上擴展定義了wtParam字段用於與WorkTableScan(負責對臨時表的掃描,即遞歸部分的査詢語句的執行,我在前面文章提到過)傳遞參數。執行中RecursiveUnion與WorkTableScan之間的參數傳遞是經過Estate中es_param_exec_vals指向的參數數組完成的,wtParam表示傳遞的參數在該數組中的偏移量。

其餘四個擴展屬性(numCols、dupColIdx、dupOperators 和 numGroups)用於 UNION 時不包含關鍵字ALL的狀況:

  • munCols存儲了用於去重判斷的屬性個數;
  • dupColIdx數組記錄用十去重判斷的屬性號;
  • dupOperatore數組記錄了用於判斷各屬性是否相同的函數的OID;
  • numGroups記錄告終果元組數的估計值。
typedef struct RecursiveUnion
{
    Plan        plan;
    int         wtParam;        /* ID of Param representing work table */
    /* Remaining fields are zero/null in UNION ALL case */
    int         numCols;        /* number of columns to check for
                                 * duplicate-ness */
    AttrNumber *dupColIdx;      /* their indexes in the target list */
    Oid        *dupOperators;   /* equality operators to compare with */
    long        numGroups;      /* estimated number of groups in input */
} RecursiveUnion;

RecursiveUnion節點的初始化過程(ExecInitRecursiveUnion函數)除標準初始化過程外,首先會根據numCols是否爲0來辨別是否須要在合併時進行去重。若是須要去重,則根據dupOpemtors字段初始化RccursiveUnionState節點的eqfunctions和hashfunctions字段,同時申請兩個內存上下文用於去重操做,並建立去重操做用的Hash表。另外,還將爲RecureiveUnionState節點的working_table和intermediate_table字段初始化元組緩存結構。

typedef struct RecursiveUnionState
{
    PlanState   ps;             /* its first field is NodeTag */
    bool        recursing;      /* True when we're done scanning the non-recursive term */
    bool        intermediate_empty;     /* True if intermediate_table is currently empty */
    Tuplestorestate *working_table;     /* working table (to be scanned by recursive term) */
    Tuplestorestate *intermediate_table;    /* current recursive output (next generation of WT) */
    /* Remaining fields are unused in UNION ALL case */
    FmgrInfo   *eqfunctions;    /* per-grouping-field equality fns */
    FmgrInfo   *hashfunctions;  /* per-grouping-field hash fns */
    MemoryContext tempContext;  /* short-term context for comparisons */
    TupleHashTable hashtable;   /* hash table for tuples already seen */
    MemoryContext tableContext; /* memory context containing hash table */
} RecursiveUnionState;

RecursiveUnion節點的執行過程(ExecRecursiveUnion函數)以下:

首先執行左子節點計劃,將獲取的元組直接返回。若是須要去重,則箱要把返回的元組加入到hashtable中。同時,將初始集存儲在working_table指向的元組緩存結構中。

當處理完畢全部的左子節點計劃後,會執行右子節點計劃以獲取結果元組。其中,右子節點計劃中的WorkTableScan會以working_table爲掃描對象。右子節點計劃返回的結果元組將做爲RecursiveUnion節點的輸出,同時緩存在另外一個元組存儲結構intermediate_table中。每當woridng_table被掃描完畢,RecursiveUnion節點的執行流程會將intermediate_table 賦值給 working_table,而後再次執行右子節點計劃獲取元組,直到無元組輸出爲止。一樣,若是須要去重,右子節點計劃的全部輸出也會被存入同一個Hash表(hashlable),若重複則不會輸出。

在RecursiveUnion節點的清理過程當中,除了對子節點完成遞歸清理和狀態節點的清理外,還會針對初始化中建立的各類數據結構進行清理,例如清理working_table和intermediate_table兩個元組緩存結構,以及去重操做時用到的內存上下文。


還剩下一個ModifyTable節點。這個節點比較複雜,涉及到Update、Insert和Delete操做的處理。咱們下一篇再見吧~

相關文章
相關標籤/搜索