跟我一起讀postgresql源碼(四)——Planer(查詢規划模塊)(上)


時間一晃周末就過完了,時間過得太快,不由得讓人倍加珍惜。時間真是不夠用哈~

好的不廢話,這次我們開始看查詢規划模塊的源碼吧。

查詢規划部分的在整個查詢處理模塊應該是在一個非常重要的地位上,這一步直接決定了查詢的方式與路徑,很大程度上影響了數據庫查詢的查詢性能。因此這一塊代碼量也很大,我也會花較多的筆墨來分析這個模塊的代碼。在篇幅上,可能查詢規划這一模塊我會用2到3篇文章來細細的說明下。今天這一篇先總體概述下查詢規划模塊的全貌,在介紹該模塊的一個重要的子模塊(總共三個主要模塊)就結束吧,剩下的交給第二篇吧。


1.查詢規划模塊總體概況

廢話不多說,我先上圖。下圖大概的刻畫了查詢規划模塊里主要的函數調用關系,當然啦,最下層只畫到主處理函數,主處理函數內部的調用關系在介紹每個主處理函數的時候再細細的說吧。

我簡單介紹下吧。exec_simple_query函數是負責查詢處理的主函數,在進行查詢規划之前,它已經調用了查詢分析和查詢重寫模塊。還記得上一篇博文的查詢重寫模塊么?該模塊返回的是重寫后的查詢樹鏈表。這里exec_simple_query函數將重寫后的查詢樹鏈表交給查詢規划模塊進一步處理。查詢規划模塊的入口函數是pg_plan_queries函數。

pg_plan_queries函數調用pg_plan_query函數對每一個查詢進行處理並返回PlannedStmt(執行計划)結構體鏈表。這里要注意,查詢規划模塊只會對非UTILITY命令進行處理。

而在pg_plan_query函數里,實際是調用planner函數負責查詢計划的生成。

planner函數調用standard_planner函數進入標准的查詢規划處理流程。該函數接受查詢樹以及相關參數,返回PlannedStmt結構體,詳細的結構如下:

typedef struct PlannedStmt
{
    NodeTag    	type;
	CmdType		commandType;	/* select|insert|update|delete */
	uint32		queryId;		/* query identifier (copied from Query) */
	bool		hasReturning;	/* is it insert|update|delete RETURNING? */
	bool		hasModifyingCTE;	/* has insert|update|delete in WITH? */
	bool		canSetTag;		/* do I set the command result tag? */
	bool		transientPlan;	/* redo plan when TransactionXmin changes? */
	struct Plan *planTree;		/* tree of Plan nodes */
	List	   *rtable;			/* list of RangeTblEntry nodes */
	/* rtable indexes of target relations for INSERT/UPDATE/DELETE */
	List	   *resultRelations;	/* integer list of RT indexes, or NIL */
	Node	   *utilityStmt;	/* non-null if this is DECLARE CURSOR */
	List	   *subplans;		/* Plan trees for SubPlan expressions */
	Bitmapset  *rewindPlanIDs;	/* indices of subplans that require REWIND */
	List	   *rowMarks;		/* a list of PlanRowMark's */
	List	   *relationOids;	/* OIDs of relations the plan depends on */
	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
	int			nParamExec;		/* number of PARAM_EXEC Params used */
	bool		hasRowSecurity; /* row security applied? */
} PlannedStmt;

該結構體里包含了后續的查詢執行模塊所需要的全部信息,包括計划樹(Plan)、子計划樹(SubPlan)和其他重要的參數信息。

在standard_planner函數里,主要通過subquery_planner函數和set_plan_references函數分別完成查詢計划的處理優化和清理工作。

set_plan_references函數這里不多做介紹,這不是查詢計划的重點,我們把重點放在subquery_planner函數上。該函數接收Query(查詢樹),返回一個Plan(計划樹)。

如圖所示,我們可以看出subquery_planner函數的處理主要分為三部分。

1.1 預處理

