SQLite中的虛擬數(shù)據(jù)庫引擎
如果你想知道SQLite內(nèi)部是如何運(yùn)行的,你需要粗略了解一下VDBE的工作原理,從下圖可以看出,VDBE處于系統(tǒng)運(yùn)行的中部,所以它似乎與大部分的部件都有關(guān)系,即使部分代碼不直接與它交互但還是起到了支持作用的,VDBE確實(shí)是SQLite的核心部分。

這部分將要介紹VDBE是如何工作的,特別是VDBE指令是如何在一起配合完成對(duì)數(shù)據(jù)庫的操作的,下面先用一些簡(jiǎn)單的例子介紹,然后再解決更加復(fù)雜的問題,如果讀完了這篇文章,你就會(huì)對(duì)SQLite如何工作有了很好的理解了。
前言
VDBE是用虛擬機(jī)語言來操作虛擬機(jī)的,每個(gè)程序的目的都是要訪問和更新數(shù)據(jù)庫,那么VDBE要執(zhí)行的虛擬機(jī)語言都是專門為查找、讀取、和修改數(shù)據(jù)庫而做的。
每個(gè)VDBE語言指令都包括一個(gè)操作符和三個(gè)操作數(shù),分別為P1,P2,P3,P1是一個(gè)任意的整數(shù),P2是一個(gè)非負(fù)整數(shù),P3是一個(gè)指向一個(gè)數(shù)據(jù)結(jié)構(gòu)或者是一個(gè)字符串,也可能是NULL,只有少數(shù)VDBE指令用到所有的操作數(shù),很多只用一兩個(gè)而已,還有一大部分根本就不用而是把自己的數(shù)據(jù)存到執(zhí)行棧中。
一個(gè)VDBE程序從第0條指令開始并執(zhí)行后面的一連串指令直到遇到錯(cuò)誤或者執(zhí)行到一個(gè)HALT指令。當(dāng)一個(gè)VDBE完成執(zhí)行任務(wù)后,所有的數(shù)據(jù)游標(biāo)都關(guān)閉了,所有的內(nèi)存都釋放,所有的數(shù)據(jù)都從棧中彈出,所以從來不用擔(dān)心內(nèi)存泄漏和沒有釋放資源的問題。
一、向數(shù)據(jù)庫中插入數(shù)據(jù)
我們開始用只有很少指令的VDEB程序來解決一個(gè)問題,假設(shè)我們已經(jīng)有一個(gè)數(shù)據(jù)表:
CREATE TABLE examp(one text, two int);也就是說我們現(xiàn)在已經(jīng)存在一個(gè)叫examp名字的表和兩列分別叫one和two,現(xiàn)在假設(shè)我們一插入下面的數(shù)據(jù):INSERT INTO examp VALUES(‘Hello, World!’,99);
現(xiàn)在我們就可以看一下這些指令,我們可以先打開SQLite3命令窗口,并且建立好上面的表,并且插入數(shù)據(jù),插入語句這樣寫:
EXPLAIN INSERT INTO examp VALUES('Hello, World!',99);
這時(shí)就會(huì)有下面的指令表產(chǎn)生:

