代码改变世界

SQLite虚拟机工作原理(一)

2009-03-04 17:11  竹 石  阅读(1668)  评论(0编辑  收藏  举报

SQLite中的虚拟数据库引擎
如果你想知道SQLite内部是如何运行的,你需要粗略了解一下VDBE的工作原理,从下图可以看出,VDBE处于系统运行的中部,所以它似乎与大部分的部件都有关系,即使部分代码不直接与它交互但还是起到了支持作用的,VDBE确实是SQLite的核心部分。

这部分将要介绍VDBE是如何工作的,特别是VDBE指令是如何在一起配合完成对数据库的操作的,下面先用一些简单的例子介绍,然后再解决更加复杂的问题,如果读完了这篇文章,你就会对SQLite如何工作有了很好的理解了。
前言
VDBE是用虚拟机语言来操作虚拟机的,每个程序的目的都是要访问和更新数据库,那么VDBE要执行的虚拟机语言都是专门为查找、读取、和修改数据库而做的。
每个VDBE语言指令都包括一个操作符和三个操作数,分别为P1,P2,P3,P1是一个任意的整数,P2是一个非负整数,P3是一个指向一个数据结构或者是一个字符串,也可能是NULL,只有少数VDBE指令用到所有的操作数,很多只用一两个而已,还有一大部分根本就不用而是把自己的数据存到执行栈中。
一个VDBE程序从第0条指令开始并执行后面的一连串指令直到遇到错误或者执行到一个HALT指令。当一个VDBE完成执行任务后,所有的数据游标都关闭了,所有的内存都释放,所有的数据都从栈中弹出,所以从来不用担心内存泄漏和没有释放资源的问题。

一、向数据库中插入数据
我们开始用只有很少指令的VDEB程序来解决一个问题,假设我们已经有一个数据表:
CREATE TABLE examp(one text, two int);也就是说我们现在已经存在一个叫examp名字的表和两列分别叫one和two,现在假设我们一插入下面的数据:INSERT INTO examp VALUES(‘Hello, World!’,99);
现在我们就可以看一下这些指令,我们可以先打开SQLite3命令窗口,并且建立好上面的表,并且插入数据,插入语句这样写:
EXPLAIN INSERT INTO examp VALUES('Hello, World!',99);
这时就会有下面的指令表产生:

从上面可以看出,对于一个简单的插入语句它执行了12条指令,前三条和后2条都是指令执行的开始和结尾,而真正执行的工作都是在中间7条完成的,这里没有跳转,程序从上面一直到下面执行,现在一条条的解释其含义:
0 Transaction 0 0
1 VerifyCookie 0 81
2 Transaction 1 0
指令Transaction是在开始一个事务,事务是如果遇到Commit或者Rollback操作符就结束。P1是指示这个事务所在的数据库文件的索引,0表示是主数据库文件,当一个事务开始时,在数据库文件上要加上写锁,当一个事务在运行的时候其它的进程就不能读或者写这个文件了,开始一下事务也会产生一个回滚日志,在数据库文件发生任何修改之前事务必须开始。
指令VerifyCookie是检查数据模式的版本来确保它与它在上次读数据库模式得到的信息是一致的。P1是数据库编号(0表示主数据库),这样是为了确保数据库模式没有被其它的线程修改。
第二个Transaction指令是在开始一个事务并且对数据库1产生一个日志文件,这个数据是用于临时表的。

3 Integer 0 0
4 OpenWrite 0 3 examp
指令Integer是将一个整型值P1(0)放到栈中,这里的0表示将要修改的数据库,如果P3不为NULL,那么它将是用一个字符串类型来表达同样的整数。现在栈的状态为:
(integer) 0

指令OpenWrite是在P1(0)数据库中,对表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个)栈顶数据弹出栈,并且将它们转换成二进制类型的数据来存入数据库方件中,被这条指令处理过的记录又一次压入栈中,现在栈的状态为:
(string) "Hello, World!",99
(integer) new record key


9 PutIntKey 0 1
指令PutIntKey是从栈弹出两个数据并且将这两个数据写入游标P1所指的表中,这个新的记录如果已经存在则被覆盖,如果不存在则新创建。这条记录的值是栈顶记录,而主键则是栈中第二条记录(注:也就是在SQLite每个表中的系统主键rowid)这条指令会使栈弹出两次,因为操作数P2是1,所以行的改变数为1并且rowid会存储到sqlite_last_insert_rowid()函数的返回值中,如果P2是0,那么行修改数就不会改变,这条指令就说明插入操作所做的工作。

10 Close 0 0
指令Close是关闭一个先前打开的游标P1,如果P1现在处于关闭状态则这条指令无操作。

11 Commit 0 0
指令Commit会使所有的改变都存储到数据库中,在下一个事务开始之前再不会产生任何的修改,这条指令也会删除日志文件并且释放了数据库锁,如果游标还是在打开状态的话,一个读锁可以继续持有。

12 Halt 0 0
指令Halt使得VDBE引擎立即退出,所有打开的游标等都自动关闭,操作数P1是sqlite_exec()接口的返回值,对于一个正常的Halt,返回应该是SQLITE_OK (0).,如果是出现错误,就可能会得到其它的信息,P2操作数只有出现错误时候才会用到,也有一个隐含的Halt指令“Halt 0 0 0”在每个程序的结尾,这是VDBE在准备执行的时候附加上去的。