跟我一起讀postgresql源碼(二)——Parser(查詢分析模塊)


上篇博客簡要的介紹了下psql命令行客戶端的前台代碼。這一次,我們來看看后台的代碼吧。

十分不好意思的是,上篇博客我們只說明了前台登陸的代碼,沒有介紹前台登陸過程中,后台是如何工作的。即:后台接到前台的連接請求后發生了什么?調用了哪些函數?啟動了哪些進程?

那么,我們就先講講后台的工作流程吧。


1.postgresql后台工作流程

這里首先我們要知道postgresql是典型的“Server/Client”的模式。即服務器后台有一個主進程(postmaster),該進程根據客戶端的連接請求,fork一個服務端進程(postgres)為之服務。

具體來說,postmaster監聽着一個特定的 TCP/IP 端口等待進來的連接。每當檢測到一個連接請求時,postmaster進程派生出一個新的叫postgres的服務器進程。服務器任務(postgres進程)相互之間使用信號量和共享內存進行通訊, 以確保在並行的數據訪問過程中的數據完整性。

前台程序發出一個啟動命令后到Postmaster后,Postmaster根據其提供的信息建立一個子進程,也就是后台進程,專門為前台服務。Postmaster負責維護后台進程的生命周期,但與后台進程相獨立。這樣在后台進程崩潰后可以重啟動后台進程而不會和這些后台進程一起崩潰。

落實到代碼里呢?

我們首先看看\src\backend\main下的main.c文件。我們說過每個程序都有個“main”函數,之前也說明了psql里的main函數。后台的main函數就定義在main.c文件里。

在這個main函數里主要做了什么?我寫在下面:

line99: 函數MemoryContextInit()啟動必須的子系統error和memory管理系統;

line110:函數set_pglocale_pgservice()獲取並設置環境變量;

line146~148: 函數init_locale初始化環境變量;

line219~228:根據輸入參數確定程序走向,這里進入了PostmasterMain(){跳轉至postmaster.c文件中}

這里我們可以看到,main函數只是做了一些初始化的工作,它隨后就把傳進來的參數原封不動的傳給了PostmasterMain(argc, argv)函數。那么繼續進入PostmasterMain函數,他在src/backend/postmaster/postmaster.c中。

在函數postmaster中又做了哪些事?

根據命令行參數設定相應的環境值,初始化監聽端口,檢查其維護的數據庫文件是否存在,設置signal handlers從操作系統上監聽其感興趣的消息;
調用StartupDataBase()啟動后台子進程;
調用ServerLoop()監聽新的建立連接消息。

ServerLoop()是一個死循環,當有一個新的建立連接消息到來的時候,查找自身維護的端口列表,看是否有空閉的端口,如果有調用static int BackendStartup(Port *port)來fork一個后台進程。然后,ServerLoop()判斷下列幾個后台支持進程的狀態(linux上你可以用ps命令查看,Windows的話就任務管理器咯):

system logger process
autovacuum process
background writer process
the archiver process
the stats collector process

當發現一個或多個進程發現崩潰后,重新啟動它們,確保數據庫整體的正常運行。Postmaster在循環中就這樣一直不停的監聽聯系請求和維護后台支持進程。

以上的這些步驟都是在啟動數據庫服務器的時候完成的(postmaster命令、postgres命令或者pg_ctl命令),即在你運行psql之前。當服務器做好上面的准備后,才可以接受前台的連接請求。

在監聽並接受了一個前台的連接請求后,postmaster調用BackendStartup(Port *port)來fork一個后台進程。在該函數里:

  • 調用函數BackendInitialize(port)完成Backend的初始化(主要包括讀入postgre中的配置文件,根據配置文件進行端口的綁定和對客戶進行驗證);
  • 調用函數調用BackendRun(),它會為backend設置好啟動參數,並傳遞給PostgresMain(ac, av, port->database_name, port->user_name)函數(其中ac為int型,代表參數個數;char **av是一個二維字符串數組,用於存儲參數;后面依次為要連接的數據庫名和連接詞數據庫的用戶名).

最后在src/backend/tcop/postgres.c的PostgresMain()函數里,設置好環境變量和內存上下文,在3933行的for循環處循環檢查前台的輸入並利用函數ReadCommand(StringInfo inBuf)讀取前台命令。

根據讀取的命令字符串的首字符的不同,可分為以下幾種命令:

至此,后台服務進程正式開始工作。


2.postgresql的Parser(查詢分析模塊)

當postgresql的后台服務進程postgres收到前台發來的查詢語句后,首先將其傳遞到查詢分析模塊,進行詞法分析,語法分析和語義分析。若是功能性命令(例如create table,create user和backup命令等)則將其分配到功能性命令處理模塊;對於查詢處理命令(SELECT/INSERT/DELETE/UPDATE)則為其構建查詢語法樹,交給查詢重寫模塊。

總的來說流程如下:

SQL命令 --(詞法和語法分析)--> 分析樹 --(語義分析)--> 查詢樹