主要調用相關預處理函數,依據消除冗余條件、減少遞歸層次和簡化路徑等原則對查詢樹進行預處理。主要包括:

  • 1.特殊情況的處理,如內聯返回值函數、預處理行標記和擴展繼承表等;
  • 2.提升子鏈接和提升子查詢;
  • 3.預處理表達式;
  • 4.預處理連接條件.

1.2查詢規划處理

此處主要調用grouping_planner函數來處理(inheritance_planner函數在處理完繼承關系后仍然調用的是grouping_planner函數來處理)。在執行過程中,該函數不再對查詢樹做變換處理,而是將查詢中的信息進行規范化並傳給query_planner函數。query_planner函數調用make_one_rel函數進入查詢優化階段,並將最優路徑放入cheapest_path。然后grouping_planner函數再調用get_cheapest_fractional_path_for_pathkeys函數尋找符合需求的排序路徑sorted_path,並最終確定最優路徑best_path並生成基本計划樹(調用create_plan函數)。

1.3 清理計划樹

調用set_plan_references函數清理現場,做一些變量的調整工作,不對計划樹做本質的改變。

那么,接下來的部分和后續文章(如果有的話)就從這三個部分展開了。


2.預處理

在進行正式的工作之前,常常需要做一些准備工作。在查詢規划模塊尤其是這樣,因為它的預處理階段做的事情還是蠻多的,因此值得單獨拿出一節來講,另外從篇幅上考慮,講完預處理這篇就差不多了,因為之后的查詢規划主過程的內容只會更多,應該要單獨拿出(至少)一篇講了。

查詢規划模塊的預處理工作主要是提升子鏈接和子查詢,預處理表達式和having子句等。處理的對象主要是查詢樹中的范圍表rtable和連接樹jointree.

2.1提升子鏈接和子查詢

首先我們先說明下子鏈接和子查詢的區別:子查詢是一條完整的查詢語句,而子鏈接是一條表達式,但是表達式內部也可以包含查詢語句。直白點說呢就是:子查詢是放在FROM子句里的而子鏈接則出現在WHERE子句或者HAVING子句中。

在subquery_planner函數里,調用pull_up_sublinks函數處理WHERE子句和JOIN/ON子句中的ANY和EXISTS類型的子鏈接。

在用pull_up_sublinks函數內部,調用pull_up_sublinks_jointree_recurse函數遞歸地處理連接樹jointree:

1.對於RangeTblRef類型,直接返回;

2.對於FromExpr類型,遞歸調用pull_up_sublinks_jointree_recurse函數處理每個節點並調用pull_up_sublinks_qual_recurse函數處理約束條件;

3.對於JoinExpr類型,遞歸調用pull_up_sublinks_jointree_recurse函數處理左右子樹並調用pull_up_sublinks_qual_recurse函數處理約束條件.

之后,subquery_planner函數調用pull_up_subqueries函數來提升子查詢。當子查詢僅僅是一個簡單的掃描或者連接時,就會把子查詢或者子查詢的一部分合並到父查詢中以進行優化。一般會分一下三種情況處理:

1.在范圍表中存在子查詢。對於簡單的子查詢,直接調用pull_up_simple_subquery函數進行提升;而對於簡單的UNION ALL子查詢,調用pull_up_simple_union_all函數進行提升,其他的情況則不處理;

2.在FROM表達式中存在子查詢。對於FROM列表中的每個節點都調用pull_up_subqueries遞歸處理;

3.連接表達式中的子查詢。調用pull_up_subqueries函數遞歸地處理.

這些話說的很頭疼,大家看了估計也頭疼。我們舉一個例子好了,假設我們有這樣一個SQL語句:

SELECT D.dname
FROM dept D
WHERE D.deptno IN
    (SELECT E.deptno FROM emp E WHERE E.sal = 100);

從字面上看,如果該語句中的子查詢被獨立地規划,也就是說對於表dept中的每一個元組deptno值,都要搜索一遍emp表。顯然這樣的做法很蠢,代價也非常大。但是如果我們把子查詢提升並合並到父查詢中,那么我們看看效果。
先做提升子鏈接:

SELECT D.dname
FROM dept D ,   (SELECT E.deptno FROM emp E WHERE E.sal = 100) AS Sub
WHERE D.deptno = Sub.deptno;

然后再做提升子查詢:

SELECT D.dname
FROM dept D ,emp E
WHERE D.deptno = E.deptno and E.sal = 100;

可以看到,這樣操作以后的SQL語句只要先做一下過濾(E.sal = 100),然后再把結果和dept表做一下連接即可,大大提高了查詢效率。

2.2預處理表達式

表達式的預處理工作主要由函數preprocess_expression完成。該函數采用遞歸掃描的方式處理PlannerInfo結構體里面保存的目標屬性、HAVING子句、OFFSET子句、LIMIT子句和連接樹jointree。總體來說,做了以下幾件事:

1.調用flatten_join_alias_vars函數,用基本關系變量取代連接別名變量;

2.調用函數eval_const_expression進行常量表達式的簡化,也就是直接計算出常量表達式的值。例如:"3+1 <> 4" 這種會直接被替換成“FALSE”;

3.調用canonicalize_qual函數對表達式進行規范化,主要是將表達式轉換為最佳析取范式或者合取范式(這個博主也不太懂啊,尷尬);

4.調用函數make_subplan將子鏈接轉換為子計划.

對於函數make_plan,還是要細細的說一下,其執行步驟如下:

1)首先復制子鏈接SubLink中的查詢樹Query,如果查詢樹是一個EXISTS型的子計划,那么則調用simplify_EXISTS_query函數對QUery副本進行處理;

2)調用subquery_planner函數為子鏈接生成計划,同時設置好參數tuple_fraction來告訴底層規划器需要去多少個元組。對於EXISTS型的子查詢,設置該值為1.0,取回一個元組即可;對於ALL和ANY型的子查詢,從概率上說大約取到50%的元組就可以得到結果,因此設置參數值為0.5;對於其他的查詢,統一設置為0;

3)調用build_subplan函數將上一步生成的計划轉換為SubPlan或者InitPlan的形式,並將當前查詢層次的參數列表傳遞給它的子計划;

4)對於生成的是SubPlan而且是一個簡單的EXISTS類型的查詢,調用convert_EXISTS_to_ANY函數嘗試將其轉換為一個ANY類型的查詢並創建查詢計划.

2.3處理HAVING子句

對於HAVING子句來說,除了進行前面所提到的預處理外,還需要處理其中的每個條件。如果HAVING子句中沒有聚集函數的話,那么它完全可以退化到WHERE子句中去,否則的話他將被寫到查詢樹的HavingQual字段里面。
具體來說的話:

step1:初始化一個空的HAVING子句鏈表newHaving;

step2:掃描HAVING子句鏈表中的每一條HAVING子句:
        1.如果HAVING子句中存在聚集函數、易失函數或者子計划中的一種或多種,則添加至newHaving鏈表中;
        2.如果不包含GROUP子句,則添加至WHERE子句中;
        3.其他情況,將這條HAVING子句添加至WHERE子句同時也保存至newHaving中.

step3:用處理后的HAVING、子句鏈表newHaving替換原來的HAVING子句鏈表.

舉個例子,例如有如下SQL語句:

SELECT a FROM b WHERE a.c = 10 HAVING  a.d > 5;

在HAVING子句里沒有聚集,那么可以直接將"a.d > 5" 提升到WHERE子句中,即:

SELECT a FROM b WHERE a.c = 10 AND a.d > 5;

3.結語

所以這一篇就又這樣水完了?不甘心啊。感覺寫的蠻糟的。這次大概就把查詢規划模塊的總體和預處理階段簡單介紹了下,感覺思路有點亂,有空再捋捋。后面一篇再細細的說說查詢規划的主處理過程吧~感覺是塊硬骨頭,不好啃。

好的,大家下期見~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM