openGauss資料庫原始碼解析系列文章--openGauss簡介(一)

Gauss松鼠會發表於2022-11-11

openGauss資料庫是華為深度融合在資料庫領域多年經驗,結合企業級場景要求推出的新一代企業級開源資料庫。此前,Gauss松鼠會已經發布了openGauss資料庫核心技術系列文章,介紹了openGauss的技術原理。從本期開始,Gauss松鼠會將陸續推出openGauss資料庫原始碼解析系列文章,帶你解析openGauss各功能模組的原始碼邏輯和實現原理。該系列文章主要面向openGauss核心開發者及研究人員。

接下來會先向大家概要介紹openGauss。本篇將從openGauss概述、應用場景、系統結構、程式碼結構四個方面對openGauss進行介紹。

一、openGauss概述

openGauss是關係型資料庫,採用客戶端/伺服器,單程式多執行緒架構;支援單機和一主多備部署方式,同時支援備機可讀、雙機高可用等特性。

openGauss有如下基本功能:

1、支援標準SQL

openGauss資料庫支援標準的SQL(Structured Query Language,結構化查詢語言)。SQL標準是一個國際性的標準,定期會進行更新和演進。SQL標準的定義分成核心特性以及可選特性,絕大部分的資料庫都沒有100%支撐SQL標準。openGauss資料庫支援SQL92/SQL99/SQL2003等,同時支援SQL2011大部分的核心特性,另外還支援部分的可選特性。

2、支援標準開發介面

openGauss資料庫提供業界標準的ODBC(Open Database Connectivity,開放式資料庫連線)及JDBC(Java Database Connectivity,java資料庫連線)介面,保證使用者能將業務快速遷移至openGauss。目前支援標準的ODBC3.5及JDBC4.0介面,其中ODBC能夠支援CentOS、openEuler、SUSE、Win32、Win64等平臺,JDBC無平臺差異。

3、混合儲存引擎支援

openGauss資料庫支援行儲存引擎、列儲存引擎和記憶體儲存引擎等。行存分為“inplace update” 和 “append update”兩種模式,前者透過單獨的回滾段(undo log)來保留元組的前像以解決讀寫衝突,可以更自然的支援資料更新;後者將更新記錄混雜在資料記錄中,透過新舊版本的形式來支援資料更新,對於舊版本需要定期做vacuum操作來支援磁碟空間的回收。列存支援資料快速分析,更適合OLAP(Online Analytical Processing,聯機分析處理)業務。記憶體引擎支援實時資料處理,對有極致效能要求的業務提供支撐。

4、事務支援

事務支援指的就是系統提供事務的能力,openGauss支援事務的原子性、一致性、隔離性和永續性。事務支援及資料一致性保證是絕大多數資料庫的基本功能,只有支援了事務,才能滿足事務化的應用需求。

  • A(Atomicity):原子性。整個事務中的所有操作,要麼全部完成,要麼全部不完成,不可能停滯在中間某個環節。
  • C(Consistency):一致性。事務需要保證從一個執行性狀態轉移到另一個一致性狀態,不能違反資料庫的一致性約束。
  • I(Isolation):隔離性。隔離事務的執行狀態,使它們好像是系統在給定時間內執行的唯一操作。例如有兩個事務併發執行,事務的隔離性將確保每一事務在系統中認為只有該事務在使用系統。
  • D(Durability):永續性。在事務提交以後,該事務對資料庫所作的更改便持久的儲存在資料庫之中,不會因掉電,程式異常故障而丟失。

openGauss資料庫支援事務的隔離級別有讀已提交和可重複讀,預設隔離級別是讀已提交,保證不會讀到髒資料。

事務分為隱式事務和顯式事務,顯式事務的相關基礎介面如下:

  • Start transaction:事務開啟
  • Commit:事務提交
  • Rollback:事務回滾

另有使用者還可以透過“Set transaction”命令設定事務的隔離級別、讀寫模式或可推遲模式。

5、軟硬結合

openGauss資料庫支援軟硬體的結合,包括多核的併發訪問控制、基於SSD(Solid-State Drive,固態硬碟)的IO(輸入/輸出Input/Output,)最佳化、智慧的Buffer Pool(緩衝池)資料管理。

6、智慧的最佳化器

openGauss資料庫提供了智慧的代價模型、智慧計劃選擇,可以顯著提升資料庫效能。openGauss的執行器包含了向量化執行和LLVM(Low Level Virtual Machine,底層虛擬機器,一種構架編譯器的框架系統)編譯執行,可以顯著提升資料庫效能。