從上面可以看出,對(duì)于一個(gè)簡(jiǎn)單的插入語句它執(zhí)行了12條指令,前三條和后2條都是指令執(zhí)行的開始和結(jié)尾,而真正執(zhí)行的工作都是在中間7條完成的,這里沒有跳轉(zhuǎn),程序從上面一直到下面執(zhí)行,現(xiàn)在一條條的解釋其含義:
0 Transaction 0 0
1 VerifyCookie 0 81
2 Transaction 1 0
指令Transaction是在開始一個(gè)事務(wù),事務(wù)是如果遇到Commit或者Rollback操作符就結(jié)束。P1是指示這個(gè)事務(wù)所在的數(shù)據(jù)庫文件的索引,0表示是主數(shù)據(jù)庫文件,當(dāng)一個(gè)事務(wù)開始時(shí),在數(shù)據(jù)庫文件上要加上寫鎖,當(dāng)一個(gè)事務(wù)在運(yùn)行的時(shí)候其它的進(jìn)程就不能讀或者寫這個(gè)文件了,開始一下事務(wù)也會(huì)產(chǎn)生一個(gè)回滾日志,在數(shù)據(jù)庫文件發(fā)生任何修改之前事務(wù)必須開始。
指令VerifyCookie是檢查數(shù)據(jù)模式的版本來確保它與它在上次讀數(shù)據(jù)庫模式得到的信息是一致的。P1是數(shù)據(jù)庫編號(hào)(0表示主數(shù)據(jù)庫),這樣是為了確保數(shù)據(jù)庫模式?jīng)]有被其它的線程修改。
第二個(gè)Transaction指令是在開始一個(gè)事務(wù)并且對(duì)數(shù)據(jù)庫1產(chǎn)生一個(gè)日志文件,這個(gè)數(shù)據(jù)是用于臨時(shí)表的。
3 Integer 0 0
4 OpenWrite 0 3 examp
指令Integer是將一個(gè)整型值P1(0)放到棧中,這里的0表示將要修改的數(shù)據(jù)庫,如果P3不為NULL,那么它將是用一個(gè)字符串類型來表達(dá)同樣的整數(shù)。現(xiàn)在棧的狀態(tài)為:
(integer) 0
指令OpenWrite是在P1(0)數(shù)據(jù)庫中,對(duì)表examp表打開一個(gè)讀/寫游標(biāo),它的根頁面是P2(在這個(gè)數(shù)據(jù)庫文件中是3),游標(biāo)可以是任意一個(gè)非負(fù)整數(shù),但是VDBE是在一個(gè)數(shù)組中分配游標(biāo)的,這個(gè)數(shù)組的大小為最大游標(biāo)數(shù)加一,所以為了節(jié)省內(nèi)存,最好就是從0位置開始一直加上去。這里的P3是將要被打開的表名,但是這里并沒有使用它,只是為了更好的讀代碼。這條指令會(huì)把數(shù)據(jù)庫編號(hào)0從棧中彈出來,所以現(xiàn)在棧又變成空的了。
5 NewRecno 0 0
指令NewRecno是產(chǎn)生一個(gè)新的整數(shù)記錄來讓游標(biāo)P1指向它,這個(gè)記錄值現(xiàn)在還不被用作關(guān)鍵字,新的記錄被存入棧中,現(xiàn)在棧的狀態(tài)如下:
(integer) new record key
6 String 0 0 Hello, World!
指令String是將操作數(shù)P3放入棧中,現(xiàn)在棧的狀態(tài)為:
(string) "Hello, World!"
(integer) new record key
7 Integer 99 0 99
Integer指令是將操作數(shù)P1放入棧中,現(xiàn)在棧的狀態(tài)為:
(integer) 99
(string) "Hello, World!"
(integer) new record key
8 MakeRecord 2 0
MakeRecord指令是將P1(2個(gè))棧頂數(shù)據(jù)彈出棧,并且將它們轉(zhuǎn)換成二進(jìn)制類型的數(shù)據(jù)來存入數(shù)據(jù)庫方件中,被這條指令處理過的記錄又一次壓入棧中,現(xiàn)在棧的狀態(tài)為:
(string) "Hello, World!",99
(integer) new record key
9 PutIntKey 0 1
指令PutIntKey是從棧彈出兩個(gè)數(shù)據(jù)并且將這兩個(gè)數(shù)據(jù)寫入游標(biāo)P1所指的表中,這個(gè)新的記錄如果已經(jīng)存在則被覆蓋,如果不存在則新創(chuàng)建。這條記錄的值是棧頂記錄,而主鍵則是棧中第二條記錄(注:也就是在SQLite每個(gè)表中的系統(tǒng)主鍵rowid)這條指令會(huì)使棧彈出兩次,因?yàn)椴僮鲾?shù)P2是1,所以行的改變數(shù)為1并且rowid會(huì)存儲(chǔ)到sqlite_last_insert_rowid()函數(shù)的返回值中,如果P2是0,那么行修改數(shù)就不會(huì)改變,這條指令就說明插入操作所做的工作。
10 Close 0 0
指令Close是關(guān)閉一個(gè)先前打開的游標(biāo)P1,如果P1現(xiàn)在處于關(guān)閉狀態(tài)則這條指令無操作。
11 Commit 0 0
指令Commit會(huì)使所有的改變都存儲(chǔ)到數(shù)據(jù)庫中,在下一個(gè)事務(wù)開始之前再不會(huì)產(chǎn)生任何的修改,這條指令也會(huì)刪除日志文件并且釋放了數(shù)據(jù)庫鎖,如果游標(biāo)還是在打開狀態(tài)的話,一個(gè)讀鎖可以繼續(xù)持有。
12 Halt 0 0
指令Halt使得VDBE引擎立即退出,所有打開的游標(biāo)等都自動(dòng)關(guān)閉,操作數(shù)P1是sqlite_exec()接口的返回值,對(duì)于一個(gè)正常的Halt,返回應(yīng)該是SQLITE_OK (0).,如果是出現(xiàn)錯(cuò)誤,就可能會(huì)得到其它的信息,P2操作數(shù)只有出現(xiàn)錯(cuò)誤時(shí)候才會(huì)用到,也有一個(gè)隱含的Halt指令“Halt 0 0 0”在每個(gè)程序的結(jié)尾,這是VDBE在準(zhǔn)備執(zhí)行的時(shí)候附加上去的。
接著上一節(jié)的插入原理,現(xiàn)在來講一下查詢的執(zhí)行原理:
二、簡(jiǎn)單的查詢
到現(xiàn)在為止,已經(jīng)知道VDBE是如何將數(shù)據(jù)寫入數(shù)據(jù)庫中的了,現(xiàn)在下來看看查詢是如何工作的,下面是我們用到的例子:SELECT * FROM examp;
下面就是對(duì)執(zhí)行這條SQL語句產(chǎn)生的指令:

在看這個(gè)問題之前我們還是先看一下SQLite的查詢是如何工作的,這樣我們才知道我們需要完成什么工作,對(duì)于查詢結(jié)果的每一行,SQLite都會(huì)調(diào)用一個(gè)Callback函數(shù):
int Callback(void *pUserData, int nColumn, char *azData[], char *azColumnName[]);
SQLite庫會(huì)給VDBE提供一個(gè)指向回調(diào)函數(shù)的指針和一個(gè)pUserData指針(不管是回調(diào)函數(shù)指針還是pUserData指針,它都是由API函數(shù)sqlite_exec()傳進(jìn)來的)VDBE的工作就是為nColumn、azData[]、azColumnName[]取值,nColumn就是查詢結(jié)果的列數(shù),azColumnName[]數(shù)組中的每個(gè)字符串就是查詢結(jié)果的每一列名,azData[]放的是實(shí)際的數(shù)據(jù)。
0 ColumnName 0 0 one
1 ColumnName 1 0 two
VDBE的查詢程序的前兩條指令是為azColumn設(shè)置值的,ColumnName指令是告訴VDBE應(yīng)該給azColumnName數(shù)組設(shè)置什么值的,每次查詢都是以ColumnName指令開始的,對(duì)查詢結(jié)果的每個(gè)列都會(huì)有這樣的指令。并且在后來的查詢中對(duì)于每一列都會(huì)有相應(yīng)的列指令。
2 Integer 0 0
3 OpenRead 0 3 examp
4 VerifyCookie 0 81
指令2和3打開一個(gè)要訪問的數(shù)據(jù)庫表的游標(biāo),這人工作和在插入數(shù)據(jù)時(shí)候用到的OpenWrite指令是差不多的,除了這回打開是用來讀的而那個(gè)是寫的,指令4象在插入的例子中一樣是用來驗(yàn)證數(shù)據(jù)庫模式的
5 Rewind 0 10
指令Rewind是初始化一個(gè)對(duì)要查詢表的循環(huán)的迭代器,它把游標(biāo)定位到表的第一個(gè)數(shù)據(jù)上,這是Column指令和下一條指令所需要的,下一條指令將會(huì)用這個(gè)游標(biāo)迭代整個(gè)表的,如果這個(gè)表是空的則跳轉(zhuǎn)到P2(10),如果表不是空的,則就跳到下一條指令,從現(xiàn)在開始就到了循環(huán)體部分了。
6 Column 0 0
7 Column 0 1
8 Callback 2 0
上面的指令是整個(gè)循環(huán)體,它對(duì)表中的每一條記錄都是只執(zhí)行一次,上面6、7指令都是將P2操作數(shù)對(duì)應(yīng)的各自的列的數(shù)據(jù)放到棧中,在這個(gè)例子中,第6條指令是將one列中的數(shù)據(jù)放到棧中,而第7條指令是將two列中的數(shù)據(jù)放到棧中,第8條指令是引發(fā)對(duì)回調(diào)函數(shù)的執(zhí)行,這時(shí)它的P1操作數(shù)將會(huì)變成查詢結(jié)果的列數(shù),這條指令會(huì)將P1條記錄出棧并放到azData[]數(shù)組中。
9 Next 0 6
這條指令是在執(zhí)行一個(gè)循環(huán)的分叉部分,從第5條指令到現(xiàn)在是構(gòu)成了整個(gè)循環(huán)的邏輯結(jié)構(gòu),這是個(gè)應(yīng)該值得注意的關(guān)鍵概念,這個(gè)指令是將游標(biāo)前進(jìn)指向下一條記錄,如果前進(jìn)成功,那么立即跳轉(zhuǎn)到這個(gè)循環(huán)開始,如果這個(gè)前進(jìn)不成功,則繼續(xù)執(zhí)行下面的結(jié)束循環(huán)的指令,而不跳回。
10 Close 0 0
11 Halt 0 0
Close指令是在程序的結(jié)尾將指向要查詢表的游標(biāo)關(guān)閉,其實(shí)沒必要在這里執(zhí)行這條指令,因?yàn)樵诔绦蚪Y(jié)束后,VDBE都會(huì)把所有的游標(biāo)自動(dòng)的關(guān)閉,Halt指令是用來關(guān)閉VDBE程序的。
注意到查詢記錄時(shí)沒有用到Transaction和Commit指令,而在插入的時(shí)候用到了,因?yàn)?span lang="EN-US">SELECT是個(gè)讀操作,而不會(huì)改變數(shù)據(jù)庫,所以它不需要事務(wù)。