本文描述了sqlite 2.8所使用的虚拟机。sqlite 3.0和3.1使用的虚拟机概念上
与此相似,但是许多操作码已变化,算法也有些不同。本文可用作sqlite 3.0所
使用的虚拟机背后思想的大致描述,但不能作为虚拟机如何工作的详细参考。
为了解sqlite库的内部工作原理,需要深入理解虚拟数据库引擎,简称为VDBE。
VDBE在处理流程的中部,涉及到库的各个部分。VDBE是sqlite的核心。
本文简要地介绍了VDBE的工作原理,特别讲述了各种VDBE指令如何组合起来做一
些有用的事情。文章使用教程的风格,从简单到复杂,顺着这种方法,我们将分
析sqlite库的大部分模块。阅读完此教程后,你应该对sqlite是如何工作的有一
个很到的理解,从而可以开始研究实际的源代码了。
$1 预备工作
VDBE实现了一个虚拟的计算机,这个计算机上运行虚拟机的语言。每个程序的目
标是查询或者改变数据库。为了这个目标,VDBE的机器语言特别设计成搜索,读
取和修改数据库。
VDBE语言的每条指令包含一个操作码和三个操作数:P1,P2,P3。操作数P1是任
意整数。P2是一个非负整数。P3是一个指向一数据结构的指针或者一个以null结
尾的字符串或者为空。只有几条VDBE指令用到所有的三个操作数。许多指令仅仅
用一两个操作数。大量的指令没有操作数,而是使用执行栈来获取数据和存储结
果。每条指令的细节描述在另一个文档中。
一个VDBE程序从指令0开始执行,继续执行后继的指令直到:
1.碰到严重错误
2.执行Halt指令
3.升高计数器到程序的最后一条指令。
当一个VDBE程序执行完了后,所有打开的数据库指针都被关闭,所有分配的内存
都会被释放,栈中的每个东西都被弹出。因此不需要担心内存泄漏或未回收的资
源问题。
如果你以前搞过汇编编程或任何种类的抽象机器,你应该很熟悉这些细节,下面
让我们开始看些代码。
$2 Insert操作
我们从一个能用少数几条VDBE指令解决的问题开始。假定我们有一个按
下列方法创建的表:
CREATE TABLE examp(one text,two int);
换句话说,我们已有一个名为examp的数据库表,这个表有两列数据,
名字分别为one和two。现在我们插入一条记录到这个表中:
INSERT INTO examp VALUES('Hello,World!',99);
我们可以使用sqlite命令行工具看到VDBE程序,首先使用sqlite在一个空的数据
库中创建表。然后改变sqlite的输出格式:使用.explain命令倒出VDBE程序。最
后,输入上面的INSERT语句,并且前面加上EXPLAIN。EXPLAIN关键字使得sqlite
打印出VDBE程序,但并不执行。如下所示:
$ sqlite test_database_1
sqlite> CREATE TABLE examp(one text,two int);
sqlite> .explain
sqlite> EXPLAIN INSERT INTO examp VALUES('Hello,99);
addr opcode p1 p2 p3
---- ------------ ----- ----- -----------------------------------
0 Transaction 0 0
1 VerifyCookie 0 81
2 Transaction 1 0
3 Integer 0 0
4 OpenWrite 0 3 examp
5 NewRecno 0 0
6 String 0 0 Hello,World!
7 Integer 99 0 99
8 MakeRecord 2 0
9 PutIntKey 0 1
10 Close 0 0
11 Commit 0 0
12 Halt 0 0
从上面的程序可以看出我们简单的插入语句使用了12条指令来实现。最前面的3
条指令和最后的2条指令总是这个样子不会变化,因此实际的工作是使用七条指
令来完成的。这儿没有跳转,因此程序从上往下执行。下面我们详细分析每条指
令:
0 Transaction 0 0
1 VerifyCookie 0 81
2 Transaction 1 0
Transaction指令开始一个事务,Commit或Rollback结束一个事务。P1
是事务在其上开始的数据库文件索引。索引0表示主数据库文件。当一个事务开
始时需要获得写锁,这时其它进程既不能读也不能写这个数据库文件了。一个事
务开始时也创建了一个回滚日志。必须在数据库要改变之前开始一个事务。
VerifyCookie检验数据库模式的版本,使之等于P2,P2是数据库模式最
后一次读中获取的。P1是数据库数字。该指令保证数据库模式没有被另一个线程
所改变,这种情况下就得从新读取数据库模式。
第二条Transaction指令在数据库1上开始了一个事务和一个回滚日志,
数据库1用于临时表。
3 Integer 0 0
4 OpenWrite 0 3 examp
Integer指令把P1的值压入堆栈,这里0表示用来读写的数据库号数。如
果P3不是NULL那么,P3所表示的字符串代表同样的整数值。这条指令执行完后,
栈如下所示:
(integer) 0
OpenWrite指令在P1(这儿为0)上即examp表上打开一个读写指针,
examp表的根页面是P2(这儿为3)。读写指针可以是任何一个非负整数。但是
VDBE使用一个数组来保存指针。为节省内存,最好从0开始,然后向上增长。这
里P3是要打开表的名字,其时这个是没有用的,仅仅是为了增加易读性。该指令
弹出栈中的数据库号数来作为参数,这里是0号数据库,它表示主数据库,因此
指令执行后栈为空。
5 NewRecno 0 0
NewRecno指令为表指针P1创建一个整数记录号。记录号不是表中的键。
新纪录号被压入堆栈。指令执行后栈中如下所示:
(integer) new record key
6 String 0 0 Hello,World!
String指令把它的P3操作数压入栈顶。其后栈如下所示:
(string) "Hello,World!"
(integer) new record key
7 Integer 99 0 99
Integer指令把其P1操作数压入栈顶,其后栈如下所示:
(integer) 99
(string) "Hello,World!"
(integer) new record key
8 MakeRecord 2 0
MakeRecord指令弹出栈顶的P1个元素,这里是2个,然后把他们转化为
数据库文件用来存储记录的二进制格式,MakeRecord新产生的记录压入栈顶。执
行后栈如下所示:
(record) "Hello,World!",99
(integer) new record key
9 PutIntKey 0 1
PutIntKey指令用栈顶的两项组成一个数据行,写入P1所指向的表。若其不存在
则创建一个新的数据行,若存在则覆盖它。记录数据是栈顶数据,键是下面的一
项。这条指令使得栈被弹出两次。P2是行改变计数,这个技术不断增加,使用
sqlite_last_insert_rowid()函数可以返回行id。如果P2是0的话行改变计数将
不变,插入操作在这条指令中实际体现。
10 Close 0 0
Close指令以前打开的P1指针,如果P1没有打开则什么也不做。
11 Commit 0 0
Commit指令使得自从最后的一个Transaction到这儿所有对数据库的修
改生效。知道另一个事务开始,没有别的修改出现。Commit指令删除日志文件,
释放写锁。如果指针还是打开的话,读锁继续保持。
12 Halt 0 0
Halt指令使得VDBE引擎立即退出。所有打开的指针,列表,排序等等都
统统关闭。P1是sqlite_exec()返回的结果代码。对一个正常的退出,这个结果
码应该是sqlITE_OK (0)。若有错误出现,它将是其它值。当有错误出现是回用
到操作数P2。Halt 0 0 0出现在VDBE运行的每一个程序的结尾。