7、AI的支援

傳統資料庫生態依賴於DBA(Database Administrator,資料庫管理員)進行資料的管理、維護、監控、最佳化。但是在大量的資料庫例項中,DBA難以支援海量例項,而AI(Artificial Intelligence ,人工智慧)則可以自動最佳化資料庫,openGauss資料庫的AI功能包括AI自調優、AI索引推薦、AI慢SQL診斷等。

8、安全的支援

openGauss資料庫具有非常好的安全特性,包括透明加密(即在磁碟的儲存檔案是加密的)、全密態(資料傳輸、儲存、計算都是加密的)、防篡改(使用者不可篡改)、敏感資料智慧發現等。

9、函式及儲存過程支援

函式和儲存過程是資料庫中的一種重要物件,主要功能將使用者特定功能的SQL語句集進行封裝,並方便呼叫。儲存過程是SQL、PL/SQL(Procedural Language SQL,過程語言SQL)的組合。儲存過程可以使執行商業規則的程式碼從應用程式中移動到資料庫。從而程式碼儲存一次能夠被多個程式使用。

  • 允許客戶模組化程式設計,對SQL語句集進行封裝,呼叫方便。
  • 儲存過程會進行編譯快取,可以提升使用者執行SQL語句集的速度。
  • 系統管理員透過對執行某一儲存過程的許可權進行限制,能夠實現對相應資料訪問許可權的限制,避免了非授權使用者對資料的訪問,保證了資料的安全。
  • 為了處理SQL語句,儲存過程分配一段記憶體區域來儲存上下文。遊標是指向上下文區域的控制程式碼或指標。藉助遊標,儲存過程可以控制上下文區域的變化。
  • 支援6種異常資訊級別方便客戶對儲存過程進行除錯。支援儲存過程除錯,儲存過程除錯是一種除錯手段,可以在儲存過程開發中,一步一步跟蹤儲存過程執行的流程,根據變數的值,找到錯誤的原因或者程式的bug,提高問題定位效率。支援設定斷點和單步除錯。

openGauss支援SQL標準中的函式及儲存過程,增強了儲存過程的易用性。

10、PostgreSQL介面相容

相容PSQL客戶端,相容PostgreSQL標準介面。

11、支援SQL hint

支援SQL hint(hints是SQL語句的註釋,可以指導最佳化器選擇人為指定的執行計劃。)影響執行計劃生成、提升SQL查詢效能。Plan Hint為使用者提供了直接影響執行計劃生成的手段,使用者可以透過指定Join順序\Join、Scan方法\指定結果行數\等多個手段來進行執行計劃的調優,以提升查詢的效能。

12、Copy介面支援容錯機制

openGauss資料庫提供使用者封裝好的函式來建立Copy錯誤表,並允許使用者在使用“Copy From”指令時指定容錯選項,使得“Copy From”語句在執行過程中部分解析、資料格式、字符集等相關的報錯不會報錯中斷事務、而是被記錄至錯誤表中,使得在“Copy From”的目標檔案即使有少量資料錯誤也可以完成入庫操作。使用者隨後可以在錯誤表中對相關的錯誤進行定位以及進一步排查。

二、 應用場景

openGauss資料庫主要的應用場景分為:

1、交易型應用

大併發、大資料量、以聯機事務處理為主的交易型應用,如電商、金融、O2O、電信CRM/計費等,應用可按需選擇不同的主備部署模式。

2、物聯網資料

在工業監控和遠端控制、智慧城市的延展、智慧家居、車聯網等物聯網場景下,感測監控裝置多、取樣率高、資料儲存為追加模型,操作和分析並重的場景。

三、系統架構

openGauss主要包含了openGauss伺服器、客戶端驅動、OM等模組,它的架構如圖1所示,模組說明如表1所示。

在這裡插入圖片描述

|圖1 openGauss軟體架構|

表1 openGauss模組說明

名稱 描述
OM 運維管理模組(Operation Manager)。提供openGauss日常運維、配置管理的管理介面、工具
客戶端驅動 客戶端驅動(Client Driver)。負責接收來自應用的訪問請求,並嚮應用返回執行結果;負責與openGauss例項的通訊,下發SQL在openGauss例項上執行,並接收命令執行結果
openGauss主(備) openGauss主(備)。負責儲存業務資料(支援行存、列存、記憶體表儲存)、執行資料查詢任務以及向客戶端驅動返回執行結果
Storage 伺服器的本地儲存資源,持久化儲存資料

