SQLite虚拟数据库引擎(一 )

欢迎转载。转载请注明出处。

文章主体为SQLite文档中的vdbe.html文档。此文介绍了SQLite虚拟机的细节。但此文档并没有及时更新。文档中描述的虚拟机是SQLite老版本的虚拟机。老的虚拟机基于栈。新版本的虚拟机基于寄存器。差别较大。基于本人对寄存器机制的理解,更新了部分文档。如果错误,请指正。


SQLite虚拟数据库引擎

 

如果你想知道SQLite内部是如何工作的,那么你先要对虚拟数据库引擎VDBE(The Virtual Database Engine of SQLite)有深入的了解。 VDBE在处理流程中处在中心位置(参见architecture diagram), 且似乎遍及了SQLite的绝大部分地方。 尽管有些代码没有直接地与VDBE交互,这些代码通常是配角VDBESQLite真正的核心。

这篇文章主要介绍了VDBE是如何工作的,尤其说明了各种VDBE指令(指令文档) 是如何协同工作来操作数据库的。 此教程的风格以简单的任务开始,之后解决复杂的问题。 遵循这一方法,我们将探讨SQLite的大部分子模块。 在完整地学习此教程后,你对SQLite是如何工作的,会有很好的理解。 为开始学习实际的源码做了准备。

前言

VDBE实现了一个虚拟机。此虚拟机运行它自己的虚拟机语言构成的程序。 每一个程序的目标是为了访问或改变数据库。 为了此目标,VDBE实现的虚拟机语言进行了专门的设计,以便来搜索、读取、修改数据库。

虚拟机语言的每一个指令包含了一个操作码(opcode)5个操作数(operand)个操作数名为P1P2P3、P4P5P1,P2,P3都是整数类型。P2常作为跳转的目标地址。P4是一个联合体。即可代表多种数据类型。可为整数类型、字符串类型,double类型,FuncDef类型,Mem*类型(寄存器类型),等等。更多个类型,请看vdbe.h文件。P5是一个无符号char类型,一个字节长。 

虚拟机程序从0号指令开始执行,之后执行后续指令,直到出现以下三种情况,才终止执行。 (1)发生了致性错误。(2)执行Halt指令。(3)步进程序计数器,计数器超过了程序中的最后一条指令。 当虚拟机执行完后,所以打开的数据库cursor都会被关闭,所有的内存会被释放。 所以无需担心任何内存泄漏问题和资源未释放问题。

如果你之前用汇编语言写过程序,或玩过任何一种抽象机(abstract machine),那么SQLite虚拟机的这些细节,你会很熟悉。 所以,让我们看看代码。

SQLite之前使用栈结构实现虚拟机,现在使用寄存器结构实现虚拟机。

插入记录到数据库

我们从一个指令少的程序来开始研究VDBE程序。假设我们有一个SQL表,表结构为:

CREATE TABLE examp(one text, two int);

换句话说,我们有一个数据表,名为"examp"。此表有两列.一列名为"one",一列名为"two"。现在假设我们想向此表插入一条记录。像这样:

INSERT INTO examp VALUES('Hello, World!',99);

