摘 要: 介紹了PostgreSQL的整體架構,,著重針對其解析器模塊進行底層研究,分析查詢請求在解析器模塊中的查詢處理過程和其內(nèi)部各種函數(shù)的調(diào)用關系,,最后就其解析器模塊的開源代碼做出的剖析以全面理解該模塊的運作,,并通過一個子查詢程序調(diào)試實現(xiàn)了一個基本的查詢請求。
關鍵詞: 數(shù)據(jù)庫,;解析器;查詢處理,;轉換處理
PostgreSQL[1]始自于美國加州大學伯克利分校的數(shù)據(jù)庫研究計劃“Ingres項目”,,最早被命名為Postgres。歷經(jīng)多年的改善和發(fā)展,,于2006年成立了EnterpriseDB公司,,其目的為了讓PostgreSQL能更好更方便地為企業(yè)級用戶服務。現(xiàn)在,,PostgreSQL數(shù)據(jù)庫已經(jīng)成為當今世界上特色最鮮明,、功能最強大、內(nèi)容最豐富和開發(fā)人員最多的開源數(shù)據(jù)庫系統(tǒng)之一,,是一種復雜的關系型數(shù)據(jù)庫管理系統(tǒng)(ORDBMS),。它的很多特性正是當今許多商業(yè)數(shù)據(jù)庫的前身。
PostgreSQL數(shù)據(jù)庫支持多版本并發(fā)控制,,可以支持所有廣為使用的SQL結構,,并且兼容時下最流行的計算機開發(fā)語言,如C,、C++,、Java、Perl等,。與其他主流數(shù)據(jù)庫一樣,,PostgreSQL擁有諸多現(xiàn)代數(shù)據(jù)庫特征,其特征特性涵蓋了SQL-2/SQL-92和SQL-3/SQL-99,。作為一款開源軟件,,PostgreSQL數(shù)據(jù)庫不斷被完善,擁有如下諸多優(yōu)勢:
(1)事務支持更徹底。對擁有海量存儲的數(shù)據(jù)庫,,若一個查詢請求長時間運行無果,,就很可能會導致阻礙表的更新。MySQL對于無事務的MyISAM表所采取的處理方式為“表鎖定”,,而PostgreSQL則根本不存在這類問題,。
(2)完美支持存儲過程。通過存儲過程,,PostgreSQL便能輕松完成商業(yè)邏輯封裝,,極大減少了服務器的運轉負荷。獨有的內(nèi)在機制會自行優(yōu)化設計原存儲過程,,最大限度地避免傳輸大量原始查詢語句,,從而提高運行效率。
(3)支持子查詢和觸發(fā)器,。使用子查詢語句使得PostgreSQL在高效運行的同時,,擁有更高的程序可讀性;而觸發(fā)器則更有利于其商業(yè)邏輯封裝,,可減少應用程序對同一商業(yè)邏輯的重復控制,,從而保證數(shù)據(jù)的完整性。
(4)支持多種特殊數(shù)據(jù)類型和自定義擴展需求,。一般來說,,特殊行業(yè)的數(shù)據(jù)會比商業(yè)數(shù)據(jù)更為復雜多變,而PostgreSQL將多維數(shù)據(jù)的集合體描述成一個對象類型,,并作為屬性存儲在表中,。這樣能保證用戶自定義數(shù)據(jù)類型與原有操作符運算規(guī)則一致,并兼容現(xiàn)有數(shù)據(jù)類型,。
盡管PostgreSQL有著無可比擬的優(yōu)勢,,但仍不可避免地存在缺點。如其穩(wěn)定性和效能上有待提高,;欠缺一些高端數(shù)據(jù)庫管理系統(tǒng)所需要的特性(如聯(lián)機熱備份,、數(shù)據(jù)庫集群等)。
1 PostgreSQL開源數(shù)據(jù)庫的體系架構
就架構技術而言,,PostgreSQL數(shù)據(jù)庫采用了經(jīng)典的客戶端/服務器(Client/Server)模型,,其客戶端進程與服務器端進程一一對應。很多工作經(jīng)由客戶端處理后再提交給服務器,,極大地提高了客戶端的響應速度,,從而充分發(fā)揮客戶端的處理能力。
PostgreSQL查詢語句的執(zhí)行流程如圖1所示,。當出現(xiàn)查詢連接請求時,,主進程(Main)派生一個服務器監(jiān)聽進程(Postmaster),,以等待從TCP/IP端口送入的連接請求??蛻魬枚送ㄟ^調(diào)用庫函數(shù)(Libpq)發(fā)出連接請求,,并把用戶請求反饋給Postmaster,后者派生出后臺服務進程(Postgres),,使其與客戶端進程直接對接(Libpq僅支持一個客戶端進程對接多個后臺服務進程),。隨后,客戶端進程和后臺服務進程不通過Postmaster而直接通信,。Postmaster和Postgres運行在數(shù)據(jù)庫服務器中,,而客戶端應用則可運行在任何機器上。服務器進程之間利用信號標志和共享內(nèi)存進行通信,,確保并發(fā)數(shù)據(jù)訪問過程的數(shù)據(jù)完整性,。一個查詢請求經(jīng)歷的完整過程可分為5個階段:
(1)連接階段:客戶端向服務器發(fā)出查詢請求,通過Postmaster與Postgres建立通信對接,。
(2)解析階段:解析器分析查詢請求,,核對語法無誤后創(chuàng)建一個查詢樹。
(3)重寫階段:在系統(tǒng)目錄(pg_rewrite)中匹配重寫規(guī)則,,重寫系統(tǒng)根據(jù)規(guī)則體進行重寫轉換,,隨后創(chuàng)建解析樹作為結果輸出。
(4)優(yōu)化階段:優(yōu)化器根據(jù)解析樹創(chuàng)建查詢規(guī)劃,。首先確定同一查詢結果的所有查詢路徑,然后計算不同查詢路徑的執(zhí)行成本并選擇最優(yōu)路徑,,最后將其拓展為完整查詢規(guī)劃樹,。
(5)執(zhí)行階段:執(zhí)行器對查詢規(guī)劃樹進行遞歸掃描,按其指定的執(zhí)行方式檢索數(shù)據(jù)記錄,。在掃描關系時,,使用存儲系統(tǒng)執(zhí)行排序和連接操作,計算條件后反饋最終查詢結果[2],。
2 PostgreSQL解析器模塊的內(nèi)部結構
PostgreSQL數(shù)據(jù)庫的解析器模塊由解析器和轉換處理器組成,。解析器又分為詞法分析器和語法分析器兩部分,分別由lex和yacc創(chuàng)建,,定義在scan.l和gram.y中,。解析器生成原始解析樹(即為一種數(shù)據(jù)結構)后,轉換處理器再對其進行修正和增補,,最終生成查尋樹,。
詞法分析器(yylex())的主要功能是做查詢語句詞法檢測,用來識別其中的標識符,、SQL關鍵字等,。它對每個關鍵字或標識符都會生成一個標記符號并將該標記結果反饋至語法分析器。語法分析器(yyparse())主要功能是對查詢語句做語法檢測。它由一套語法規(guī)則和觸發(fā)規(guī)則組成,,當任意一條規(guī)則被觸發(fā)時,,兩者將被執(zhí)行。
所謂語法語義解析,,即將來自庫函數(shù)的SQL查詢請求轉化為解析樹,,供優(yōu)化器/執(zhí)行器或指令集使用。在解析階段,,當以ASCII碼組成的查詢字符串到達解析器后,,首先對其執(zhí)行詞法分析,將其轉換為解析器所能識別的關鍵字,、標識符和常量,;再執(zhí)行語法分析,檢查該查詢字符串的語法的有效性,,并生成命令形式的查詢結構(即解析樹),。若語法正確,解析器將會反饋生成的解析樹,;若語法錯誤,,則返回一個錯誤標志或空值;隨后,,解析樹被分離,、檢查和傳送到指令集中的處理函數(shù),或轉換為結點鏈表供優(yōu)化器/執(zhí)行器處理,。
在解析器階段,,解析器只依據(jù)與SQL語法結構相關的固定規(guī)則創(chuàng)建解析樹。由于自身不會自動查詢?nèi)魏蜗到y(tǒng)目錄,,因此就不能理解查詢語句的詳細語義,。故查詢語句被轉換為解析樹后,轉換處理器將對其做進一步處理,,分析查詢語句所引用的表,、函數(shù)和操作符的語義,然后生成查詢樹(Query Tree),,用來表示解析樹具體信息的數(shù)據(jù)結構,。把原始解析(僅包含查詢語句的原始信息)和語義分析分成兩個過程是因為系統(tǒng)目錄查詢操作只能在一個事務中執(zhí)行,并且無法在接收到查詢字符串后就立即發(fā)起一個事務,。服務器一旦確認正在處理一個查詢操作,,那么就可以發(fā)起一個事務。此時,,轉換處理器才能被順利調(diào)用,。
從數(shù)據(jù)結構來說,,轉換處理器生成的查詢樹與原始解析樹十分類似,但是結構細節(jié)上仍有差異,。例如:在一個解析樹中,,一個FuncCall節(jié)點可能在語句構成上被理解為函數(shù)調(diào)用功能,也可能根據(jù)引用名是普通函數(shù)還是聚合函數(shù)被轉換成FuncExpr節(jié)點或Aggref節(jié)點,。類似地,,查尋樹也可能添加了具體數(shù)據(jù)類型的信息(如相關字段或表達式)。本文以transformFuncCall調(diào)用為例:
(1)對每一個參數(shù)調(diào)用transformExpr,;
(2)調(diào)用ParseFuncOrColumn:
?、僬{(diào)用ParseComplexProjection辨別Column Projection函數(shù):關系型參數(shù)和復合類型參數(shù)。
?、谡{(diào)用func_get_detail(普通函數(shù)):
調(diào)用FuncnameGetCandidates,,在Cache PROCNAME-ARGSNSP中找到符合查詢條件的候選函數(shù)列表FuncCandidateList;
返回無需做參數(shù)類型轉換的項,;
若只有一個參數(shù),,則嘗試對系統(tǒng)未定義的類型轉換型函數(shù)做二進制兼容的強制轉換;
若為一般函數(shù),,則在候選函數(shù)中找最匹配函數(shù)func_match_argtypes,;
嘗試解決存在的沖突調(diào)用func_select_candidate;
返回值,。
?、垡罁?jù)返回值采取相應措施;
?、苌上鄳?jié)點并返回:FuncExpr節(jié)點或Aggref節(jié)點[1],。
3 PostgresSQL解析器模塊的源碼剖析
PostgreSQL開源數(shù)據(jù)庫的源代碼相對路徑為Postgresql\source\src\backend,其代碼整體編譯流程大致如下:PostgresSQL在exec_simple_query函數(shù)中處理與用戶交互的簡單查詢請求,,其查詢操作使用兩種協(xié)議:Simple query protocol和Extended query protocol。Extended query protocol包含Parse,、Bind,、Execute三個步驟,并定義了Prepared Statement和Portal兩種重要數(shù)據(jù)結構,。Prepared Statement用來表示詞法解析,、查詢規(guī)劃等結果,Portal用來表示可以被執(zhí)行或已部分執(zhí)行完畢的語句,。由于本文側重研究解析階段,,故只對Simple query protocol協(xié)議進行具體分析,第三步生成物理查詢規(guī)劃和第四步執(zhí)行物理規(guī)劃由于不是本文的側重點在此不贅述,。
(1)編譯查詢
通過pg_parse_query函數(shù)返回一系列查詢解析樹,,并生成列表文件parsetree_list,。具體工作由raw_parse函數(shù)通過調(diào)用base_yyparse函數(shù)對查詢語句進行詞法分析完成。
(2)生成邏輯規(guī)劃與查詢重寫(QueryRewrite)
通過語義分析將已有解析樹轉化為所需查詢規(guī)劃,,建立最優(yōu)查詢規(guī)劃,。具體由pg_analyze_and_rewrite函數(shù)調(diào)用parse_analyze函數(shù)轉換完成。部分代碼如下:
foreach(parsetree_item,,parsetree_list)
{
Node*parsetree=(Node*)lfirst
(parsetree_item),;
…
}
再執(zhí)行parse_analyze函數(shù)中的query=transformStmt(pstate,parseTree),;最后,,通過調(diào)用語句querytree_list= pg_rewrite_query(query);完成查詢重寫,。pg_rewrite_query函數(shù)通過調(diào)用查詢重寫器的入口函數(shù)QueryRewrite來完成操作,,而查詢重寫的作用就是根據(jù)系統(tǒng)或用戶自定義規(guī)則來重寫規(guī)劃。
PostgreSQL解析器模塊所接受的查詢請求表現(xiàn)形式為查詢字符串,,經(jīng)過解析生成原始解析樹,,并返回列表文件(parsetree_list),其每一個節(jié)點都是一個完整的語法解析樹,。一個原始解析樹由多個節(jié)點構成,,一個節(jié)點表示一個類型(由NodeTag定義)。PostgreSQL解析器模塊內(nèi)部函數(shù)調(diào)用關系如圖2所示,。
解析器模塊有3個主要函數(shù),,分別是:raw_parser()、free_parser()和filtered_base_yylex(),,可以在.\Parser\Parser.c文件中找到其定義,。下面就其主要源碼進行剖析和理解:
(1)raw_parser()
raw_parser(const char *str)
{
Int yyresult;
…
return parsetree,;
}
輸入:為輸入的查詢字符串定義一個指向字符常數(shù)的指針,。
功能:掃描查詢字符串并返回相應值。
返回值:若無語法錯誤,,則建立解析樹(Parser Tree),,并將查詢語句存儲在該解析樹中;否則,,向解析器返回空指針(NIL),。
所調(diào)用函數(shù):pool_memory_create(); scanner_init(),; parser_init(),; base_yyparse()
(2)pg_parse_string_token()
pg_parse_string_token(const char *token)
{
Int ctoken;
…
return base_yylval.str,;
}
輸入:為所給字符串定義一個指向字符常數(shù)的指針,。
功能:獲取所給字符串的值,。
返回值:base_yylval.str;
調(diào)用函數(shù):scanner_init(),; base_yylex(),; scanner_finish()。
(3)filtered_base_yylex()
filtered_base_yylex(void)
{
Int cur_token,;
…
return cur_token,;
}
輸入:無輸入值。
功能:作為解析器的中間件,,對查詢語句執(zhí)行優(yōu)化處理,,過濾部分累贅語句。例如:當查詢語句存在多個空字符時,,該函數(shù)會自動將其合并,。
返回值:無返回值。
調(diào)用函數(shù):base_yylex(),。
(4)free_parser(void)
輸入:無輸入值,。
功能:釋放已生成的語法解析樹所占用的內(nèi)存空間。
返回值:無返回值,。
調(diào)用函數(shù):pool_memory_delete(),。
對查詢語句做轉換處理是為了把生成的解析樹都轉換成根為Query型節(jié)點的查尋樹。轉換處理工作在parse_analyze函數(shù)中進行:首先定義ParseState *pstate函數(shù),,用以記錄轉換處理過程中的狀態(tài)信息,,然后調(diào)用transformStmt函數(shù)進行正式的轉換處理工作,它會根據(jù)解析樹不同類型的根節(jié)點調(diào)用相應的函數(shù),。部分代碼如下:
SelectStmt*n=(SelectStmt*)parseTree,;
if(n->valuesLists)
result=transformValuesClause(pstate,n),;
else if(n->op==SETOP_NONE)
result=transformSelectStmt(pstate,,n);
else
result=transformSetOperationStmt(pstate,,n),;
Select statement分為簡單型和復合型。復合型內(nèi)含集合操作符,,在這種語法樹中,,Select語句都處于葉節(jié)點位置,,而內(nèi)部節(jié)點則表示集合操作符,。簡單型也可分為兩類:一類有VALUES,一類無VALUES,。
把原始解析樹轉換處理到查尋樹的目標就是為了從系統(tǒng)關系表中得到相關信息并據(jù)此進行相關節(jié)點的轉換和信息添加,。得到了查尋樹后,,它將被執(zhí)行重寫、優(yōu)化,、執(zhí)行等操作,,至此完成了一次完整的查詢請求。
4 PostgreSQL子查詢調(diào)試
由于PostgreSQL是個開源數(shù)據(jù)庫,,可以支持多種操作系統(tǒng)的編譯與執(zhí)行,。本文選用以Linux為內(nèi)核的Ubuntu v11.04作為調(diào)試環(huán)境,其內(nèi)核版本為2.6.38-8-generic,。操作系統(tǒng)加載的PostgreSQL版本為v8.4.8,。搭建系統(tǒng)中,所選用的調(diào)試工具為DDD,,其版本號為v3.3.12,。所選用編譯器為gcc,其版本為v4.4.5,,庫版本為GNU libc v2.12,。
執(zhí)行子查詢調(diào)試步驟如下:
首先,在PostgreSQL數(shù)據(jù)庫中建立一張新表,,表名為“film”,,該表中存放電影相關信息字段:
Create table "film"
(
"id" Serial NOT NULL UNIQUE,
"filmname" Varchar(20)NOT NULL,,
"director" Varchar(10)NOT NULL,,
"actor" Varchar(10)NOT NULL,
"production" Varchar(40)NOT NULL,,
"description" Varchar(255),,
UNIQUE(filmname),
PRIMARY KEY("id")
),;
Create index "film_index" on "film" using btree("id",,"filmname");
然后,,再在數(shù)據(jù)庫中建立第二張表,,表名為“cinema”,該表中存放有某一部電影的放映信息:
Create table "cinema"
(
"id" Serial NOT NULL UNIQUE,,
"cinema" Varchar(40)NOT NULL,,
"releasedate" Timestamp Default now(),
UNIQUE (cinema),,
PRIMARY KEY ("id")
),;
Create index "cinema_index" on "cinema" using btree ("id","cinema"),;
在所建的兩張表中,,同時建立了B-tree類型的索引,。B-tree索引主要處理那些按照某種順序存儲的數(shù)據(jù)查詢,它能為索引的每個列(字段)聲明一個操作符表,。PostgreSQL的create index語句一般都默認使用B-tree索引類型,。
最后,再新建一張鏈接表,,表名為“announcement”,。該表通過建立外部關鍵字(foreign key)鏈接生成,主要用來顯示所需要查詢的某部電影的相關信息:
Create table "announcement"
(
"id" Serial NOT NULL UNIQUE,,
"fid" integer NOT NULL Default 0,,
"cid" integer NOT NULL Default 0,
PRIMARY KEY ("id")
),;
Alter table "announcement" add foreign key("fid")references "film"("id")on update restrict on delete restrict,;
Alter table "announcement" add foreign key("cid")references "cinema"("id")on update restrict on delete restrict;
在更改好表“announcement”中的相關字段屬性后,,通過新建視圖表來完成所查詢請求,,并顯示相關結果:
該結果中,字段fid即為表“film”中的id字段,,每一個id字段值對應唯一一部電影,;字段cid即為表“cinema”中的id字段,每一個字段值所對應的電影都被標示著相應的放映信息,。在本次查詢請求的建立表中,,表“cinema”中的字段id=1所對應的cinema=Wanda,即表示所進行的查詢請求就是顯示所有在萬達影城播放的電影信息,。更多具體電影和放映信息在表中略去,,但通過增加字段來完整顯示即可。
PostgresSQL開源數(shù)據(jù)庫目前已被成功應用于包括醫(yī)療,、電子商務等在內(nèi)的廣闊領域中,。經(jīng)過二十多年的快速發(fā)展,PostgreSQL的內(nèi)部結構和工作機制已經(jīng)相當成熟,,直到今天依然在全球眾多開源程序員的努力中不斷地被改進和優(yōu)化,。
本文主要對PostgreSQL開源數(shù)據(jù)庫的解析器做了底層研究并對該模塊的源碼做了一定分析,并通過一個子查詢調(diào)試實驗實現(xiàn)了一個基本的查詢請求,。針對PostgreSQL開源數(shù)據(jù)庫的源碼分析在國內(nèi)尚處起步階段,,而源碼分析對深入理解PostgreSQL開源數(shù)據(jù)庫的開發(fā)思想有著里程碑的意義。
參考文獻
[1] PostgreSQL中文之家. [2010-05].http://www.pgsqldb.org.
[2] The PostgreSQL Global development Group. PostgreSQL 8.4.8 documentation[S/OL].(2010-05-17)[2011-04-02]. http://www.postgresql.org/docs/8.4/static/index.html.
[3] 陳景峰.PostgreSQL實用實例參考[R/OL](2004-5-20). [2011-04-20].
[4] 郭龍江,,李金寶.PostgreSQL分析器的研究[J].黑龍江大學自然科學學報,,2001,18(4):49-52.
[5] DOUGLAS K, DOUGLAS S. PostgreSQL: the comprehensive guide to building,, programming, and administering PostgreSQL databases,, second edition[M]. [S.l.]: Sams Publishing,,2005.
[6] 陳文星,付繼宗.Linux下數(shù)據(jù)庫PostgreSQL分析與應用[J].電腦開發(fā)與應用,,2006,,19(11):56-57.