上面這張圖從整體上概括了Postgresql的查詢處理的步驟以及牽涉到的各個模塊,源碼參考自postgresql-12.6。
一、Parser(查詢分析模塊)
查詢分析模塊主要是pg_parse_query函數(\src\backend\tcop\postgres.c 631行),輸入const char * query_string,輸出List *raw_parsetree_list。由於query_string中可能存在多個命令,函數返回值就是多個parsetrees(RawStmt nodes)組成的列表。pg_parse_query函數代碼如下,主要邏輯就是調用raw_parser函數。
1 List *pg_parse_query(const char *query_string) { 2 List *raw_parsetree_list; 3 TRACE_POSTGRESQL_QUERY_PARSE_START(query_string); 4 if (log_parser_stats) 5 ResetUsage(); 6 raw_parsetree_list = raw_parser(query_string); 7 if (log_parser_stats) 8 ShowUsage("PARSER STATISTICS"); 9 #ifdef COPY_PARSE_PLAN_TREES 10 /* Optional debugging check: pass raw parsetrees through copyObject() */ 11 { 12 List *new_list = copyObject(raw_parsetree_list); 13 /* This checks both copyObject() and the equal() routines... */ 14 if (!equal(new_list, raw_parsetree_list)) 15 elog(WARNING, "copyObject() failed to produce an equal raw parse tree"); 16 else 17 raw_parsetree_list = new_list; 18 } 19 #endif 20 /* Currently, outfuncs/readfuncs support is missing for many raw parse tree nodes, so we don't try to implement WRITE_READ_PARSE_PLAN_TREES here. */ 21 TRACE_POSTGRESQL_QUERY_PARSE_DONE(query_string); 22 return raw_parsetree_list; 23 }
語法分析模塊位於src/backend/parser下,src/backend/parser/parser.c包含raw_parser和base_yylex兩個函數。raw_parser函數以字符串格式輸入查詢,返回raw(未分析)的分析樹列表,該列表的元素永遠是RawStmt節點。
先分析一下raw_parser函數返回的RawStmt節點列表的數據結構,RawStmt是任何statement raw分析樹的容器,stmt_location/stmt_len定位該raw statement在源字符串的位置。PG數據庫中的結構體采用了統一的形式,都是基於Node結構體進行擴展,Node結構體值包含一個NodeTag成員,NodeTag是enum類型,RawStmt也不列外。
1 typedef struct RawStmt { 2 NodeTag type; 3 Node *stmt; /* raw parse tree */ 4 int stmt_location; /* start location, or -1 if unknown */ 5 int stmt_len; /* length in bytes; 0 means "rest of string" */ 6 } RawStmt;
以List為例,ListCell的data連接體可以指向RawStmt,從而可以在List存放RawStmt節點,組成raw(未分析)的分析樹列表。
1 typedef struct List{ 2 NodeTag type; /* T_List, T_IntList, or T_OidList */ 3 int length; 4 ListCell *head; 5 ListCell *tail; 6 } List; 7 struct ListCell{ 8 union{ 9 void *ptr_value; 10 int int_value; 11 Oid oid_value; 12 } data; 13 ListCell *next; 14 };
如下所示是新建RawStmt節點的函數,Node *stmt指向makeRawStmt函數的第一個參數。從gram.c中的第25768行可以看到創建RawStmt列表。如第25785行可以看到代碼list_nake1(makeRawStmt((yyvsp[0].node),0))。
1 // gram.c第45878行 2 static RawStmt *makeRawStmt(Node *stmt, int stmt_location){ 3 RawStmt *rs = makeNode(RawStmt); 4 rs->stmt = stmt; 5 rs->stmt_location = stmt_location; 6 rs->stmt_len = 0; /* might get changed later */ 7 return rs; 8 }
查詢分析模塊最重要的函數raw_parser主要執行流程如下:
- 聲明變量yyscanner和yyextra
- 調用 scanner_init函數初始化yyscanner和yyextra
- 設置yyextra.have_lookahead = false
- 調用parser_init設置yyextra->parsetree = NIL
- 調用base_yyparse進行詞法分析
- 調用scanner_finish釋放內存
- 返回語法樹
1 List *raw_parser(const char *str){ 2 core_yyscan_t yyscanner; 3 base_yy_extra_type yyextra; 4 int yyresult; 5 /* initialize the flex scanner */ 6 yyscanner = scanner_init(str, &yyextra.core_yy_extra, &ScanKeywords, ScanKeywordTokens); 7 /* base_yylex() only needs this much initialization */ 8 yyextra.have_lookahead = false; 9 /* initialize the bison parser */ 10 parser_init(&yyextra); 11 /* Parse! */ 12 yyresult = base_yyparse(yyscanner); 13 /* Clean up (release memory) */ 14 scanner_finish(yyscanner); 15 if (yyresult) /* error */ 16 return NIL; 17 return yyextra.parsetree; 18 }
該函數所涉及的變量的類型,core_yyscan_t是空指針類型,base_yy_extra_type是一個結構體,主要保存用於解析詞法語法的私有狀態變量。
1 /* The YY_EXTRA data that a flex scanner allows us to pass around. Private state needed for raw parsing/lexing goes here. 2 */ 3 typedef struct base_yy_extra_type { 4 /* Fields used by the core scanner. */ 5 core_yy_extra_type core_yy_extra; 6 /* State variables for base_yylex(). */ 7 bool have_lookahead; /* is lookahead info valid? */ 8 int lookahead_token; /* one-token lookahead */ 9 core_YYSTYPE lookahead_yylval; /* yylval for lookahead token */ 10 YYLTYPE lookahead_yylloc; /* yylloc for lookahead token */ 11 char *lookahead_end; /* end of current token */ 12 char lookahead_hold_char; /* to be put back at *lookahead_end */ 13 /* State variables that belong to the grammar. */ 14 List *parsetree; /* final parse result is delivered here */ 15 } base_yy_extra_type;
對於base_yy_extra_type結構體中的parsetree是gram.c第25760行的代碼來填充的,代碼如下圖所示。
1.1 詞法和語法分析模塊
上面簡述了查詢分析模塊最重要的函數raw_parser主要執行流程如下:聲明變量yyscanner和yyextra;調用 scanner_init函數初始化yyscanner和yyextra;設置yyextra.have_lookahead = false;調用parser_init設置yyextra->parsetree = NIL;調用base_yyparse進行詞法分析;調用scanner_finish釋放內存;返回語法樹。scanner_init函數聲明變量scanner,調用core_yylex_init為scanner開辟內存空間,調用函數core_yyset_extra(將scanner賦值給yyg(struct yyguts_t結構體,保存了可重入scanner的狀態),yyextra賦值給yyg->yyextra_r),設置yyextra的keywordlist和keyword_tokens等參數,初始化yyextra的文字緩沖區,返回scanner。
parser_init設置yyextra->parsetree = NIL,代碼如下所示:
1 /* parser_init() Initialize to parse one query string */
2 void parser_init(base_yy_extra_type *yyext) {
3 yyext->parsetree = NIL; /* in case grammar forgets to set it */
4 }
我們先通過一個標准SELECT語句分析lex和bison輸出的scan.c和gram.c代碼,從gram.y中給出的SSELECT語句定義可以看出其包括目標列、FROM子句、條件語句等。
1 simple_select: 2 SELECT opt_distinct opt_target_list 3 into_clause from_clause where_clause 4 group_clause having_clause window_clause
由simple_select可以看出,當PG語法分析器識別為第一種情況時,則在相應的動作部分執行創建SelectStmt類型對象並將該對象的targetList、fromClause等域設置為對應的opt_target_list、from_clause等語句的操作。如下所示是該部分代碼在gram.c中的位置。
base_yylex函數是parser和核心lexer(scan.l中的core_yylex)之間的中間filter。
base_yyparse函數
- 聲明各種變量值並初始化,如yystate保存語法分析器當前的狀態,該狀態能反映出剛剛讀取的記號,yyssp指向分析棧棧頂的指針
- yybackup:yylex(base_yylex)函數在這里調用,判斷是否向前看了token,若是則將yyextra內記錄的lookahead_token賦給當前標記cur_token,否則調用函數core_yylex,讀入下一個token;查看cur_token,記錄長度,如果cur_token是NOT、NULLS_P、WITH則執行前看動作,否則直接返回cur_token;調用core_yylex讀入下一個token,賦值給yyextra->lookahead_token;根據本次讀入的next_token,將cur_token(即NOT、NULLS_P、WITH中的一個)替換為NOT_LA,NULLS_LA,WITH_LA;返回cur_token。
- yyreduce根據文法進行規約和語法制導翻譯。
1.2 語義分析模塊
函數parse_analyze(處於analyze.c文件中)主要工作是處理語義分析,其會根據分析樹生成一個對應的查詢樹,函數調用如下所示: query = parse_analyze(parsetree, query_string, paramTypes, numParams, queryEnv)。語義分析階段會檢查命令中是否有不符合語義規定的成分。例如,所使用的表、屬性、過程函數等是否存在,聚集函數(如求和函數SUM、平均函數AVG等)是否可以合法使用等。其主要作用在於檢查該命令是否可以正確執行。語義分析器會根據分析樹中的內容得到更有利於執行的數據,例如,根據表的名字得到其OID,根據屬性名得到其屬性號,根據操作符的名字得到其對應的計算函數等。
1 Query * parse_analyze(RawStmt *parseTree, const char *sourceText, Oid *paramTypes, int numParams, QueryEnvironment *queryEnv){ 2 ParseState *pstate = make_parsestate(NULL); 3 Query *query; 4 Assert(sourceText != NULL); /* required as of 8.4 */ 5 pstate->p_sourcetext = sourceText; 6 if (numParams > 0) 7 parse_fixed_parameters(pstate, paramTypes, numParams); 8 pstate->p_queryEnv = queryEnv; 9 query = transformTopLevelStmt(pstate, parseTree); 10 if (post_parse_analyze_hook) 11 (*post_parse_analyze_hook) (pstate, query); 12 free_parsestate(pstate); 13 return query; 14 }
函數parse_analyze首先將生成一個ParseState結構用於記錄語義分析的狀態,然后通過調用函數transformStmt來完成語義分析過程。transformTopLevelStmt函數負責在調用transformOptionalSelectInto函數之后,將parseTree中的stmt_localtion和stmt_len復制到查詢樹Query相應字段中。而transformOptionalSelectInfo函數的作用是在select語句中還有INTO時,將其轉換為CREATE TABLE AS。
1 Query * transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree){ 2 Query *result; 3 /* We're at top level, so allow SELECT INTO */ 4 result = transformOptionalSelectInto(pstate, parseTree->stmt); 5 result->stmt_location = parseTree->stmt_location; 6 result->stmt_len = parseTree->stmt_len; 7 return result; 8 } 9 10 static Query *transformOptionalSelectInto(ParseState *pstate, Node *parseTree){ 11 if (IsA(parseTree, SelectStmt)){ 12 SelectStmt *stmt = (SelectStmt *) parseTree; 13 /* If it's a set-operation tree, drill down to leftmost SelectStmt */ 14 while (stmt && stmt->op != SETOP_NONE) 15 stmt = stmt->larg; 16 Assert(stmt && IsA(stmt, SelectStmt) &&stmt->larg == NULL); 17 if (stmt->intoClause){ 18 CreateTableAsStmt *ctas = makeNode(CreateTableAsStmt); 19 ctas->query = parseTree; 20 ctas->into = stmt->intoClause; 21 ctas->relkind = OBJECT_TABLE; 22 ctas->is_select_into = true; 23 /* Remove the intoClause from the SelectStmt. This makes it safe for transformSelectStmt to complain if it finds intoClause set (implying that the INTO appeared in a disallowed place). */ 24 stmt->intoClause = NULL; 25 parseTree = (Node *) ctas; 26 } 27 } 28 return transformStmt(pstate, parseTree); 29 }
函數transformStmt會根據不同的查詢類型調用相應的函數進行處理,其將命令類型分為七種情況處理【UTILITY(建表、建索引等附件命令)、EXPLAIN(顯示查詢的執行計划)、DECLARECURSOR(定義游標)、UPDATE、DELETE、INSERT、SELECT、CREATE TABLE AS、CALL】。transformStmt函數只有兩個參數,一個是ParseState,另一個就是要處理的包裝成節點的分析樹。通過節點type字段,transformStmt可以處理七種分析樹,並調用相應的處理函數。
NodeTag值 | 語義分析函數 |
T_InsertStmt | transformInsertStmt |
T_DeleteStmt | transformDeleteStmt |
T_UpdateStmt | transformUpdateStmt |
T_SelectStmt | transformSelectStmt或transformValuesClause或transformSetOperationStmt |
T_DeclareCursorStmt | transformDeclareCursorStmt |
T_ExplainStmt | transformExplainStmt |
T_CreateTableAsStmt | transformCreateTableAsStmt |
T_CallStmt | transformCallStmt |
其他 | 作為Utility類型處理,直接在分析樹上封裝一個Query節點返回 |
1 Query *transformStmt(ParseState *pstate, Node *parseTree){ 2 Query *result; 3 /* We apply RAW_EXPRESSION_COVERAGE_TEST testing to basic DML statements; we can't just run it on everything because raw_expression_tree_walker() doesn't claim to handle utility statements. */ 4 #ifdef RAW_EXPRESSION_COVERAGE_TEST 5 switch (nodeTag(parseTree)){ 6 case T_SelectStmt: 7 case T_InsertStmt: 8 case T_UpdateStmt: 9 case T_DeleteStmt: 10 (void) test_raw_expression_coverage(parseTree, NULL); 11 break; 12 default: 13 break; 14 } 15 #endif /* RAW_EXPRESSION_COVERAGE_TEST */ 16 switch (nodeTag(parseTree)){ 17 /* Optimizable statements */ 18 case T_InsertStmt: 19 result = transformInsertStmt(pstate, (InsertStmt *) parseTree); 20 break; 21 case T_DeleteStmt: 22 result = transformDeleteStmt(pstate, (DeleteStmt *) parseTree); 23 break; 24 case T_UpdateStmt: 25 result = transformUpdateStmt(pstate, (UpdateStmt *) parseTree); 26 break; 27 case T_SelectStmt:{ 28 SelectStmt *n = (SelectStmt *) parseTree; 29 if (n->valuesLists) 30 result = transformValuesClause(pstate, n); 31 else if (n->op == SETOP_NONE) 32 result = transformSelectStmt(pstate, n); 33 else 34 result = transformSetOperationStmt(pstate, n);} 35 break; 36 /* Special cases */ 37 case T_DeclareCursorStmt: 38 result = transformDeclareCursorStmt(pstate,(DeclareCursorStmt *) parseTree); 39 break; 40 case T_ExplainStmt: 41 result = transformExplainStmt(pstate, (ExplainStmt *) parseTree); 42 break; 43 case T_CreateTableAsStmt: 44 result = transformCreateTableAsStmt(pstate,(CreateTableAsStmt *) parseTree); 45 break; 46 case T_CallStmt: 47 result = transformCallStmt(pstate, (CallStmt *) parseTree); 48 break; 49 default: 50 /* other statements don't require any transformation; just return the original parsetree with a Query node plastered on top. */ 51 result = makeNode(Query); 52 result->commandType = CMD_UTILITY; 53 result->utilityStmt = (Node *) parseTree; 54 break; 55 } 56 /* Mark as original query until we learn differently */ 57 result->querySource = QSRC_ORIGINAL; 58 result->canSetTag = true; 59 return result; 60 }
以transformDeleteStmt函數為例,其將Delete語句進行語義解析,會執行函數transformWithClause、setTargetTable、transformFromClause、transformWhereClause、transformReturningList、assign_query_collations、parseCheckAggregates。transformWithClause函數定義在parse_cte.c文件中,setTargetTable、transformFromClause和transformWhereClause定義在parse_clause.c文件中,transformReturnungList函數定義在analyze文件中,assign_query_collations函數定義在parse_cllate.c文件中,parseCheckAggregates函數定義在parse_agg.c文件中。
1 static Query *transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt){ 2 Query *qry = makeNode(Query); 3 ParseNamespaceItem *nsitem; 4 Node *qual; 5 qry->commandType = CMD_DELETE; 6 /* process the WITH clause independently of all else */ 7 if (stmt->withClause){ 8 qry->hasRecursive = stmt->withClause->recursive; 9 qry->cteList = transformWithClause(pstate, stmt->withClause); 10 qry->hasModifyingCTE = pstate->p_hasModifyingCTE; 11 } 12 /* set up range table with just the result rel */ 13 qry->resultRelation = setTargetTable(pstate, stmt->relation,stmt->relation->inh,true,ACL_DELETE); 14 /* grab the namespace item made by setTargetTable */ 15 nsitem = (ParseNamespaceItem *) llast(pstate->p_namespace); 16 /* there's no DISTINCT in DELETE */ 17 qry->distinctClause = NIL; 18 /* subqueries in USING cannot access the result relation */ 19 nsitem->p_lateral_only = true; 20 nsitem->p_lateral_ok = false; 21 /* The USING clause is non-standard SQL syntax, and is equivalent in functionality to the FROM list that can be specified for UPDATE. The USING keyword is used rather than FROM because FROM is already a keyword in the DELETE syntax. */ 22 transformFromClause(pstate, stmt->usingClause); 23 /* remaining clauses can reference the result relation normally */ 24 nsitem->p_lateral_only = false; 25 nsitem->p_lateral_ok = true; 26 qual = transformWhereClause(pstate, stmt->whereClause, EXPR_KIND_WHERE, "WHERE"); 27 qry->returningList = transformReturningList(pstate, stmt->returningList); 28 /* done building the range table and jointree */ 29 qry->rtable = pstate->p_rtable; 30 qry->jointree = makeFromExpr(pstate->p_joinlist, qual); 31 qry->hasSubLinks = pstate->p_hasSubLinks; 32 qry->hasWindowFuncs = pstate->p_hasWindowFuncs; 33 qry->hasTargetSRFs = pstate->p_hasTargetSRFs; 34 qry->hasAggs = pstate->p_hasAggs; 35 assign_query_collations(pstate, qry); 36 /* this must be done after collations, for reliable comparison of exprs */ 37 if (pstate->p_hasAggs) 38 parseCheckAggregates(pstate, qry); 39 return qry; 40 }
由上例可以看出語義分析模塊提供的函數主要集中在如下parse_*.c文件下
1.3 查詢分析模塊調試
打印bison語法分析過程:
修改gram.c(去掉78行的注釋,變為define YYDEBUG 1;將25201行的int yydebug賦值為1,變為int yydebug = 1)
重新編譯源碼
打印parsetree_list:
(gdb)call elog_node_display(17,"parsetree_list",parsetree_list,0)或(gdb)call elog_node_display(17,"parsetree_list",parsetree_list,1)
二、Rewriter(重寫模塊)
查詢重寫的核心就是規則系統,而規則系統則由一系列的規則組成。系統表pg_write存儲重寫規則(每一個元祖代表一條規則)。對於每條規則,該元組的ev_class屬性表示該規則適用的表名,如果在該表的指定屬性(由ev_attr屬性記錄)上執行特定的命令(由ev_type屬性記錄)且滿足了規則的條件(由ev_qual屬性記錄)時,用規則的動作(由ev_action屬性記錄)替換原始命令的動作或將規則的動作附加在原始命令之前(或者之后)。
名字 | 類型 | 引用 | 描述 |
rulename | name | 規則名 | |
ev_class | oid | pg_class.oid | 使用該規則的表名稱 |
ev_attr | int2 | 規則使用的屬性 | |
ev_type | char | 規則使用的命令類型:1=SELECT,2=UPDATE,3=INSERT,4=DELETE | |
is_instead | bool | 如果是INSEAD規則,則為真 | |
ev_qual | text | 規則的條件表達式(WHERE子句) | |
ev_action | text | 規則動作的查詢樹(DO子句) |
根據系統表pg_rewrite的不同屬性,規則可以按兩種方式分類:按照規則適用的命令類型分類,可以分成SELECT、UPDATE、INSERT和DELETE四種;按照規則執行動作的方式分類,可以分成INSTEAD(替代)規則和ALSO規則。
SELECT/INSERT/UPDATE/DELETE四種規則通過其pg_rewrite元組的ev_type屬性值來區分。SELECT規則中只能有一個動作,而且只能是不帶條件的INSTEAD規則,SELECT規則的執行效果類似於視圖,使用方法可以參考“視圖和規則系統”部分的舉例。INSERT/UPDATE/DELETE規則具有以下特性:可以沒有動作,可以有多個動作,可以是INSTEAD型規則(替代規則)或ALSO型規則(缺省),可以使用偽關系NEW和OLD,可以使用規則條件,不是修改查詢樹,而是創建零個或多個新查詢樹並且可能把原始的查詢樹丟棄。
INSTEAD規則和ALSO規則通過規則的pg_rewrite元組的is_instead屬性值來區分,為真表示INSTEAD規則,為假則表示ALSO規則。INSTEAD規則執行動作的方式很簡單,就是用規則中定義的動作替代原始的查詢樹中的對規則所在表的引用。ALSO型規則中原始查詢和規則動作都會被執行,但執行順序根據不同的命令也有所不同,對於INSERT規則,原始查詢在規則動作執行之前完成,這樣可以保證規則動作能引用插入的行,對於UPDATE和DELETE規則,原始查詢在規則動作之后完成,這樣能保證規則動作可以引用將要更新或刪除的元組;否則,那些要訪問舊版本元組的規則動作就無法完成。
PG的視圖是通過規則系統實現。在創建視圖時,系統會自動按照其定義生成相應的規則,當查詢涉及該視圖時,查詢重寫模塊都會用對應的規則對該查詢進行重寫,將對視圖的查詢改寫為對基本表的查詢。在生成視圖的規則時,規則動作是視圖創建命令中SELECT語句的拷貝,並且該規則是無條件的INSTEAD規則。
定義重寫規則
刪除重寫規則
對查詢樹進行重寫
pg_rewrite_query函數對CMD_UTILITY不做任何處理,直接調用list_make1(query)生成querytree_list。對於其他語句進行重寫,調用querytree_list = QueryRewrite(query)。QueryRewrite函數會使用規則系統判斷來進行查詢樹的重寫,並且有可能會將這個查詢樹改寫成一個包含多棵查詢樹的鏈表。查詢重寫的源代碼在src/backend/rewrite文件夾中。
1 static List * pg_rewrite_query(Query *query){ 2 List *querytree_list; 3 if (Debug_print_parse) 4 elog_node_display(LOG, "parse tree", query, Debug_pretty_print); 5 if (log_parser_stats) 6 ResetUsage(); 7 if (query->commandType == CMD_UTILITY){ 8 /* don't rewrite utilities, just dump 'em into result list */ 9 querytree_list = list_make1(query); 10 } else { 11 /* rewrite regular queries */ 12 querytree_list = QueryRewrite(query); 13 } 14 if (log_parser_stats) 15 ShowUsage("REWRITER STATISTICS"); 16 #ifdef COPY_PARSE_PLAN_TREES 17 /* Optional debugging check: pass querytree through copyObject() */ 18 { 19 List *new_list; 20 new_list = copyObject(querytree_list); 21 /* This checks both copyObject() and the equal() routines... */ 22 if (!equal(new_list, querytree_list)) 23 elog(WARNING, "copyObject() failed to produce equal parse tree"); 24 else 25 querytree_list = new_list; 26 } 27 #endif 28 #ifdef WRITE_READ_PARSE_PLAN_TREES 29 /* Optional debugging check: pass querytree through outfuncs/readfuncs */ 30 { 31 List *new_list = NIL; 32 ListCell *lc; 33 /* We currently lack outfuncs/readfuncs support for most utility statement types, so only attempt to write/read non-utility queries. */ 34 foreach(lc, querytree_list){ 35 Query *query = castNode(Query, lfirst(lc)); 36 if (query->commandType != CMD_UTILITY){ 37 char *str = nodeToString(query); 38 Query *new_query = stringToNodeWithLocations(str); 39 /* queryId is not saved in stored rules, but we must preserve it here to avoid breaking pg_stat_statements. */ 40 new_query->queryId = query->queryId; 41 new_list = lappend(new_list, new_query); 42 pfree(str); 43 } else 44 new_list = lappend(new_list, query); 45 } 46 /* This checks both outfuncs/readfuncs and the equal() routines... */ 47 if (!equal(new_list, querytree_list)) 48 elog(WARNING, "outfuncs/readfuncs failed to produce equal parse tree"); 49 else 50 querytree_list = new_list; 51 } 52 #endif 53 if (Debug_print_rewritten) 54 elog_node_display(LOG, "rewritten parse tree", querytree_list, Debug_pretty_print); 55 return querytree_list; 56 }
QueryRewirte函數位於src/backend/rewrite/rewriteHandler.c的第3964行,函數流程共分為三個步驟:1. Apply all non-SELECT rules possibly getting 0 or many queries 2. Apply all the RIR rules on each query 3. Determine which, if any, of the resulting queries is supposed to set the command result tag; and update the canSetTag fields accordingly. If the original query is still in the list, it sets the command tag. Otherwise, the last INSTEAD query of the same kind as the original is allowed to set the tag. (Note these rules can leave us with no query setting the tag. The tcop code has to cope with this by setting up a default tag based on the original un-rewritten query.) The Asserts verify that at most one query in the result list is marked canSetTag. If we aren't checking asserts, we can fall out of the loop as soon as we find the original query.
1 List *QueryRewrite(Query *parsetree){ 2 uint64 input_query_id = parsetree->queryId; 3 List *querylist; 4 List *results; 5 ListCell *l; 6 CmdType origCmdType; 7 bool foundOriginalQuery; 8 Query *lastInstead; 9 Assert(parsetree->querySource == QSRC_ORIGINAL); 10 Assert(parsetree->canSetTag); 11 /* Step 1 */ 12 querylist = RewriteQuery(parsetree, NIL); 13 /* Step 2 */ 14 results = NIL; 15 foreach(l, querylist){ 16 Query *query = (Query *) lfirst(l); 17 query = fireRIRrules(query, NIL); 18 query->queryId = input_query_id; 19 results = lappend(results, query);} 20 /* Step 3 */ 21 origCmdType = parsetree->commandType; 22 foundOriginalQuery = false; 23 lastInstead = NULL; 24 foreach(l, results){ 25 Query *query = (Query *) lfirst(l); 26 if (query->querySource == QSRC_ORIGINAL){ 27 Assert(query->canSetTag); 28 Assert(!foundOriginalQuery); 29 foundOriginalQuery = true; 30 #ifndef USE_ASSERT_CHECKING 31 break; 32 #endif 33 }else{ 34 Assert(!query->canSetTag); 35 if (query->commandType == origCmdType &&(query->querySource == QSRC_INSTEAD_RULE || query->querySource == QSRC_QUAL_INSTEAD_RULE)) 36 lastInstead = query; 37 } 38 } 39 if (!foundOriginalQuery && lastInstead != NULL) 40 lastInstead->canSetTag = true; 41 return results; 42 }
QueryRewrite的處理流程如下:1)用非select規則將一個查詢重寫為0個或多個查詢,此工作通過調用函數RewriteQuery完成 2)對上一步得到的每個查詢分別用RIR規則(無條件INSERT規則,並且只能有一個SELECT規則動作)重寫,通過調用函數fireRIRrules完成 3)將這些查詢樹作為查詢重寫的結果返回。
1 List *pg_analyze_and_rewrite(RawStmt *parsetree, const char *query_string, Oid *paramTypes, int numParams, QueryEnvironment *queryEnv) { 2 Query *query; 3 List *querytree_list; 4 TRACE_POSTGRESQL_QUERY_REWRITE_START(query_string); 5 /* (1) Perform parse analysis. */ 6 if (log_parser_stats) 7 ResetUsage(); 8 query = parse_analyze(parsetree, query_string, paramTypes, numParams, queryEnv); 9 if (log_parser_stats) 10 ShowUsage("PARSE ANALYSIS STATISTICS"); 11 /* (2) Rewrite the queries, as necessary */ 12 querytree_list = pg_rewrite_query(query); TRACE_POSTGRESQL_QUERY_REWRITE_DONE(query_string); 13 return querytree_list; 14 }
三、Planner(查詢計划模塊)
通過數據庫的查詢優化方法分為兩個層次:基於規則的查詢優化(邏輯優化,Rule Based Optimization,簡稱RBO);基於代價的查詢優化(物理優化,Cost Based Optimization,簡稱CBO)。邏輯優化是建立在關系代數基礎上的優化,關系代數中有一些等價的邏輯變換規則,通過對關系代數表達式進行邏輯上的等價變換,可能會獲得執行性能比較好的等式,這樣就能提高查詢的性能;而物理優化則是在建立物理執行路徑的過程中進行優化,關系代數中雖然指定了兩個關系如何進行連接操作,但是這時的連接操作符屬於邏輯運算符,它沒有指定以何種方式實現這種邏輯連接操作,而查詢執行器是不認識關系代數中的邏輯連接操作的,我們需要生成多個物理連接路徑來實現關系代數中的邏輯連接操作,並根據查詢執行器的執行步驟,建立代價計算模式,通過計算所有的物理連接路徑的代價,從中選擇出最優的路徑。
PostgreSQL數據庫的查詢優化的代碼在src/backend/optimizer目錄下,其中有plan、prep、path、geqo、util共5個子目錄,plan是總入口目錄,它調用了prep目錄進行邏輯優化,調用path、geqo目錄進行物理優化,util目錄是一些公共函數,供所有目錄使用。在執行中,從Plan模塊入口,先調用Prep模塊進行預處理,再調用Path模塊進行優化。Path模塊中有開關,指示是否啟用遺傳算法進行優化,如果啟用,且連接的表超過11,就調用geqo目錄中的遺傳算法進行優化。prep目錄主要處理邏輯優化中的邏輯重寫的部分,對投影、選擇條件、集合操作、連接操作都進行了重寫。path目錄則主要是生成物理路徑的部分,包括生成掃描路徑、連接路徑等。geqo目錄主要是實現了一種物理路徑的搜索算法——遺傳算法,通過這種算法可以處理參與連接的表比較多的情況。
查詢規划的最終目的是得到可被執行模塊執行的最優計划,整個過程可分為預處理、生成路徑和生成計划三個階段。預處理實際上是對查詢樹(Query結構體)的進一步改造,這種改造可通過SQL語句體現。在此過程中,最重要的是提升子鏈接和提升子查詢。在生成路徑階段,接收到改造后的查詢樹后,采用動態規划算法或遺傳算法,生成最優連接路徑和候選路徑鏈表。在生成計划階段,用得到的最優路徑,首先生成基本計划樹,然后添加GROUP BY、HAVING和ORDER BY等子句所對應的計划節點形成完整計划樹。
預處理
依照 PostgreSQL 數據庫邏輯優化的源代碼的分布情況,我們把邏輯優化分成了兩部分邏輯重寫優化和邏輯分解優化。划分的依據是:在邏輯重寫優化階段主要還是對查詢樹進行“重寫”,也就是說在查詢樹上進行改造,改造之后還是 顆查詢樹,而在邏輯分解階段,會將查詢樹打散,會重新建立等價於查詢樹的邏輯關系。
生成路徑
生成計划
四、Executor(執行模塊)
同查詢編譯器一樣,查詢執行器也是被函數exec_simple_query調用,從總體上看,查詢執行器實際就是按照執行計划的安排,有機地調用存儲、索引、並發等模塊,按照各種執行計划中各種計划節點的實現算法來完成數據的讀取或者修改的過程。 查詢執行器有四個主要的子模塊:Portal、ProcessUtility、Executot和特定功能子模塊部分。從查詢編譯器輸出執行計划,到執行計划被具體的執行部件處理這一過程,被稱為執行策略的選擇過程,負責完成執行策略選擇的模塊稱為執行策略選擇器。Portal模塊就是這樣的策略選擇器,它會根據輸入執行計划選擇相應的處理模塊(ProcessUtility或Executor)。Exceutor輸入包含了一個查詢計划樹(Plan Tree),用於實現針對數據表中元組的增刪查改等操作。ProcessUtility處理其他各種情況,這些情況間差別很大(如游標、表的模式創建、事務相關操作等),所以在ProcessUtility中為每種情況實現了處理流程。在兩種執行模塊中都少不了各種輔助的子系統,例如執行過程中會涉及表達式計算、投影運算以及元組操作等,這些功能相對獨立,並且在整個查詢執行過程中會被重復調用,因此將其單獨划分為一個模塊(特定功能子模塊)。
- 對於Executor模塊,它根據輸入的查詢計划樹按部就班地處理數據表中元組的增刪改查(DML)操作,它的執行邏輯是統一的(所有的增刪改查最后都歸結為SELECT,只是分別在SELECT的基礎上進行一些額外的操作)。其主要代碼放在src/backend/executor下。
- 對於ProcessUtility模塊,由於處理的是除了增刪改查之外的所有其他操作,而這些操作往往差異很大,例如數據定義操作(DDL),事務的處理以及游標用戶角色定義這些,因此在ProcessUtility模塊中,為每種操作單獨地設計了子過程(函數)去處理。主要代碼在src/backend/commands下。
可優化語句說白了就是DML語句,這些語句的特點就是都要查詢到滿足條件的元組。這類查詢都在查詢規划階段生成了規划樹,而規划樹的生成過程中會根據查詢優化理論進行重寫和優化以提高查詢速度,因此稱作可優化語句,由Executor模塊處理。那反過來講,那些沒有生成執行計划樹的功能性操作就是非可優化語句了,語句之間功能相對獨立,所以也被稱為功能性操作,由ProcessUtility模塊處理。
從以下五個部分介紹查詢執行模塊:1.查詢執行策略 2.非可優化語句的執行 3.可優化語句的執行 4.計划節點 5.其它子功能
1. 查詢執行策略
1.1 五種執行策略
一條簡單的SQL語句會被查詢編譯器轉化為一個執行計划樹或者一個非計划樹操作。而一條復雜的SQL語句往往同時帶有DDL和DML語句,即它會被轉換為一個可執行計划樹和非執行計划樹操作的序列。由查詢編譯器輸出的每一個執行計划樹或者一個非計划樹操作所代表的處理動作稱為一個原子操作。而可執行計划樹和非可執行計划樹是由不同的子模塊去處理的。這樣就有了三種不同的情況,需要三種不同的策略去應對。
然而除此之外,我們還有一種額外的情況需要考慮到:有些SQL語句雖然可以被轉換為一個原子操作,但是其執行過程中由於各種原因需要能夠緩存語句執行的結果,等到整個語句執行完畢在返回執行結果。具體的說:
-
對於可優化語句,當執行修改元組操作時,希望能夠返回被修改的元組(例如帶RETURNING子句的DELETE),由於原子操作的處理過程不能被可能有問題的輸出過程終止,因此不能邊執行邊輸出,因此需要一個緩存結構來臨時存放執行結果;
-
某些非優化語句是需要返回結果的(例如SHOW,EXPLAIN) ,因此也需要一個緩存結構暫存處理結果。
針對以上情況,PG實現了不同的執行流程,共分為五類執行策略:
-
1)PORTAL_ONE_SELECT:處理單個的SELECT語句,調用Executor模塊;
-
2)PORTAL_ONE_RETURNING:處理帶RETURNING的UPDATE/DELETE/INSERT語句,調用Executor模塊;
-
3)PORTAL_UTIL_SELECT:處理單個的數據定義語句,調用ProcessUtility模塊;
-
4)PORTAL_ONE_MOD_WITH:處理帶有INSERT/UPDATE/DELETE的WITH子句的SELECT,其處理邏輯類似PORTAL_ONE_RETURNING。調用Executor模塊;
-
5)PORTAL_MULTI_QUERY:是前面幾種策略的混合,可以處理多個原子操作。
1.2 策略實現
執行策略選擇器的工作是根據查詢編譯階段生成的計划樹鏈表來為當前的查詢選擇五種執行策略中的一種。在這個過程中,執行策略選擇器會使用數據結構PortalData來存儲查詢計划樹鏈表以及最后選中的執行策略等信息,對於Portal這一數據結構定義在src/include/utils/portal.h。
1 typedef struct PortalData{ 2 /* Bookkeeping data */ 3 const char *name;/* portal's name */ 4 ...... 5 /* The query or queries the portal will execute */ 6 const char *sourceText;/* text of query (as of 8.4, never NULL) 原始SQL語句*/ 7 const char *commandTag;/* command tag for original query */ 8 List *stmts;/* PlannedStmts and/or utility statements 查詢編譯器輸出的查詢計划樹鏈表 */ 9 ...... 10 ParamListInfo portalParams; /* params to pass to query */ 11 12 /* Features/options */ 13 PortalStrategy strategy;/* see above 為當前查詢選擇的執行策略 */ 14 PortalStatus;/* Portal的狀態 */ 15 /* If not NULL, Executor is active; call ExecutorEnd eventually: */ 16 QueryDesc *queryDesc;/* info needed for executor invocation 查詢描述符,存儲執行查詢所需的所有信息 */ 17 /* If portal returns tuples, this is their tupdesc: */ 18 TupleDesc tupDesc;/* descriptor for result tuples tupDesc描述可能的返回元組的結構 */ 19 ...... 20 } PortalData;
對於查詢執行器來說,在執行一個SQL語句時都會以一個Portal作為輸入數據,在Portal中存放了與執行該SQL相關的所有信息,例如查詢樹、計划樹和執行狀態等。Portal結構和與之相關的主要字段的結構如下所示:
stmts字段是由查詢編譯器輸出的原子操作的鏈表,,圖中僅給出兩種可能的原子操作PlannedStmt和 Query,兩者都能包含查詢計划樹,用於保存含有查詢的操作。當然,有些含有查詢計划樹的原子操作不一定是SELECT語句,例如游標的聲明(utilityStmt字段不為空),以及SELECT INTO類型的語句(intoClause字段不為空)。對於UPDATE、INSERT、DELETE類型,含有RETURNING子句時returningList字段不為空。
PostgreSQL主要根據原子操作的命令類型以及stmts中原子操作的個數來為Portal選擇合適的執行策略。由查詢編譯器輸出的每一個查詢計划樹中都包含一個類型為CmdType的字段,用於標識該原子操作對應的命令類型。命令類型分類如下:
1 // src/include/nodes/nodes.h 2 typedef enum CmdType{ 3 CMD_UNKNOWN, /* 未定義 */ 4 CMD_SELECT, /* select stmt SELECT查詢類型 */ 5 CMD_UPDATE,/* update stmt 更新操作 */ 6 CMD_INSERT, /* insert stmt 插入操作 */ 7 CMD_DELETE, /* 刪除操作 */ 8 CMD_UTILITY, /* cmds like create, destroy, copy, vacuum, 功能性操作(數據定義語句) 9 * etc. */ 10 CMD_NOTHING /* dummy command for instead nothing rules 用於由查詢編譯器新生成的操作,即如果一個語句通過編譯器的處理之后需要額外生成一個附加的操作,則該操作的命令類型就被設置為CMD_NOTHING 11 * with qual */ 12 } CmdType;
選擇PORTAL_ONE_SELECT策略應滿足以下條件:stmts鏈表中只有一個PlannedStmt類型或是Query類型的節點;節點是CMD_SELECT類型操作;節點的utilityStmt字段和intoClause字段為空。
選擇PORTAL_UTIL_SELECT策略應滿足以下條件:stmts鏈表僅有的一個Query類型的節點;節點是CMD_UTILITY類型操作;節點的utilityStmt字段保存的是FETCH語句(類型為T_FetchStmt)、EXECUTE語句(類型為T_ExecuteStmt)、EXPLAIN語句(類型為T_ExplainStmt)或是SHOW語句(類型為T_VariableShowStmt)之一。
選擇PORTAL_ONE_RETURNING策略適用於stmts鏈表中只有一個包含RETURNING子句(returningList不為空)的原子操作。其他的各種情況都將以PORTAL_MULTI_QUERY策略進行處理。
執行策略選擇器的主函數名為ChoosePortalStrategy,其輸入為PortalData的stmts鏈表,輸出的是預先定義的執行策略枚舉類型PortalStrategy。
1.3 Portal的執行過程
Portal是查詢執行器執行一個SQL語句的門戶,所有SQL語句的執行都從一個選擇好執行策略的Portal開始。所有Portal的執行過程都必須依次調用PortalStart(初始化)、PortalRun(執行)、PortalDrop(清理)三個過程,PostgreSQL為Portal提供的幾種執行策略實現了單獨的執行流程,每種策略的Portal在執行時會經過不同的處理過程。
Portal的創建、初始化、執行及清理過程都在exec_simple_query函數中進行,其過程如下:
1) 調用函數CreatePortal創建一個干凈的Portal,其中內存上下文、資源跟蹤器、清理函數等都已經設置好,但sourceText、stmts等字段並沒有設置。
2) 調用函數PortalDefineQuery為剛創建的Portal設置sourceText、stmts等字段,這些字段的值都來自於查詢編譯器輸出的結果,其中還會將Portal的狀態設置為PORTAL_DEFINED表示Portal已被定義。
3) 調用函數PortalStart對定義好的Portal進行初始化,初始化工作主要如下:
1. 調用ChoosePortalStrategy為Portal選擇策略
2. 如果選擇的是PORTAL_ONE_SELECCT策略,調用CreateQueryDesc為Portal創建查詢描述符
3. 如果選擇的是PORTAL_ONE_RETURNING或者PORTAL_UTIL_SELECT策略,為Portal創建返回元組的描述符
4. 將Portal的狀態設置為PORTAL_READY,表示Portal已經初始化好,准備開始執行
4) 調用函數PortalRun執行Portal,該函數將按照Portal中執行的策略調用相應的執行部件來執行Portal。
5) 調用函數PortalDrop清理Portal,主要是對Portal運行中所占用的資源進行釋放,特別是用於緩存結果的資源。
對於PORTAL_ONE_SELECT策略的Portal,其中包含一個簡單SELECT類型的查詢計划樹,在PortalStart中將調用ExecutorStart進行Executor初始化,然后在PortalRun中調用ExecutorRun開始執行器的執行過程。PORTAL_ONE_RETURNING和PORTAL_UTIL_SELECT策略需要在執行后將結果緩存,然后將緩存的結果按要求進行返回。因此,在PortalStart中僅會初始化返回元組的結構描述信息。接着PortalRun會調用FillPortalStore執行查詢計划得到所有的結果元組並填充到緩存中,然后調用RunFromStore從緩存中獲取元組並返回。從圖中可以看到,FillPortalStore中對於查詢計划的執行會根據策略不同而調用不同的處理部件,PORTAL_ONE_RETURING策略會使用PorttalRunMulti進行處理,而PORTAL_UTIL_SELECT使用PortalRunUtility處理。Portal_MULTI_QUERY策略在執行過程中,PortalRun會使用PortalRunMulti進行處理。
2. 非可優化語句的執行
數據定義語言是一類用於定義數據模式、函數等的功能性語句。不同於元組增刪查改的操作,其處理方式是為每一種類型的描述語句調用相應的處理函數。數據定義語句的處理過程比較簡單,其執行流程最終會進入到ProcessUtility處理器,然后執行語句對應的不同處理過程。由於數據定義語句的種類很多,因此整個處理過程中的數據結構和方式種類繁冗、復雜,但流程相對簡單、固定。
數據定義語句執行流程
由於ProcessUtility需要處理所有類型的數據定義語句,因此其輸入數據結構的類型也是各種各樣,每種類型的數據結構表示不同的操作類型。ProcessUtility將通過判斷數據結構中NodeTag字段的值來區分各種不同節點,並引導執行流程進入相應的處理函數。針對各種不同的查詢樹,
執行實例
1 CREATE TABLE course (no SERIAL, name VARCHAR, credit INT, CONSTRAINT con1 CHECK(credit >= 0 AND name <>"), PRIMARY KEY (no));
查詢編譯器會生成一個僅包含一個T_CreateStmt類型節點的查詢樹鏈表,因此對應的Portal的stmts字段中也只包含一個T_CreateStmt類型節點。創建及初始化Portal-->PortalStart-->ChooseProtalStrategy:ChooseProtalStrategy函數根據stmts字段值選擇策略時會選擇PORTAL_MULTI_QUERY策略。調用Portal執行過程-->PortalRun-->PortalRunMulti-->PortalRunUtility-->ProcessUtility-->transformCreateStmt-->DefineRelation:PortalRun函數將會調用PortalRunMulti來執行PORTAL_MULTI_QUERY策略,將會把處理流程引導到ProcessUtility中,ProcessUtility將首先調用函數trandformCreateStmt對T_CreateStmt節點進行轉換處理。
主要的功能處理器函數
3. 可優化語句的執行
物理代數與處理模型
物理操作符的數據結構
執行器的運行
執行實例
4. 計划節點
控制節點
掃描節點
物化節點
連接節點
5. 其它子功能
元組操作
表達式計算
投影操作
五、各個模塊tree打印
打印parse tree、rewritten parse tree、plan tree,在postgresql.conf文件中,修改如下配置為on
#debug_print_parse = off --> 開啟該選項所對應執行的代碼處於pg_rewrite_query函數(postgres.c 773行) if (Debug_print_parse) elog_node_display(LOG,"parse tree",query,Debug_pretty_print);
#debug_print_rewritten = off --> 開啟該選項所對應執行的代碼處於pg_rewrite_query函數(postgres.c 848行)if (Debug_print_rewritten) elog_node_display(LOG,"rewritten parse tree",querytree_list,Debug_pretty_print);
#debug_print_plan = off --> 開啟該選項所對應執行的代碼處於pg_plan_query函數(postgres.c 929行)if (Debug_print_plan) elog_node_display(LOG,"plan",plan,Debug_pretty_print);
#debug_pretty_print = on
也可在gdb調試情況下,使用(gdb) call elog_node_display(17, "what ever", Node * var, 0 or 1),17代表INFO,詳見elog.h,比如希望打印在日志里,將17替換為16,0或1取決於debug_pretty_print為on還是off。
將打印信息圖形化工具:https://github.com/shenyuflying/pgNodeGraph
使用流程:1. copy and paste the node tree in text form 2. put it in node dir 3. run ./pgNodeGraph