標籤:select 引用 node text pen apply gre first exec
5.ModifyTable節點
先看一個ModifyTable節點的例子:
postgres=# explain update test_01 set id = 5 where name = 'xxx'; QUERY PLAN--------------------------------------------------------------- Update on test_01 (cost=0.00..23.75 rows=6 width=48) -> Seq Scan on test_01 (cost=0.00..23.75 rows=6 width=48) Filter: ((name)::text = 'xxx'::text)(3 rows)
你可能疑惑為啥上面的查詢計劃裡面沒有"ModifyTable"這樣的字眼,下面是explain.c檔案中的一段:
case T_ModifyTable: sname = "ModifyTable"; switch (((ModifyTable *) plan)->operation) { case CMD_INSERT: pname = operation = "Insert"; break; case CMD_UPDATE: pname = operation = "Update"; break; case CMD_DELETE: pname = operation = "Delete"; break; default: pname = "???"; break; } break;
由此我們可以看到,對於ModifyTable節點,explain會判斷是增刪改中的哪一種從而作出判斷。所以當在explain中看到INSERT、Update和Delete時,我們就知道這是走了ModifyTable節點。
那這裡,我們還要再解釋ModifyTable節點嗎?解釋下吧:
* Apply rows produced by subplan(s) to result table(s), * by inserting, updating, or deleting.
也就是說,我先從下層的subplan中獲得rows,然後根據命令類型選擇是insert, update還是delete操作。所以我們可以知道,這是一個頂層節點,它下面是查詢節點(就是SELECT)。這也符合我們以前一直說的,所有的增刪改查其實都是SELECT!
typedef struct ModifyTable{ Plan plan; CmdType operation; /* INSERT, UPDATE, or DELETE */ bool canSetTag; /* do we set the command tag/es_processed? */ Index nominalRelation; /* Parent RT index for use of EXPLAIN */ List *resultRelations; /* integer list of RT indexes */ int resultRelIndex; /* index of first resultRel in plan's list */ List *plans; /* plan(s) producing source data */ List *withCheckOptionLists; /* per-target-table WCO lists */ List *returningLists; /* per-target-table RETURNING tlists */ List *fdwPrivLists; /* per-target-table FDW private data lists */ List *rowMarks; /* PlanRowMarks (non-locking only) */ int epqParam; /* ID of Param for EvalPlanQual re-eval */ OnConflictAction onConflictAction; /* ON CONFLICT action */ List *arbiterIndexes; /* List of ON CONFLICT arbiter index OIDs */ List *onConflictSet; /* SET for INSERT ON CONFLICT DO UPDATE */ Node *onConflictWhere; /* WHERE for ON CONFLICT UPDATE */ Index exclRelRTI; /* RTI of the EXCLUDED pseudo relation */ List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */} ModifyTable;
由於ModifyTable節點涉及的操作比較多,這裡稍微解釋下ModifyTable中的一些欄位。
withCheckOptionLists欄位
這個和視圖相關,我們知道建立視圖有這樣的用法:
CREATE VIEW xxx_view AS query WITH CHECK OPTION
在postgres中,對建立語句中帶有WITH CHECK OPTION的VIEW,在通過視圖進行的操作(增刪改),必須也能通過該視圖看到操作後的結果。
也就是說:
對於INSERT,那麼加的這條記錄在視圖查詢後必須可以看到。
對於UPDATE,修改完的結果也必須能通過該視圖看到。
對於DELETE,只能刪除視圖裡有顯示的記錄。
因此對這一類操作,我們在動作表/視圖的時候,要在(INSERT/UPDATE/DELETE的)WHERE條件中加上WITH OPTION中的條件。
returningLists欄位
這個很簡單,因為postgres的文法中有類似以下的用法:
DELETE FROM xxx_table WHERE condition RETURNING xxx;UPDATE xxx_table SET a = '123' WHERE condition RETURNING xxx;INSERT INTO xxx_table VALUES (somevalues) RETURNING xxx;
是的,postgres的RETURNING子句可以返回修改的行,所以對於含有RETURNING子句的查詢,除了在對錶中的資料進行INSERT/UPDATE/DELETE,還要額外返回一些行。即還要有額外的輸出。
fdwPrivLists欄位
Postgres支援訪問外部資料庫的嘛,所以這個欄位提供對fdw的處理的支援。
rowMarks欄位
這個和行鎖相關,針對SELECT的LOCK子句:
FOR lock_strength [ OF table_name [, ...] ] [ NOWAIT | SKIP LOCKED ]
具體見這裡:http://www.postgres.cn/docs/9.5/sql-select.html
onConflictAction、arbiterIndexes、arbiterIndexes和onConflictWhere欄位
是的,對於INSERT操作,我們有以下文法(用於支援INSERT中發生的衝突):
INSERT INTO table_name VALUES (somevalues) ON CONFLICT [ conflict_target ] conflict_action並且 conflict_action 是以下之一: DO NOTHING DO UPDATE SET { column_name = { expression | DEFAULT } | ( column_name [, ...] ) = ( { expression | DEFAULT } [, ...] ) | ( column_name [, ...] ) = ( sub-SELECT ) } [, ...] [ WHERE condition ]
這樣一看,很容易對的上了。
exclRelRTI、exclRelTlist欄位
對於建表語句有以下子句:
EXCLUDE [ USING index_method ] ( exclude_element WITH operator [, ... ] ) index_parameters [ WHERE ( predicate ) ]
EXCLUDE子句指定一個排除約束,它保證如果任意兩行在指定列或運算式上使用指定操作符進行比較,不是所有的比較都將會返回TRUE。具體見這裡:
因此,你可以把這個欄位看做是一個限制欄位,在做更新操作時需要判斷。
typedef struct ModifyTableState{ PlanState ps; /* its first field is NodeTag */ CmdType operation; /* INSERT, UPDATE, or DELETE */ bool canSetTag; /* do we set the command tag/es_processed? */ bool mt_done; /* are we done? */ PlanState **mt_plans; /* subplans (one per target rel) */ int mt_nplans; /* number of plans in the array */ int mt_whichplan; /* which one is being executed (0..n-1) */ ResultRelInfo *resultRelInfo; /* per-subplan target relations */ List **mt_arowmarks; /* per-subplan ExecAuxRowMark lists */ EPQState mt_epqstate; /* for evaluating EvalPlanQual rechecks */ bool fireBSTriggers; /* do we need to fire stmt triggers? */ OnConflictAction mt_onconflict; /* ON CONFLICT type */ List *mt_arbiterindexes; /* unique index OIDs to arbitrate * taking alt path */ TupleTableSlot *mt_existing; /* slot to store existing target tuple in */ List *mt_excludedtlist; /* the excluded pseudo relation's * tlist */ TupleTableSlot *mt_conflproj; /* CONFLICT ... SET ... projection * target */} ModifyTableState;
那麼對於ModifyTableState的一些欄位,我們參照ModifyTable節點的解釋,也能理解的差不多,這裡不多說了。
下面進入正題:
ModifyTable節點的初始化由ExecInitModifyTable函數來做。該函數除了做一些基礎的初始化操作外,針對我上面提到的那些欄位,做了設定和初始化。說細一點的話就是:
(1)調用ExecInitNode函數對ModifyTable節點中的plans列表中的subplans節點進行初始化並將其結果儲存到ModifyTableState結構的mt_plans欄位中。在這一步中,同時也順便做了這些事:驗證了查詢所涉及的target relations是否是合法的;開啟這些target relations上的index,因為對於UPDATE/INSERT操作,我們同時還要對相應的索引進行操作(DELETE操作不刪除索引,DELETE後遺留的index留給VACUUM去清理)。
(2)根據ModifyTable節點中的withCheckOptionLists欄位初始化上面提到的WITH CHECK OPTION(如果存在的話)。初始化後儲存在ModifyTableState結構的resultRelInfo欄位的成員變數ri_WithCheckOptions和ri_WithCheckOptionExprs中。
(3)根據ModifyTable節點中的returningLists欄位初始化上面提到的RETURNING子句(如果存在的話)並根據此構造返回的結果集的類型。如果returningLists欄位為空白,說明沒有RETURNING子句。那麼返回結果集的類型設定為NULL。
(4)如果存在ON CONFLICT DO UPDATE欄位那麼為他初始化目標列表Target List、投影條件resultRelInfo和過濾條件qual,結果儲存在ModifyTableState結構的相應欄位中。
(5)處理ModifyTable節點中的RowMark欄位,結果儲存在ModifyTableState結構的mt_arowmarks欄位中。
(6)初始化junk filter。這個junk filter的由來是因為在plan階段,我們會產生一些"中間資訊"放在tuple中供Excutor使用。比如ctid,用來定位該元組放到磁碟檔案的位置。但是當我們將元組寫到磁碟時,我們不需要儲存這個資訊。那麼這個資訊相當於是冗餘的了,我們需要用這個JunkFilter將其過濾和刪除。
(7)我們知道我們在INSERT/UPDATE/DELETE時可能會涉及到trigger,這裡設定trigger相關的slot,供後續函數調用。
(8)如果本ModifyTable節點不是頂層ModifyTable節點的話(上層還有ModifyTable節點),設定全域狀態結構estate->es_auxmodifytables屬性,做上標記。
ModifyTable節點的執行由ExecModifyTable函數執行。具體的來說:
(1)首先我們要記得可能有BEFORE STATEMENT triggers這玩意兒,顧名思義,就是要在STATEMENT執行之前執行這個trigger。如果存在,在進入正式的處理之前我們先要調用fireBSTriggers函數來處理它。
(2)接下來是一個大for迴圈。在這個for迴圈裡面,程式調用ExecProcNode函數迴圈地從下層節點中讀取元組。需要注意的是這個迴圈裡面類似Append節點的操作,在讀取完第一個subplans節點中的元組後,會依次讀取後續subplan中的元組,直到全部讀取完畢。我們以前說過postgres是一次讀取一個元組並處理一個元組的。這裡也不例外,每讀取一個元組後根據操作的類型分別調用ExecInsert/ExecUpdate/ExecDelete函數去處理。
(3)有始有終,既然可能有BEFORE STATEMENT triggers,那麼也可能有AFTER STATEMENT triggers,這裡調用fireASTriggers函數來處理它。
那麼我們應該對ExecInsert/ExecUpdate/ExecDelete函數感興趣了。下面我們開始討論他們。
1.ExecInsert
對於ExecInsert函數的話,主要是兩件事:將元組插入到目標表target relation中同時將對應的索引項目插入到相關的索引表index relations(可能有多個索引要處理)中。
(1)首先要將需要插入的tuple從slot中取出,本地化。why?因為這個slot在隨後的操作heap_insert函數中可能不安全,因此將其提前取出來。這個工作由ExecMaterializeSlot函數完成。
(2)從全域狀態中estate->es_result_relation_info擷取資訊,判斷result relation是否需要一個oid。如果需要,則先將tuple中的oid欄位設為0。
(3)處理BEFORE ROW INSERT Triggers。這裡我們要注意這個觸發器是ROW層級的,而BEFORE STATEMENT triggers是語句層級的,他們不一樣。
(4)處理INSTEAD OF ROW INSERT Triggers。如果存在則調用ExecIRInsertTriggers函數去處理並直接返回,不進行INSERT操作。
(5)處理foreign table的情況,為其初始化ri_FdwRoutine。調用foreign server的API去處理該條元組的插入並擷取返回的slot
(6)處理WITH CHECK OPTION中的條件(ExecWithCheckOptions函數)和唯一性限制式(ExecConstraints函數)ON ONCONFLICT OPTION。
(7)如果存在ON ONCONFLICT OPTION條件,則先獲得speculative insertion lock,調用heap_insert函數將元組插入到堆表中。如果插入成功,不發生衝突則正常釋放該lock。否則強制釋放lock,並執行ON ONCONFLICT DO UPDATE(如果有的話)。
(8)不存在(7)中的條件,我們正常地調用heap_insert函數將元組插入到堆表中。同時調用ExecInsertIndexTuples函數插入相應的索引元組。
(9)調用ExecARInsertTriggers函數處理AFTER ROW INSERT Triggers。類似(3)的處理。
(10)還記得上面提到的CREATE VIEW中的WITH CHECK OPTION嗎?這裡調用ExecWithCheckOptions函數做處理,不滿足則報錯退出。
(11)如果存在RETURNING子句,我們調用ExecProcessReturning函數處理之。
2.ExecDelete
ExecDelete函數相對簡單,他只需要將元組刪除即可,不需要針對索引做任何操作。
(1)從全域狀態中estate->es_result_relation_info擷取資訊。
(2)處理BEFORE ROW DELETE Triggers。這裡我們要注意這個觸發器是ROW層級的,而BEFORE STATEMENT triggers是語句層級的,他們不一樣。
(3)處理INSTEAD OF ROW DELETE Triggers。如果存在則調用ExecIRDeleteTriggers函數去處理並直接返回,不進行INSERT操作。
(4)處理foreign table的情況,為其初始化ri_FdwRoutine。調用foreign server的API去處理該條元組的刪除並擷取返回的slot。
(5)我們正常地調用heap_delete函數執行DELETE操作。如果傳回值不是HeapTupleMayBeUpdated則說明操作失敗,根據失敗的錯誤碼執行相應的處理。
(6)調用ExecARDeleteTriggers函數處理AFTER ROW DELETE Triggers。類似(2)的處理。
(7)如果存在RETURNING子句,我們調用ExecProcessReturning函數處理之。
3.ExecUpdate
ExecUpdate函數實際上執行的是"INSERT"操作。因為postgres內部是MVCC機制,多版本並發控制。舊的元組實際上沒有刪除,只是不再引用。同時,UPDATE操作在資料庫內部也是要在"transaction"中的,否則postgres會不停的將新增的updated元組看成是需要update的元組,迴圈下去。
(1)判斷當前是否屬於BootstrapProcessing模式,在該模式下所有的transaction id都被設定為1。這個時候才能保證不迴圈更新。
(2)首先要將需要插入的tuple從slot中取出,本地化。why?因為這個slot在隨後的操作heap_update函數中可能不安全,因此將其提前取出來。這個工作由ExecMaterializeSlot函數完成。
(3)處理BEFORE ROW UPDATE Triggers。
(4)處理INSTEAD OF ROW UPDATE Triggers。如果存在則調用ExecIRUpdateTriggers函數去處理並直接返回,不進行INSERT操作。
(5)處理foreign table的情況,為其初始化ri_FdwRoutine。調用foreign server的API去處理該條元組的更新並擷取返回的slot。
(6)處理WITH CHECK OPTION中的條件(ExecWithCheckOptions函數)和唯一性限制式(ExecConstraints函數)ON ONCONFLICT OPTION。
(7)我們正常地調用heap_update函數執行UPDATE、操作。如果傳回值不是HeapTupleMayBeUpdated則說明操作失敗,根據失敗的錯誤碼執行相應的處理。如果成功,則調用ExecInsertIndexTuples函數向索引中插入索引元組。
(8)調用ExecARUpdateTriggers函數處理AFTER ROW UPDATE Triggers。
(9)針對錶的上層VIEW再次執行WITH CHECK OPTION。
(10)如果存在RETURNING子句,我們調用ExecProcessReturning函數處理之。
ModifyTable節點的清理簡單些了(ExecEndModifyTable函數)。除了常規的清理工作,清理可能存在FDW結構,清理初始化中額外初始化的那些subplans節點。
control節點到此結束。
跟我一起讀postgresql源碼(十六)——Executor(查詢執行模組之——control節點(下))