四、 程式碼結構

(一)通訊管理

openGauss查詢響應是使用簡單的“單一使用者對應一個伺服器執行緒”的客戶端/伺服器模型實現的。由於我們無法提前知道需要建立多少個連線,因此必須使用主程式(GaussMaster)主程式在指定的TCP/IP(Transmission Control Protocol/Internet Protocol,傳輸控制協議/網際網路協議)埠上偵聽傳入的連線,只要檢測到連線請求,主程式就會生成一個新的伺服器執行緒。伺服器執行緒之間使用訊號量和共享記憶體相互通訊,以確保整個併發資料訪問期間的資料完整性。

客戶端程式可以被理解為滿足openGauss協議的任何程式。許多客戶端都基於C語言庫libpq進行通訊,但是該協議有幾種獨立的實現,例如Java JDBC驅動程式。

建立連線後,客戶端程式可以將查詢傳送到後端伺服器。查詢是使用純文字傳輸的,即在前端(客戶端)中沒有進行解析。伺服器解析查詢語句、建立執行計劃、執行並透過在已建立的連線上傳輸檢索到的結果集,將其返回給客戶端。

openGauss資料庫中處理客戶端連線請求的模組叫做postmaster。前端程式傳送啟動資訊給postmaster,postmaster根據資訊內容建立後端響應執行緒。postmaster也管理系統級的操作比如呼叫啟動和關閉程式。postmaster在啟動時建立共享記憶體和訊號量池,但它自身不管理記憶體、訊號量和鎖操作。

當客戶端發來一個請求資訊,postmaster立刻啟動一個新會話,新會話對請求進行驗證,驗證成功後為它匹配後端工作執行緒。這種模式架構上處理簡單,但是高併發下由於執行緒過多,切換和輕量級鎖區域的衝突過大導致效能急劇下降。因此openGauss透過執行緒資源池化複用的技術來解決該問題。執行緒池技術的整體設計思想是執行緒資源池化,並且在不同連線直接複用。

1. postmaster原始碼組織

postmaster原始碼目錄為:/src/gausskernel/process/postmaster。postmaster原始碼檔案如表2所示。

表2 postmaster原始碼檔案

postmaster.cpp 使用者響應主程式
aiocompleter.cpp 完成預取(Prefetch)和後端寫(BackWrite)I/O操作
alarmchecker.cpp 鬧鐘檢查執行緒
lwlockmonitor.cpp 輕量鎖的死鎖檢測
pagewriter.cpp 寫頁面
pgarch.cpp 日誌存檔
pgaudit.cpp 審計執行緒
pgstat.cpp 統計資訊收集
startup.cpp 服務初始化和恢復
syslogger.cpp 捕捉並寫所有錯誤日誌
autovacuum.cpp 垃圾清理執行緒
bgworker.cpp 後臺工作執行緒(服務共享記憶體)
bgwriter.cpp 後臺寫執行緒(寫共享快取)
cbmwriter.cpp remoteservice.cpp postmaster訊號處理
checkpointer.cpp 檢查點處理
fencedudf.cpp 保護模式下執行使用者定義函式
gaussdb_version.cpp 版本特性控制
twophasecleaner.cpp 清理兩階段事務執行緒
walwriter.cpp 預寫式日誌寫入

2. postmaster主流程

postmaster主流程程式碼如下。

/* postmaster.cpp */...int PostmasterMain(int argc, char* argv[]){InitializePostmasterGUC(); /*  初始化postmaster配置引數*/...pgaudit_agent_init(); /*  初始化審計模組*/...for (i = 0; i < MAXLISTEN; i++) /* 建立輸入socket監聽*/ 
 t_thrd.postmaster_cxt.ListenSocket[i] = PGINVALID_SOCKET;
 ...
 /*  建立共享記憶體和訊號量池 */reset_shared(g_instance.attr.attr_network.PostPortNumber);
 ...
 /*  初始化postmaster訊號管理 */gs_signal_slots_init(GLOBAL_ALL_PROCS + EXTERN_SLOTS_NUM);
 ...
 InitPostmasterDeathWatchHandle(); /*  初始化當機監聽*
 ...
 pgstat_init(); /*  初始化統計資料收集子系統*/
 InitializeWorkloadManager(); /*  初始化工作負載管理器*/
 ...
 InitUniqueSQL(); /*  初始化unique sql資源*/
 ...
 autovac_init(); /*  初始化垃圾清理執行緒子系統*/
 ...
 status = ServerLoop(); /*  啟動postmaster主業務迴圈*/
 ...
 }

