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

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

十分很差意思的是,上篇博客咱們只說明瞭前臺登錄的代碼,沒有介紹前臺登錄過程當中,後臺是如何工做的。即:後臺接到前臺的鏈接請求後發生了什麼?調用了哪些函數?啓動了哪些進程?linux

那麼,咱們就先講講後臺的工做流程吧。sql


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文件裏。app

在這個main函數裏主要作了什麼?我寫在下面:ide

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)模塊的代碼,你們下期見吧。

最後一句,但願本身能堅持下去,加油。

相關文章
相關標籤/搜索