在代碼里的調用路徑如下(方框內為函數,數字顯示了調用順序):

因此,查詢分析的處理過程如下:

  • exec_simple_query函數(在src/backend/tcop/postgres.c下)調用函數pg_parse_query進入詞法分析和語法分析的主過程,函數pg_parse_query再調用詞法分析和語法分析的入口函數raw_parser生成分析樹;
  • 函數pg_parse_query返回分析樹(raw_parsetree_list)給exec_simple_query;
  • exec_simple_query函數調用函數pg_analyze_and_rewrite進行語義分析(調用parse_analyze函數,返回查詢樹)和查詢重寫(調用pg_rewrite_query函數);
  • 返回查詢樹鏈表給exec_simple_query。

2.1 詞法分析和語法分析

postgre命令的詞法分析和語法分析是由Unix工具Yacc和Lex制作的。它們依賴的文件定義在src\backend\parser下的scan.l和gram.y。其中:

  • 詞法器在文件 scan.l里定義。負責識別標識符,SQL 關鍵字等,對於發現的每個關鍵字或者標識符都會生成一個記號並且傳遞給分析器;
  • 分析器在文件 gram.y里定義。包含一套語法規則和觸發規則時執行的動作.

在raw_parser函數(在src/backend/parser/parser.c下)中,主要通過調用Lex和Yacc配合生成的base_yyparse函數來實現詞法分析和語法分析的工作。
其它重要的文件如下:

kwlookup.c:提供ScanKeywordLookup函數,該函數判斷輸入的字符串是否是關鍵字,若是則返回單詞表中對應單詞的指針;

scanup.c:提供幾個詞法分析時常用的函數。scanstr函數處理轉義字符,downcase_truncate_identifier函數將大寫英文字符轉換為小寫字符,truncate_identifier函數截斷超過最大標識符長度的標識符,scanner_isspace函數判斷輸入字符是否為空白字符。

scan.l:定義詞法結構,編譯生成scan.c;

gram.y:定義語法結構,編譯生成gram.c;

gram.h:定義關鍵字的數值編號。

值得一提的是,如果你想修改postgresql的語法,你要關注下兩個文件“gram.y”和“kwlist.h”。簡要地說就是將新添加的關鍵字添加到kwlist.h中,同時修改gram.y文件中的語法規則,然后重新編譯即可。具體可以看下這篇博客如何修改Postgres的語法規則文件---gram.y

至於文件間的調用關系?我還是上個圖吧:

至於詞法分析和語法分析實現的細節,這應該是編譯原理課程上學習的東西,這里先就不提了,以后有時間好好學習下Lex和Yacc的語法好了,到時候再寫點東西與大家共享。

2.2 語義分析

語義分析階段會檢查命令中是否有不符合語義規則的成分。主要作用是為了檢查命令是否可以正確的執行。

exec_simple_query函數在從詞法和語法分析模塊獲取了parsetree_list之后,會對其中的每一顆子樹調用pg_analyze_and_rewrite進行語義分析和查詢重寫。其中負責語義分析的模塊是在src/backend/parser/analyze.c中的parse_analyze函數。該函數會根據得到的分析樹生成一個對應的查詢樹。然后查詢重寫模塊會對這顆查詢樹進行修正,這就是查詢重寫的任務了,而這並不是這篇博客的重點,放在下一篇博客里再說好了。

在parse_analyze函數里,會首先生成一個ParseState類型的變量記錄語義分析的狀態,然后調用transformTopLevelStmt函數處理語義分析。transformTopLevelStmt是處理語義分析的主過程,它本身只執行把'SELECT ... INTO'語句轉換成'CREATE TABLE AS'的任務,剩下的語義分析和生成查詢樹的任務交給transformStmt函數去處理。

在transformStmt函數里,會先調用nodeTag函數獲取傳進來的語法樹(praseTree)的NodeTag。有關NodeTag的定義在src/include/nodes/nodes.h中。postgresql使用NodeTag封裝了大多數的數據結構,把它們封裝成節點這一統一的形式,每種節點類型作為一個枚舉類型。那么只要讀取節點的NodeTag就可以知道節點的類型信息。

因此,隨后在transformStmt函數中的switch語句里根據NodeTag區分不同的命令類型,從而進行不同的處理。在這里共有8種不同的命令類型:

SELECT INSERT DELETE UPDATE     //增 刪 改 查
DeclareCursor   //定義游標
Explain         //顯示查詢的執行計划
CreateTableAs   //建表、視圖等命令
UTILITY         //其它命令

對應這8種命令的NodeTag值和語義分析函數如下:

NodeTag值                       語義分析函數
T_InsertStmt                transformInsertStmt
T_DeleteStmt                transformDeleteStmt
T_UpdateStmt                transformUpdateStmt
T_SelectStmt                ( transformValuesClause 
                            或者 transformSelectStmt 
                            或者 transformSetOperationStmt )
T_DeclareCursorStmt         transformDeclareCursorStmt
T_ExplainStmt               transformExplainStmt
T_CreateTableAsStmt         transformCreateTableAsStmt
default                     作為Unility類型處理,直接在分析樹上封裝一個Query節點返回