(二) SQL引擎

資料庫的SQL引擎是資料庫重要的子系統之一,它對上負責承接應用程式傳送過來的SQL語句,對下則負責指揮執行器執行執行計劃。其中最佳化器作為SQL引擎中最重要、最複雜的模組,被稱為資料庫的“大腦”,最佳化器產生的執行計劃的優劣直接決定資料庫的效能。

本篇從SQL語句傳送到資料庫伺服器開始,對SQL引擎的各個模組進行全面的介紹與原始碼解析,以實現對SQL語句執行的邏輯與原始碼更深入的理解。其響應流程如圖1所示。

在這裡插入圖片描述

|圖2 openGauss資料庫SQL查詢響應流程|

1. 查詢解析——parser

SQL解析對輸入的SQL語句進行詞法分析、語法分析、語義分析,獲得查詢解析樹或者邏輯計劃。SQL查詢語句解析的解析器(parser)階段包括如下:

  • 詞法分析:從查詢語句中識別出系統支援的關鍵字、識別符號、運算子、終結符等,每個詞確定自己固有的詞性。
  • 語法分析:根據SQL語言的標準定義語法規則,使用詞法分析中產生的詞去匹配語法規則,如果一個SQL語句能夠匹配一個語法規則,則生成對應的語法樹(Abstract Synatax Tree,AST)。
  • 語義分析:對語法樹(AST)進行檢查與分析,檢查AST中對應的表、列、函式、表示式是否有對應的後設資料(指資料庫中定義有關資料特徵的資料,用來檢索資料庫資訊)描述,基於分析結果對語法樹進行擴充,輸出查詢樹。主要檢查的內容包括:

①檢查關係的使用:FROM子句中出現的關係必須是該查詢對應模式中的關係或檢視。

②檢查與解析屬性的使用:在SELECT句中或者WHERE子句中出現的各個屬性必須是FROM子句中某個關係或檢視的屬性。

③檢查資料型別:所有屬性的資料型別必須是匹配的。

詞法和語法分析程式碼基於gram.y和scan.l中定義的規則,使用Unix工具bison和flex構建產生。其中,詞法分析器在檔案scan.l中定義,它負責識別識別符號、SQL關鍵字等。對於找到的每個關鍵字或識別符號,都會生成一個標記並將其傳遞給解析器。語法解析器在檔案gram.y中定義,由一組語法規則和每當觸發規則時執行的動作組成,基於這些動作程式碼構架並輸出語法樹。在解析過程中,如果語法正確,則進入語義分析階段並建立查詢樹返回,否則將返回錯誤,終止解析過程。

解析器在詞法和語法分析階段僅使用有關SQL語法結構的固定規則來建立語法樹。它不會在系統目錄中進行任何查詢,因此無法理解所請求操作的詳細語義。

語法解析完成後,語義分析過程將解析器返回的語法樹作為輸入,並進行語義分析以瞭解查詢所引用的表、函式和運算子。用來表示此資訊的資料結構稱為查詢樹。解析器解析過程分為原始解析與語義分析,分開的原因是,系統目錄查詢只能在事務內完成,並且我們不希望在收到查詢字串後立即啟動事務。原始解析階段足以識別事務控制命令(BEGIN,ROLLBACK等),然後可以正確執行這些命令而無需任何進一步分析。一旦知道我們正在處理實際查詢(例如SELECT或UPDATE),就可以開始事務,這時才呼叫語義分析過程。

1) parser原始碼組織

parser原始碼目錄為:/src/common/backend/parser。parser原始碼檔案如表3所示。

表3 parser原始碼檔案

parser.cpp 解析主程式
scan.l 詞法分析,分解查詢成token
scansup.cpp 處理查詢語句轉義符
kwlookup.cpp 將關鍵詞轉化為具體的token
keywords.cpp 標準關鍵詞列表
analyze.cpp 語義分析
gram.y 語法分析,解析查詢tokens併產生原始解析樹
parse_agg.cpp 處理聚集操作,比如SUM(col1),AVG(col2)
parse_clause.cpp 處理子句,比如WHERE,ORDER BY
parse_compatibility.cpp 處理資料庫相容語法和特性支援
parse_coerce.cpp 處理表示式資料型別強制轉換
parse_collate.cpp 對完成表示式新增校對資訊
parse_cte.cpp 處理公共表格表示式(WITH 子句)
parse_expr.cpp 處理表示式,比如col, col+3, x = 3
parse_func.cpp 處理函式,table.column和列識別符號
parse_node.cpp 對各種結構建立解析節點
parse_oper.cpp 處理表示式中的運算子
parse_param.cpp 處理引數
parse_relation.cpp 支援表和列的關係處理程式
parse_target.cpp 處理查詢解析的結果列表
parse_type.cpp 處理資料型別
parse_utilcmd.cpp 處理實用命令的解析分析

