SQLite数据库的VDBE虚拟机(翻译自sqlite.org)

本文描述了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运行的每一个程序的结尾。

相关文章

SQLite架构简单,又有Json计算能力,有时会承担Json文件/RES...
使用Python操作内置数据库SQLite以及MySQL数据库。
破解微信数据库密码,用python导出微信聊天记录
(Unity)SQLite 是一个软件库,实现了自给自足的、无服务器...
安卓开发,利用SQLite实现登陆注册功能