這篇文章主要講解了“PostgreSQL的查詢處理過程是什么”,文中的講解內(nèi)容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“PostgreSQL的查詢處理過程是什么”吧!
創(chuàng)新互聯(lián)公司是由多位在大型網(wǎng)絡公司、廣告設(shè)計公司的優(yōu)秀設(shè)計人員和策劃人員組成的一個具有豐富經(jīng)驗的團隊,其中包括網(wǎng)站策劃、網(wǎng)頁美工、網(wǎng)站程序員、網(wǎng)頁設(shè)計師、平面廣告設(shè)計師、網(wǎng)絡營銷人員及形象策劃。承接:成都網(wǎng)站設(shè)計、網(wǎng)站建設(shè)、網(wǎng)站改版、網(wǎng)頁設(shè)計制作、網(wǎng)站建設(shè)與維護、網(wǎng)絡推廣、數(shù)據(jù)庫開發(fā),以高性價比制作企業(yè)網(wǎng)站、行業(yè)門戶平臺等全方位的服務。
數(shù)據(jù)庫查詢處理(Query Processing)是數(shù)據(jù)庫比較核心的技術(shù),也是距離用戶最近的子系統(tǒng)。數(shù)據(jù)庫系統(tǒng)在除了實現(xiàn)事務的隔離界別外,還需要在SQL上做到一定程度的兼容,因為數(shù)據(jù)庫本身就是在做查詢處理,很多的內(nèi)核模塊工作都是為了支持這個功能。
大家在學校學到的可能更多的是關(guān)系代數(shù)(Relational Algebra),它定義了一組在關(guān)系(Relation)上進行操作的操作符。關(guān)系代數(shù)的操作數(shù)是關(guān)系(即,數(shù)據(jù)庫中的二維表),其結(jié)果也是關(guān)系。操作符包含如下幾類:
集合操作符:交,并,差;
過濾/投影;
連接;
別名(alias);
一些擴展的操作符,例如:分組,去重,Aggregate。
除了關(guān)系代數(shù),還有一種描述二維關(guān)系表的操作方法:DataLog(Database Logic)。這種方式相對來說比較強大,關(guān)系代數(shù)的操作符都可以用它來表述,但是有些關(guān)系的操作是關(guān)系代數(shù)表示不了的,只能用DataLog來表述,比如:遞歸查詢。
直接使用關(guān)系代數(shù)對數(shù)據(jù)庫操作比較晦澀,難度比較高,因此,今天的商業(yè)數(shù)據(jù)庫都實現(xiàn)了一種更高級的查詢語言——SQL(Structured Query Language),在表達上更加簡潔易懂,也更容易學習。
實際上,在數(shù)據(jù)庫系統(tǒng)內(nèi)部,SQL語句也是被轉(zhuǎn)化成對應關(guān)系代數(shù)的操作符,然后再進行處理,只是這些工作對最終用戶來說是不可見的。其實,關(guān)系型數(shù)據(jù)庫直接的“本地語言”是關(guān)系代數(shù),SQL語言只是人類與關(guān)系數(shù)據(jù)庫進行交流的“更加便捷的”橋梁。
可能大家有疑問,為何使用SQL作為交流橋梁,而不是用C、Java或者Python作為數(shù)據(jù)庫的查詢語言?
因為一個較短的SQL可以完成千百行C或者Java的工作,特別是在訪問一些層次化的數(shù)據(jù)模型(例如:Oracle的層次查詢,一條語句可以把層次結(jié)構(gòu)輸出出來;PostgreSQL的WITH-RECURSIVE語句也可以完成類似的功能)。
更加重要的是,數(shù)據(jù)庫內(nèi)核在實現(xiàn)SQL查詢的時候,可以對SQL進行特定的優(yōu)化,產(chǎn)生更加有效的訪問方法,這些都是高級語言不太可能具備的功能。
從用戶在客戶端發(fā)送一條SQL語句,經(jīng)過網(wǎng)絡傳輸給PostgreSQL進行處理、執(zhí)行,其流程經(jīng)過如下幾個步驟:
SQL字符串可以認為是一個大的正則式,語法分析來檢查這個大的“正則式”是否match定義好的規(guī)則。
在PostgreSQL中,pg_parse_query是語法分析的入口函數(shù),實際上由scan.l(Flex文件)以及gram.y(Bison文件)完成語法檢查。
scan.l是詞法分析,將輸入SQL分解一個個的Token,輸入到gram.y中進行規(guī)則匹配。gram.y中定義了所有SQL類型的語法規(guī)則以及操作符的優(yōu)先級和結(jié)合律,例如,下段代碼定義了操作符的優(yōu)先級和結(jié)合規(guī)則:
下段代碼定了語法規(guī)則:
語法分析結(jié)束后,以查詢(SELECT)為例,返回的結(jié)構(gòu)體是SelectStmt,它會作為作為語義分析模塊的輸入。SelectStmt保存了SQL語句中的各個語法子部分,例如:from子句,投影列,group子句等,從其定義可以看出更多細節(jié):
parse_analyze()函數(shù)是這一步的入口函數(shù),根據(jù)不同的語句類型調(diào)用transformXXXXStmt()函數(shù)進行分析處理。對于SelectStmt,調(diào)用的transformSelectStmt(),對于DeleteStmt調(diào)用transformDeleteStmt()。在這一步將會:
檢查表是否存在,列是否合法,將表、排序列、投影列等轉(zhuǎn)化為內(nèi)部對象ID;
SQL語義是否正確合法。
比如:Aggregate 函數(shù)不能用在WHERE中。如下查詢:
select 1 from x where max(x2) > 1;
調(diào)整聚集函數(shù)在適當?shù)膶哟沃杏嬎?,如下查詢?/p>
select (select max(x.x2) from y) from x;
max(x.x2)在SQL語義上應該是在最外層查詢中計算,而不是將x.x2傳入到內(nèi)層子查詢,在內(nèi)層子查詢中計算Aggregate函數(shù)max()的值。而對于如下查詢:
select (select max(x.x2+y.x2) from y) from x;
max(x.x2+y.x2)是在內(nèi)層子查詢中被計算,而不是作為外層查詢的Aggregate函數(shù)。
經(jīng)過語義檢查,會將SelectStmt變形為Query結(jié)構(gòu),作為查詢重寫的輸入。Query結(jié)構(gòu)包含的部分與SelectStmt類似,只不過內(nèi)容更加豐富:
保存的都是數(shù)據(jù)庫內(nèi)部的對象信息;
一些flag標記,表明是否包含:Aggregate函數(shù)、窗口函數(shù)、SubLink子查詢等;
確定了表達式所在的Query層次。
之前提到過,數(shù)據(jù)庫內(nèi)核處理SQL時都是轉(zhuǎn)化成關(guān)系代數(shù)相關(guān)的元素,這個在Query結(jié)構(gòu)體中可以看到這點:
例如:
關(guān)系代數(shù)的投影是:targetList;
關(guān)系代數(shù)的過濾/join是:jointree;
關(guān)系代數(shù)的Aggregate是:targetList;
關(guān)系代數(shù)的分組:groupClause;
關(guān)系代數(shù)的sort是:sortClause。
后續(xù)所有的工作都是基于上面的元素進行。
根據(jù)用戶定義的規(guī)則對查詢進行重寫,實際是對Query結(jié)構(gòu)里面的成員進行修改或替換,這些規(guī)則可以使用CREATE RULE創(chuàng)建。如果用戶在查詢對應的表上沒有規(guī)則,此步跳過。
查詢優(yōu)化是比較復雜子系統(tǒng),通常稱這個模塊是“優(yōu)化器”,也用來衡量數(shù)據(jù)庫系統(tǒng)優(yōu)秀的一個方面。在數(shù)據(jù)庫領(lǐng)域另一個復雜的子系統(tǒng)是事務處理,這里也不做展開。
PostgreSQL在這一步的輸入是Query對象,入口函數(shù)是planner(),輸出查詢計劃(Query Plan),查詢計劃是指導查詢?nèi)绾伪粓?zhí)行以及用何種方法執(zhí)行的一種結(jié)構(gòu),通常是樹形結(jié)構(gòu)。
優(yōu)化器做的主要工作就是對Query結(jié)構(gòu)的各個語法部分,選擇較優(yōu)的執(zhí)行算法,輸出較優(yōu)的執(zhí)行計劃。在PostgreSQL中,通常分成如下幾步:
在PostgreSQL內(nèi)部有2類的子查詢:一種在from語句后面稱為SubQuery,另一種在作為表達式的一部分,可以出現(xiàn)在targetList,過濾條件,連接條件中,稱為sub-link。
這兩種都可以統(tǒng)稱為Sub-Select,而優(yōu)化器在這一步會進行Sub-Select Elimination:將子查詢上拉到頂層查詢,消除子查詢。
這樣做可以減少查詢層數(shù),增加上層表的個數(shù),從而增加join順序的搜索空間,有助于找到較優(yōu)的連接順序。以sub-link為例,說明一下這個步驟的工作。對于查詢:
select * from x where x.x2 in (select y.x2 from y);
PostgreSQL在這步可以將IN語句轉(zhuǎn)化成Semi-Join,原來的O(m*n)的查找算法簡化為O(1)HASH-JOIN算法。
這里執(zhí)行計劃并沒有使用Hash Semi-Join,是因為inner plantree用了group hashagg進行了去重,所以原來的Semi-Join可以進一步優(yōu)化為Hash Join,這種優(yōu)化進一步擴大了Join順序搜索空間。
在這一步,會將targetList,過濾條件等列修改為對基表的引用;對表達式里面的SubLink遞歸調(diào)用優(yōu)化器優(yōu)先進行優(yōu)化;計算表達式里面的常量表達式等。
如果內(nèi)核可以確定GROUP BY中的一些屬性集合Y函數(shù)依賴于其他屬性集合X,那么可以刪除GROUP BY中的屬性集合Y。函數(shù)依賴檢查工作由check_functional_grouping完成。這樣可以減少分組計算代價。
將某些OUTER JOIN轉(zhuǎn)化為INNER JOIN。
在這一步完成主要完成:條件的下推,基于連接條件生成等價類,以及通過動態(tài)規(guī)劃選擇較優(yōu)的JOIN順序。從整體來看,JOIN順序的選擇是Condition-Driven,而不是完全的對所有的表進行排列組合求解。例如對于查詢:
select * from r, p, q where r1 = (p1+q1) and r2=q2;
通常我們可能認為r和q在r2=q2的條件進行連接,然后與p在r1 = (p1+q1)上進行連接;但是PostgreSQL內(nèi)核在也會做這樣的嘗試:將p和q進行product join,再與r在條件r1 = (p1+q1) and r2=q2;進行連接,p和q之所以可以連接完全是由r1 = (p1+q1)決定的。
做完Join Plan之后,再針對GROUP BY、Aggregate、ORDER BY、LIMIT等子句進行處理。以GROUP BY為例,在PostgreSQL內(nèi)部,實現(xiàn)GROUP BY的有2個算法:Sort Group By以及 HashAgg Group By,通過函數(shù)cost_group以及cost_agg分別來計算二者代價,選擇較優(yōu)的算法執(zhí)行。
完成這些這些步驟后,調(diào)用set_plan_references()以及SS_finalize_plan()函數(shù)最后處理參數(shù)和變量引用后,就可以輸出最終的查詢計劃(Execution Query Plan)了。查詢計劃由很多節(jié)點組成:投影、掃描、連接、Aggregate、GROUP BY、排序等,從這些名稱也可以看出他們就是關(guān)系代數(shù)的操作符,它們會被傳給查詢執(zhí)行組件進行執(zhí)行。如下查詢計劃示例:
這是查詢處理的最后一步,將優(yōu)化器輸出的執(zhí)行計劃,進行初始化、執(zhí)行。查詢執(zhí)行子系統(tǒng)我們一般稱為執(zhí)行器。執(zhí)行過程有ExecutorStart、ExecutorRun、ExecutorFinish這三個入口函數(shù),分別完成對查詢計劃的初始化,執(zhí)行,以及清理。在這個過程中會訪問數(shù)據(jù)庫的其他子系統(tǒng),如:事務系統(tǒng)、存儲系統(tǒng)、日志系統(tǒng)。
以上就是在PostgreSQL內(nèi)核中對一個查詢處理的整個生命周期,基本可以了解到一個SQL字符串在數(shù)據(jù)庫內(nèi)核中是如何一步步被解析,直到到執(zhí)行的基本過程。
上文中描述的一些方法和理論不僅僅在PostgreSQL數(shù)據(jù)庫有效,也可以推導到其他數(shù)據(jù)庫系統(tǒng)中。
上文講述了數(shù)據(jù)庫內(nèi)核中查詢處理的基本流程,現(xiàn)在我們先展開講述執(zhí)行器算法。
數(shù)據(jù)庫的執(zhí)行器包含了很多個算子的執(zhí)行算法,比較簡單的一種就是SeqScan,就是從按照順序(一般是存儲順序)對表進行掃描。
PostgreSQL頁面存儲與大多數(shù)數(shù)據(jù)庫的類似,包含:頁面頭,ItemId 數(shù)組,以及Item(元組),布局如下:
其中PageHeader包含了頁面LSN,ItemId數(shù)組最后一個元素的頁面偏移(pd_lower),第一條元組在頁面內(nèi)偏移(pd_upper),以及其他字段。
PostgreSQL的順序掃描的入口函數(shù)是SeqNext,每次執(zhí)行這個函數(shù)會返回一條元組,主要工作是由heapgettup:
初始化掃描過程就是設(shè)置HeapScanDesc對象,主要設(shè)置初始掃描的頁面,一般從0號頁面的第一個元組開始,即scan->rs_startblock是0。
在PostgreSQL的掃描過程有一個優(yōu)化,即sync_scan,這個特性允許當前的掃描從表的中間頁面開始掃描,這個頁面是其他掃描進程填寫到共享內(nèi)存,由ss_report_location完成,代表這些頁面剛剛被訪問過,如果當前掃描從這些頁面開始,那么可以直接在內(nèi)存中訪問到,從而減少存儲讀取頁面的IO次數(shù),提升性能。
每次更新表的sync start page時,需要遍歷整個list。為了減少這個list的訪問,每隔SYNC_SCAN_REPORT_INTERVAL個頁面才去更新list,這個數(shù)值是128 * 1024 / BLCKSZ。
按照頁面結(jié)構(gòu)掃描頁面。首先讀取頁面頭(PageHeaderData)的pd_linp成員,這是一個Offset數(shù)組(ItemIdData),記錄了元組在頁面上的偏移(lp_off)。
后續(xù)的主要邏輯是遍歷pd_linp數(shù)組,通過offset+page地址獲取到元組內(nèi)存地址。然后對元組做可見性判斷。邏輯如下:
HeapTupleSatisfiesVisibility進行元組可見性判斷,PostgreSQL是MVCC實現(xiàn)的事務隔離,這個函數(shù)就是MVCC的入口邏輯。
繼續(xù)讀取后續(xù)頁面進行掃描。
所有的掃描狀態(tài)保存在HeapScanDesc,下次掃描的時候,可以從上次的狀態(tài)開始。
感謝各位的閱讀,以上就是“PostgreSQL的查詢處理過程是什么”的內(nèi)容了,經(jīng)過本文的學習后,相信大家對PostgreSQL的查詢處理過程是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!