2) parser主流程

parser主流程程式碼如下。

/* parser.cpp */.../* 原始解析器,輸入查詢字串,做詞法和語法分析,返回原始語法解析樹列表*/List* raw_parser(const char* str, List** query_string_locationlist){...
    /* 初始化 flex scanner */    yyscanner = scanner_init(str, &yyextra.core_yy_extra, ScanKeywords, NumScanKeywords);
    ...  
      /* 初始化 bison parser */
          parser_init(&yyextra);
    /* 解析! */ 
       yyresult = base_yyparse(yyscanner);
    /* 清理釋放記憶體*/  
      scanner_finish(yyscanner);
      ...  
        return yyextra.parsetree;  
         }
         /* analyze.cpp */
         ...
         /* 分析原始語法解析樹,做語義分析並輸出查詢樹 */
         Query* parse_analyze(
             Node* parseTree, const char* sourceText, Oid* paramTypes, int numParams, bool isFirstNode, bool isCreateView)
             {
                 /* 初始化解析狀態和查詢樹 */ 
                    ParseState* pstate = make_parsestate(NULL); 
                       Query* query = NULL;
                       ...   
                        /* 將解析樹轉化為查詢樹 */
                            query = transformTopLevelStmt(pstate, parseTree, isFirstNode, isCreateView);
                            ...   
                             /* 釋放解析狀態 */ 
                                free_parsestate(pstate);
                                ... 
                                   return query;
                                   }

2. SQL查詢分流——traffic cop

traffic cop模組負責查詢的分流,它負責區分簡單和複雜的查詢指令。事務控制命令(例如BEGIN和ROLLBACK)非常簡單,因此不需要其它處理,而其它命令(例如SELECT和JOIN)則傳遞給重寫器(參考第6章)。這種區分透過對簡單命令執行最少的最佳化,並將更多的時間投入到複雜的命令上,從而減少了處理時間。簡單和複雜查詢指令也對應如下2類解析:

  • 軟解析(簡單,舊查詢):當openGauss共享緩衝區中存在已提交SQL語句的已解析表示形式時,可以重複利用快取內容執行語法和語義檢查,避免查詢最佳化的相對昂貴的操作。
  • 硬解析(複雜,新查詢):如果無快取語句可重用,或者第一次將SQL語句載入到openGauss共享緩衝區中,則會導致硬解析。同樣,當一條語句在共享緩衝區中老化時,再次重新載入該語句時,還會導致另一次硬解析。因此,共享Buffer的大小也會影響解析呼叫的數量。

我們可以查詢gs_prepared_statements來檢視快取了什麼,以區分軟/硬解析(它僅對當前會話可見)。此外,gs_buffercache模組提供了一種實時檢查共享緩衝區快取記憶體內容的方法,它甚至可以分辨出有多少資料塊來自磁碟,有多少資料來自共享緩衝區。

1) traffic cop(tcop)原始碼組織

traffic cop(tcop)原始碼目錄為:/src/common/backend/tcop。traffic cop(tcop)原始碼檔案如表4所示。

表4 traffic cop(tcop)原始碼檔案

auditfuncs.cpp 記錄資料庫操作審計資訊
autonomous.cpp 建立可被用來執行SQL查詢的自動會話
dest.cpp 與查詢結果被髮往的終點通訊
utility.cpp 資料庫通用指令控制函式
fastpath.cpp 在事務期間快取操作函式和型別等資訊
postgres.cpp 後端伺服器主程式
pquery.cpp 查詢處理指令
stmt_retry.cpp SQL語句錯誤錯誤碼並重試

2) traffic cop主流程

traffic cop主流程程式碼如下。

/*postgres.cpp*/.../*原始解析器,輸入查詢字串,做詞法和語法分析,返回原始解析樹列表*/int PostgresMain(int argc, char* argv[], const char* dbname, const char* username){... 
   /* 整體初始化*/
       t_thrd.proc_cxt.PostInit->SetDatabaseAndUser(dbname, InvalidOid, username); 
          ...   
           /* 自動事務的錯誤處理 */ 
              if (sigsetjmp(local_sigjmp_buf, 1) != 0) { ... }
              ...
              /* 錯誤語句的重新嘗試階段 */
              if (IsStmtRetryEnabled() && StmtRetryController->IsQueryRetrying()){ ... }  
                /* 無錯誤查詢指令迴圈處理*/
                    for (;;) {
                    ...
                    /* 按命令型別執行處理流程*/switch(firstchar){
                    ...
                    case: 'Q': ... /* 簡單查詢 */case: 'P': ... /* 解析 */case: 'E': ... /* 執行 */  
 }
 ...  
   } 
      ...
      }

3. 查詢重寫——rewriter

查詢重寫利用已有語句特徵和關係代數運算來生成更高效的等價語句,在資料庫最佳化器中扮演關鍵角色;尤其在複雜查詢中,能夠在效能上帶來數量級的提升,可謂是“立竿見影”的“黑科技”。SQL語言是豐富多樣的,非常的靈活,不同的開發人員依據經驗的不同,手寫的SQL語句也是各式各樣,另外還可以透過工具自動生成。同時SQL語言是一種描述性語言,資料庫的使用者只是描述了想要的結果,而不關心資料的具體獲取方式,輸入資料庫的SQL語言很難做到是以最優形式表示的,往往隱含了一些冗餘資訊,這些資訊可以被挖掘用來生成更加高效的SQL語句。查詢重寫就是把使用者輸入的SQL語句轉換為更高效的等價SQL,查詢重寫遵循2個基本原則:

  • 等價性:原語句和重寫後的語句,輸出結果相同。
  • 高效性:重寫後的語句,比原語句在執行時間和資源使用上更高效。

介紹openGauss如下幾個關鍵的查詢重寫技術:

  • 常量表示式化簡:常量表示式即使用者輸入SQL語句中包含運算結果為常量的表示式,分為算數表示式、邏輯運算表示式、函式表示式。查詢重寫可以對常量表示式預先計算以提升效率。例如“SELECT * FROM table WHERE a=1+1; ”語句被重寫為“SELECT * FROM table WHERE a=2”語句。
  • 子查詢提升:由於子查詢表示的結構更清晰,符合人的閱讀理解習慣,使用者輸入的SQL語句往往包含了大量的子查詢,但是相關子查詢往往需要使用巢狀迴圈的方法來實現,執行效率較低,因此將子查詢最佳化為“Semi Join”的形式可以在最佳化規劃時選擇其它的執行方法,或能提高執行效率。例如“SELECT * FROM t1 WHERE t1.a in (SELECT t2.a FROM t2); ”語句可重寫為“SELECT * FROM t1 LEFT SEMI JOIN t2 ON t1.a=t2.a”語句。
  • 謂詞下推:謂詞(Predicate),通常為SQL語句中的條件,例如“SELECT * FROM t1 WHERE t1.a=1; ”語句中的“t1.a=1”即為謂詞。等價類(Equivalent-Class)是指等價的屬性、實體等物件的集合,例如“WHERE t1.a=t2.a”語句中,t1.a和t2.a互相等價,組成一個等價類{t1.a,t2.a}。利用等價類推理(又稱作傳遞閉包),我們可以生成新的謂詞條件,從而達到減小資料量和最大化利用索引的目的。如圖2所示,我們舉一個形象的例子來說明謂詞下推的威力。假設有兩個表t1、t2;它們分別包含[1,2,3,…100]共100行資料,那麼查詢語句“SELECT * FROM t1 JOIN t2 ON t1.a=t2.a WHERE t1.a=1”的邏輯計劃在經過查詢重寫前後的對比。

在這裡插入圖片描述
|圖3 查詢重寫前後對比|

查詢重寫的主要工作在最佳化器中實現,除此之外,openGauss還提供了基於規則的rewrite介面,使用者可以透過建立替換規則的方法對邏輯執行計劃進行改寫。例如檢視展開功能即透過rewrite模組中的規則進行替換,而檢視展開的規則是在建立檢視的過程中預設建立的。

1) rewriter原始碼組織

rewriter原始碼目錄為:/src/gausskernel/optimizer/rewrite。rewriter原始碼檔案如表5所示。

表5 rewriter原始碼檔案

rewriteDefine.cpp 定義重寫規則
rewriteHandler.cpp 重寫主模組
rewriteManip.cpp 重寫操作函式
rewriteRemove.cpp 重寫規則移除函式
rewriteRlsPolicy.cpp 重寫行粒度安全策略
rewriteSupport.cpp 重寫輔助函式

2) rewriter主流程

rewriter主流程程式碼如下。

/* rewrite.cpp  /…/ 查詢重寫主函式  /List QueryRewrite(Query* parsetree){… /* 應用所有non-SELECT規則獲取改寫查詢列表  / querylist = RewriteQuery(parsetree, NIL); / 對每個改寫查詢應用RIR規則  / results = NIL; foreach (l, querylist) { Query query = (Query*)lfirst(l); query = fireRIRrules(query, NIL, false); query->queryId = input_query_id; results = lappend(results, query); } /* 從重寫列表確定一個重寫結果 */ origCmdType = parsetree->commandType; foundOriginalQuery = false; lastInstead = NULL; foreach (l, results) {…} … return results;}

4. 查詢最佳化——optimizer

最佳化器(optimizer)的任務是建立最佳執行計劃。一個給定的SQL查詢(以及一個查詢樹)實際上可以以多種不同的方式執行,每種方式都會產生相同的結果集。如果在計算上可行,則查詢最佳化器將檢查這些可能的執行計劃中的每一個,最終選擇預期執行速度最快的執行計劃。

在某些情況下,檢查執行查詢的每種可能方式都會佔用大量時間和記憶體空間,特別是在執行涉及大量連線操作(Join)的查詢時。為了在合理的時間內確定合理的(不一定是最佳的)查詢計劃,當查詢連線數超過閾值時,openGauss使用遺傳查詢最佳化器(genetic query optimizer),透過遺傳演算法來做執行計劃的列舉。

最佳化器的查詢計劃(plan)搜尋過程實際上與稱為路徑(path)的資料結構一起使用,該路徑只是計劃的簡化表示,其中僅包含確定計劃所需的關鍵資訊。確定代價最低的路徑後,將構建完整的計劃樹以傳遞給執行器。這足夠詳細地表示了所需的執行計劃,供執行者執行。在下文中,我們將忽略路徑和計劃之間的區別。

5. 查詢執行——executor

執行器(executor)採用最佳化器建立的計劃,並對其進行遞迴處理以提取所需的行的集合。這本質上是一種需求驅動的流水線執行機制。即每次呼叫一個計劃節點時,它都必須再傳送一行,或者報告已完成傳送所有行。
在這裡插入圖片描述

|圖5 執行計劃樹示例|

如圖4所示的執行計劃樹示例,頂部節點是Merge Join節點。在進行任何合併操作之前,必須獲取2個元組(MergeJoin節點的2個子計劃各返回1個元組)。因此,執行器以遞迴方式呼叫自身以處理其子計劃(如從左子樹的子計劃開始)。Merge Join由於要做歸併操作,因此它要子計劃按序返回元組,從圖4可以看出,它的子計劃是一個Sort節點。Sort的子節點可能是Seq Scan節點,代表對錶的實際讀取。執行SeqScan節點會使執行程式從表中獲取一行並將其返回到呼叫節點。Sort節點將反覆呼叫其子節點以獲得所有要排序的行。當輸入完畢時(如子節點返回NULL而不是新行),Sort運算元對獲取的元組進行排序,它每次返回1個元組,即已排序的第1行。然後不斷排序並向父節點傳遞剩餘的排好序的元組。

Merge Join節點類似地需要獲得其右側子計劃中的第1個元組,看是否可以合併。如果是,它將向其呼叫方返回1個連線行。在下1次呼叫時,或者如果它不能連線當前輸入對,則立即前進到1個表或另1個表的下1行(取決於比較的結果),然後再次檢查是否匹配。最終,1個或另1個子計劃用盡,並且Merge Join節點返回NULL,以指示無法再形成更多的連線行。

複雜的查詢可能涉及多個級別的計劃節點,但是一般方法是相同的:每個節點都會在每次呼叫時計算並返回其下1個輸出行。每個節點還負責執行最佳化器分配給它的任何選擇或投影表示式。

執行器機制用於執行所有4種基本SQL查詢型別:SELECT、INSERT、UPDATE和DELETE。

  • 對於SELECT,頂級執行程式程式碼僅需要將查詢計劃樹返回的每一行傳送給客戶端。
  • 對於INSERT,每個返回的行都插入到為INSERT指定的目標表中。這是在稱為ModifyTable的特殊頂層計劃節點中完成的。(1個簡單的“INSERT … VALUES”命令建立了1個簡單的計劃樹,該樹由單個Result節點組成,該節點僅計算一個結果行,並傳遞給ModifyTable樹節點實現插入)。
  • 對於UPDATE,最佳化器對每個計算的更新行附著所更新的列值,以及原始目標行的TID(元組ID或行ID);此資料被饋送到ModifyTable節點,並使用該資訊來建立新的更新行並標記舊行已刪除。
  • 對於DELETE,計劃實際返回的唯一列是TID,而ModifyTable節點僅使用TID訪問每個目標行並將其標記為已刪除。

執行器的主要處理控制流程如下:

  • 建立查詢描述。
  • 查詢初始化:建立執行器狀態(查詢執行上下文)、執行節點初始化(建立表示式與每個元組上下文、執行表示式初始化)。
  • 查詢執行:執行處理節點(遞迴呼叫查詢上下文、執行表示式,然後釋放記憶體,重複操作)。
  • 查詢完成;執行未完成的表格修改節點。
  • 查詢結束:遞迴釋放資源、釋放查詢及其子節點上下文。
  • 釋放查詢描述。

1) executor原始碼組織

executor原始碼目錄為:/src/gausskernel/runtime/executor。executor原始碼檔案如表7所示。

表7 executor原始碼檔案

execAmi.cpp 各種執行器訪問方法
execClusterResize.cpp 叢集大小調整
execCurrent.cpp 支援WHERE CURRENT OF
execGrouping.cpp 支援分組、雜湊和聚集操作
execJunk.cpp 偽列的支援
execMain.cpp 頂層執行器介面
execMerge.cpp 處理MERGE指令
execParallel.cpp 支援並行執行
execProcnode.cpp 分發函式按節點呼叫相關初始化等函式
execQual.cpp 評估資質和目標列表表示式
execScan.cpp 通用的關係掃描
execTuples.cpp 元組相關的資源管理
execUtils.cpp 多種執行相關工具函式
functions.cpp 執行SQL語言函式
instrument.cpp 計劃執行工具
lightProxy.cpp 輕量級執行代理
node*.cpp 處理*相關節點操作的函式
opfusion.cpp、opfusion_util.cpp、opfusion_scan.cpp 旁路執行器:處理簡單查詢
spi.cpp 伺服器程式設計介面
tqueue.cpp 並行後端之間的元組資訊傳輸
tstoreReceiver.cpp 儲存結果元組

2)executor主流程

executor主流程程式碼為。

/* execMain.cpp */
...
/* 執行器啟動 */
void ExecutorStart(QueryDesc *queryDesc, int eflags)
{
    gstrace_entry(GS_TRC_ID_ExecutorStart);
    if (ExecutorStart_hook) {
        (*ExecutorStart_hook)(queryDesc, eflags);
    } else {
        standard_ExecutorStart(queryDesc, eflags);
    }
    gstrace_exit(GS_TRC_ID_ExecutorStart);
}
/* 執行器執行 */
void ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, long count)
{
...
    /* SQL 自調優: 查詢執行完畢時,基於執行時資訊分析查詢計劃問題 */
    if (u_sess->exec_cxt.need_track_resource && queryDesc != NULL && has_track_operator &&(IS_PGXC_COORDINATOR || IS_SINGLE_NODE)) {
        List *issue_results = PlanAnalyzerOperator(queryDesc, queryDesc->planstate);
        /* 如果查詢問題找到,存在系統檢視 gs_wlm_session_history */
        if (issue_results != NIL) {
            RecordQueryPlanIssues(issue_results);
        }
    }
    /* 查詢動態特徵, 操作歷史統計資訊 */
    if (can_operator_history_statistics) {
        u_sess->instr_cxt.can_record_to_table = true;
        ExplainNodeFinish(queryDesc->planstate, queryDesc->plannedstmt, GetCurrentTimestamp(), false);
       ...
    }
}
/* 執行器完成 */
void ExecutorFinish(QueryDesc *queryDesc)
{
    if (ExecutorFinish_hook) {
        (*ExecutorFinish_hook)(queryDesc);
    } else {
        standard_ExecutorFinish(queryDesc);
    }
}
/* 執行器結束 */
void ExecutorEnd(QueryDesc *queryDesc)
{
    if (ExecutorEnd_hook) {
        (*ExecutorEnd_hook)(queryDesc);
    } else {
        standard_ExecutorEnd(queryDesc);
    }
}


本篇中,openGauss的程式碼結構從資料庫系統通訊管理、SQL引擎兩方面展開了介紹,由於內容較多,其第三方面的內容——儲存引擎將放在下一篇圖文中具體介紹,敬請期待!


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69997967/viewspace-2922926/,如需轉載,請註明出處,否則將追究法律責任。

相關文章