我们看一下VDBE程序。SQLite使用VDBE程序实现了INSERT操作。INSERT语句通过sqllite的命令行工具执行。 刚开始时,sqlite启动时,创建了一个新的、空的数据库,之后创建表。下一步,改变 sqlite的输出格式,格式变成VDBE程序dump的形式。要输入".explain"命令才可以。最后, 执行[INSERT]语句,但[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, World!',99);
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     9     0                    00  Start at 9
1     OpenWrite      0     2     0     2              00  root=2 iDb=0; examp
2     NewRowid       0     1     0                    00  r[1]=rowid
3     String8        0     2     0     Hello, World!  00  r[2]='Hello, World!'
4     Integer        99    3     0                    00  r[3]=99
5     MakeRecord     2     2     4     BD             00  r[4]=mkrec(r[2..3])
6     Insert         0     4     1     examp          1b  intkey=r[1] data=r[4]
7     Noop           0     0     0                    00
8     Halt           0     0     0                    00
9     Transaction    0     1     1     0              01  usesStmtJournal=0
10    TableLock      0     2     1     examp          00  iDb=0 root=2 write=1
11    Goto           0     1     0                    00

正如上面我们看到的那样,我们写的简单的插入语句编译成了12个指令。第一条指令和最后三条指令是开始、结束的通用指令,所以实际起作用的是中间的8条指令。 在执行地址0处的指令Init,程序跳转到地址9处的指令。之后执行地址10处的指令。执行地址11处的指令后,跳转到地址1处。现在让我们看看每条指令的细节。

0      Init          0     9     0                                         
9      Transaction   0     1     1     0              01                                      
10     TableLock     0     2     1     examp
11          Goto        0     1     0 

VDBE程序包含了一个OP_Init指令,并总是作为第一条指令。如果P2不为零,跳转到由P2指定的地址处。

Transaction指令开启了一个事务。 当遇到ComitRollback指令时,事件结束。 P1是开始此事务的数据库文件的索引。 索引0是主数据库文件。如果P2不为零,那么开始写-事务,或如果读事务已经被激活,则将它升级成一个写事务。如果P2为零,那么开始一个读事务。P3操作数保存了编译此SQL时的schema版本号。

当开始一个事务时,获得了此数据库文件的写锁。 当此事务正在进行中时,没有其它进程可以读或写此文件。 开始一个事务时,也会创建回滚日志。 事务要在数据库发生任何变化之前开始。

编译此SQL时的schema版本号(me:设为版本号为v1)保存在内部的缓存中。在此指令中,会检查此查询语句被执行时的数据库版本号(me:设此版本号为v2),与此版本号(me:v1)是否相匹配。不匹配则出错。

TableLock指令获得某个表的锁。只有当shared-cache功能开始时,此指令才会被使用。P1是数据库的索引。此索引其实是sqlite3.aDb数据库数组的索引。P2包含了要被锁定的表的根页面。如果P3==0,则获得一个读锁。如果P3==1,则获得一个写锁。

 

Goto指令,是一个无条件的跳转指令,跳转到P2地址上。下一条指令将从地址P2处执行。

 

程序运行时,执行指令的顺序为0: Init--->9:Transaction--->10:TableLock--->11:Goto--->1:OpenWrite。

OpenWrite打开一个新的读/cursor,此cursorIDP1P1指向了一张表或一个索引。Cursor句柄可以是任意非负数。VDBE会在一个数组中分配cursor。数据的大小会比最大的cursor1(But the VDBE allocates cursors in an array with the size of the array being one more than the largest cursor. )为了节省内存,句柄最好从0开始,并连续的向上增长。 此cursor指向了表"examp"

表或索引的根页面为P2

P3==0,意思是主数据库,P3==1意思是此数据库用于临时表。P3>1意思是使用对应的附属数据库。

P4值要么是一个整数(P4_INT32)或一个KeyInfo结构体(P4_KEYINFO)指针。如果P4是一个整数值,P4被设置成此表中列的数量。

 

NewRowid生成一个新的整数记录号(又名rowid),用作表中记录的key。生成的rowid保存在P2指向的寄存器中。在此程序中,rowid保存在1号寄存器中(r[1]=rowid)

String8操作,将P4指向的UTF8字符串,保存在P2指向的寄存器中。在此程序中,字符串保存在2号寄存器中(r[2]='Hello, World!')

Integer指令,要P1中的32位整数写入P2代表的寄存器中(r[3]=99)

MakeRecord指令,从P1开始。转换P2个寄存器,多个寄存器为r[P1,P1+P2]。将这些寄存器写成record format,作为数据库表的数据记录,或索引的keyP3指向了输出结果寄存器。即生成的记录保存在r[P3]中。P4指向了一个字符串。标识了r[P1, P1+P2]中每个寄存器的数据类型。

Insert指令,从P3指定的寄存器中读取rowid,从P2指定的寄存器读取要插入的数据,形成一条记录。将此记录保存到P1代表的游标所指的表中。

Noop指令,什么也不做。

 

Halt指令,立即退出。所有打开的游标,都会被自动释放。每个程序结束的地方都会被插入"Halt 0 0 0"指令。如果开启自动事务功能,若执行过程中无错,则提交事务。若出错,则执行回滚操作。

 

跟踪VDBE程序的执行

如果SQLite库编译时没有使用NDEBUG预处理宏,那么PRAGMAvdbe_trace VDBE跟踪程序的执行。 这个功能最开始意图是测试和debug,它也用来学习VDBE是如何执行的。 使用"PRAGMA vdbe_trace=ON;"来开启跟踪,使用 "PRAGMA vdbe_trace=OFF"来关闭跟踪。像这样: 

sqlite> PRAGMA vdbe_trace=ON;
SQL: [PRAGMA vdbe_trace=ON;]
VDBE Trace:
   0 Init             0    0    0               00 Start at 0
   1 Expire           0    0    0               00
   2 Halt             0    0    0               00
sqlite> INSERT INTO examp VALUES('Hello, World!',99);
SQL: [INSERT INTO examp VALUES('Hello, World!',99);]
VDBE Trace:
   0 Init             0    9    0               00 Start at 9
   9 Transaction      0    1    2 0             01 usesStmtJournal=0
  10 TableLock        0    4    1 examp         00 iDb=0 root=4 write=1
  11 Goto             0    1    0               00
   1 OpenWrite        0    4    0 2             00 root=4 iDb=0; examp
   2 NewRowid         0    1    0               00 r[1]=rowid
REG[1] =  i:1
   3 String8          0    2    0 Hello, World! 00 r[2]='Hello, World!'
REG[2] =   t13[Hello, World!](8)
   4 Integer         99    3    0               00 r[3]=99
REG[3] =  i:99
   5 MakeRecord       2    2    4 BD            00 r[4]=mkrec(r[2..3])
REG[4] =  s17[03270148656C6C6F2C20576F726C6421.'.Hello, World!](8)
   6 Insert           0    4    1 examp         1B intkey=r[1] data=r[4]
REG[4] =  s17[03270148656C6C6F2C20576F726C6421.'.Hello, World!](8)
REG[1] =  i:1
   7 Noop             0    0    0               00
   8 Halt             0    0    0               00

trace模式中,VDBE在执行每一条指令之前都会打印它们。在这些指令执行之前,会显示操作数的内容。在执行之后,会显示所修改的寄存器的内容。如果没有修改寄存器,则不显示。

 

在显示寄存器内容时,会显示一个前缀,来说明它的数据类型。整数(Integer)类型的前缀为"i:"。 浮点类型的前缀为"r:".("r"代表real-number".)字符串类型的前缀为 "s:", "t:", "e:""z:". 这些不同的前缀是因为内存分配方式不同而导致的。z:类型字符串的内存由malloc()分配。 t类型的字符串的内存是静态分配的。e:类型的字符串是临时分配的。所有其它内存分配类型都加上s:前缀。对于使用者来说,这些类型没有什么不一样,但时对于VDBE来说则非常重要。因为当z类型的字符串出栈时,为了 避免内存泄漏,z类型的字符串需要被free()释放。 注意,只有字符串的前10个字符会被显示出来。二进制值(MakeRecord指令的结果)被当作字符串处理。

 

简单查询

你将了解VDBE是如何写数据库的。现在,让我们看一下它是如何查询的。我们将使用下面的简单的SELECT语句作为我们的例子。

sqlite> explain select * from examp;
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     10    0                    00  Start at 10   ????????????????????10
1     OpenRead       0     2     0     2              00  root=2 iDb=0; examp
2     Explain        0     0     0     SCAN TABLE examp  00
3     Rewind         0     8     0                    00
4       Column         0     0     1                    00  r[1]=examp.one
5       Column         0     1     2                    00  r[2]=examp.two
6       ResultRow      1     2     0                    00  output=r[1..2]
7     Next           0     4     0                    01
8     Close          0     0     0                    00
9     Halt           0     0     0                    00
10    Transaction    0     0     1     0              01  usesStmtJournal=0
11    TableLock      0     2     0     examp          00  iDb=0 root=2 write=0
12    Goto           0     1     0                    00

在我们查看此问题之前,让我们对SQLite的查询是如何工作的有个大概的了解。 我们将知道我们尝试完成什么。对于查询结果中的每一行,SQLite将调用callback函数。此函数的原型为:

int Callback(void *pUserData, int nColumn, char **azData, char **azColumnName);

SQLite库为VDBE提供此回调函数和pUserData指针。 (callbackuser data作为参数传递给sqlite_exec()函数。) VDBE的工作是为了填充nColumn, azData, and azColumnName的值。(The job of the VDBE is to come up with values for nColumn, azData[], and azColumnName[])nColumn是结果集中的列数, azColumnName是一个字符串数组。每一个字符串是结果集中一列的名称。 azData是一个字符数组,它保存了实际的数据。

0     Init           0     10    0                    00
10    Transaction    0     0     1     0              01  usesStmtJournal=0
11    TableLock      0     2     0     examp          00  iDb=0 root=2 write=0
12    Goto           0     1     0                    00

此四条指令,是通用的初始化指令。在之前已经介绍过了。这里不再介绍了。

 

1     OpenRead       0     2     0     2              00  root=2 iDb=0; examp

指令1打开一个读cursor。这个cursor指向了要查询的数据库中的表。INSERT例子中的OpenWrite指令也具有同样的功能。 不同之处是,这次是为了读而打开一个cursor

 

2     Explain        0     0     0     SCAN TABLE examp  00

此指令并不影响查询操作。略过。

 

3     Rewind         0     8     0                    00

Rewind指令初始化一个循环,此循环用来迭代遍历"examp"表。此指令使P1重新指向此表中的第一个entry。此循环需要ColumnNext指令,这两个指令使用此游标来遍历此表。 如果此表为空,则跳转到P2(8)指向的指令,此指令(me:Close指令)会跳出循环。如果此表不为空,则运行在地址4处的指令(me:Column指令),此指令为循环体的第一个指令。

4       Column         0     0     1                    00  r[1]=examp.one
5       Column         0     1     2                    00  r[2]=examp.two
6       ResultRow      1     2     0                    00  output=r[1..2]

地址4到地址6形成了循环的主体,此循环对于数据库文件中的每条记录执行一次。 地址45处的Column指令,解析游标P1指向的数据。此数据的格式为数据记录,由MakeRecord指令生成此数据记录。(关于此记录格式的更多信息请看MakeRecord操作码。)  从此记录中抽取第P2列。抽取出的值保存在P3寄存器中。在此例中,第一个Column指令将"one"列的值放入寄存器1中。第二个Column指令将"two"列的值放入寄存器2中。

地址6处的ResultRow指令,将之前由Column指令填充好的多个连续的寄存器,赋给了sqlite3_stmt。即将r(P1)r(P1+P2-1)寄存器赋给sqlite3_stmtr(P1)r(P1+P2-1)寄存器即是查询结果中的一行的内容。ResultRow指令可返回SQLITE_ROW,终止sqlite3_step()

    假设使用sqlite3_exec函数执行SQL查询语句。从sqlite3_step函数返回后,从sqlite3_stmt取出此行数据。并调用用户提供的回调函数,传出此行的列数量,列名称,列内容。

 

7     Next           0     4     0                    01

地址7处的Next指令实现了循环的分支。与地址3处的Rewind指令一起形成了循环逻辑。 这是一个关键的概念,你应该着重关注。 Next指令步进此P1代表的游标,使之批向下一条记录。如果此游标步进成功,那么立即跳转到P24,循环开始的地方)。 如果此游标已指向结尾,那么运行之后的指令,不再执行循环。

8     Close          0     0     0                    00
9     Halt           0     0     0                    00

程序尾部的Close指令关闭了cursor。此cursor指向了表"examp"。调用Close也不是必需的, 因为当此程序结束时,VDBE会自动地关闭所有的cursor。但是为了Rewind指令跳转,我们需要一个指令。Halt指令结束此VDBE程序。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值