程序就根據這8種不同的命令類型,指派不同的語義分析函數去執行語義分析,生成一個查詢樹。

那在這里就以SELECT語句的transformSelectStmt函數為例看看語義分析函數的流程吧:

  • 1)創建一個新的Query節點並設置其commandType字段值為CMD_SELECT;
  • 2)調用transformWithClause函數處理WITH子句;
  • 3)調用transformFromClause函數處理FROM子句;
  • 4)調用transformTargetList函數處理目標屬性;
  • 5)調用transformWhereClause函數處理WHERE子句;
  • 6)調用transformSortClause函數處理ORDER BY子句;
  • 7)調用transformGroupClause函數處理GROUP BY子句;
  • 8)調用transformDistinctClause或者transformDistinctOnClause函數處理DISTINCT子句;
  • 9)調用transformLimitClause函數處理LIMIT和OFFSET;
  • 10)調用transformWindowDefinitions函數處理窗口函數;
  • 11)調用transformLockingClause函數處理FOR [KEY] UPDATE/SHARE子句;
  • 12)設置Query節點的其他標志;
  • 13)返回Query節點.

這樣以后我們就得到了一個查詢命令的查詢樹Query。

其實寫到這里本來還想繼續分析transformWithClause這些解析各種子句的函數,后來想想這樣篇幅也太多了,而且未免也太細了,也留給各位朋友們一起討論吧。

最后,我們再來看看生成的Query節點的結構(定義在src/include/nodes/parsenodes.h)吧。這里貼一下代碼了,上次都因為貼的代碼太多被博客園團隊踢出首頁了,這次求放過。

typedef struct Query
{
    NodeTag		type;
	CmdType		commandType;	/* select|insert|update|delete|utility */
	QuerySource querySource;	/* where did I come from? */
	uint32		queryId;		/* query identifier (can be set by plugins) */
	bool		canSetTag;		/* do I set the command result tag? */
	Node	   *utilityStmt;	/* non-null if this is DECLARE CURSOR or a	non-optimizable statement */
	int			resultRelation; /* rtable index of target relation for INSERT/UPDATE/DELETE; 0 for SELECT */
	bool		hasAggs;		/* has aggregates in tlist or havingQual */
	bool		hasWindowFuncs; /* has window functions in tlist */
	bool		hasSubLinks;	/* has subquery SubLink */
	bool		hasDistinctOn;	/* distinctClause is from DISTINCT ON */
	bool		hasRecursive;	/* WITH RECURSIVE was specified */
	bool		hasModifyingCTE;	/* has INSERT/UPDATE/DELETE in WITH */
	bool		hasForUpdate;	/* FOR [KEY] UPDATE/SHARE was specified */
	bool		hasRowSecurity; /* row security applied? */
	List	   *cteList;		/* WITH list (of CommonTableExpr's) */
	List	   *rtable;			/* list of range table entries */
	FromExpr   *jointree;		/* table join tree (FROM and WHERE clauses) */
	List	   *targetList;		/* target list (of TargetEntry) */
	OnConflictExpr *onConflict; /* ON CONFLICT DO [NOTHING | UPDATE] */
	List	   *returningList;	/* return-values list (of TargetEntry) */
	List	   *groupClause;	/* a list of SortGroupClause's */
	List	   *groupingSets;	/* a list of GroupingSet's if present */
	Node	   *havingQual;		/* qualifications applied to groups */
	List	   *windowClause;	/* a list of WindowClause's */
	List	   *distinctClause; /* a list of SortGroupClause's */
	List	   *sortClause;		/* a list of SortGroupClause's */
	Node	   *limitOffset;	/* # of result tuples to skip (int8 expr) */
	Node	   *limitCount;		/* # of result tuples to return (int8 expr) */
	List	   *rowMarks;		/* a list of RowMarkClause's */
	Node	   *setOperations;	/* set-operation tree if this is top level of a UNION/INTERSECT/EXCEPT query */
	List	   *constraintDeps; /* a list of pg_constraint OIDs that the query depends on to be semantically valid */
	List	   *withCheckOptions;	/* a list of WithCheckOption's, which are
									 * only added during rewrite and therefore
									 * are not written out as part of Query. */
} Query;

值得關注的是commandType,rtable,resultRelation,jointree和targetList這幾個變量,看懂這幾個變量也就比較好懂這個數據結構了。大家看看英文也就懂了,我也不多說了。


3.寫在最后

好吧,水了這么多,這篇算是告一段落了,自己對查詢處理這一塊也有個粗淺的認識了。這里要感謝《PostgreSQL數據庫內核分析》這本書,雖然基於的是8.x的版本,但是對於我理解新版本也很有幫助。感謝圖書的作者的無私奉獻。
下一篇准備繼續未完的事業,讀一讀postgresql的查詢重寫(rewrite)模塊的代碼,大家下期見吧。

最后一句,希望自己能堅持下去,加油。


免責聲明!

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



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