Small. Fast. Reliable.
Choose any three.

SQLite的虚拟数据库引擎

过时的文档警告: 本文档描述了SQLite 2.8.0版中使用的虚拟机。SQLite 3.0和3.1版中的虚拟机在概念上相似,但现在基于寄存器而不是基于堆栈,每个操作码有五个操作数,而不是三个,并且操作码集与下面显示的不同。有关 当前VDBE操作码集和VDBE如何操作的简要概述,请参见虚拟机说明文档。本文档保留为历史参考。

如果您想了解SQLite库在内部的工作方式,则需要对虚拟数据库引擎或VDBE有扎实的了解。VDBE恰好出现在处理流的中间(请参阅体系结构图),因此它似乎触及了库的大多数部分。甚至没有直接与VDBE交互的部分代码也通常起辅助作用。VDBE确实是SQLite的心脏。

本文简要介绍了VDBE的工作方式,尤其是各种VDBE指令(在此处记录)如何协同工作以对数据库进行有用的操作。样式是教程,从简单的任务开始,逐步解决更复杂的问题。在此过程中,我们将访问SQLite库中的大多数子模块。完成本教程后,您应该对SQLite的工作原理有一个很好的了解,并准备开始研究实际的源代码。

初赛

VDBE实现了一个虚拟计算机,该虚拟计算机以其虚拟机语言运行程序。每个程序的目标是查询或更改数据库。为此,VDBE实现的机器语言专门设计用于搜索,读取和修改数据库。

VDBE语言的每个指令包含一个操作码和三个标记为P1,P2和P3的操作数。操作数P1是任意整数。P2是一个非负整数。P3是指向数据结构或零终止字符串的指针,可能为空。只有很少的VDBE指令使用所有这三个操作数。许多指令仅使用一个或两个操作数。大量指令根本不使用任何操作数,而是取其数据并将其结果存储在执行堆栈中。每个指令的作用及其使用的操作数的详细信息在单独的操作码描述文档中进行了 描述

VDBE程序从指令0开始执行,并继续执行后续指令,直到它(1)遇到致命错误,(2)执行暂停指令或(3)使程序计数器越过程序的最后一条指令为止。VDBE完成执行后,所有打开的数据库游标都将关闭,所有内存将被释放,所有内容将从堆栈中弹出。因此,永远不会担心内存泄漏或未分配资源。

如果您曾经做过任何汇编语言编程或曾经使用过任何种类的抽象机,那么所有这些细节都应该为您所熟悉。因此,让我们直接进入并开始看一些代码。

将记录插入数据库

我们从一个只有几条指令的VDBE程序就可以解决的问题开始。假设我们有一个这样创建的SQL表:

CREATE TABLE望派(一个文本,两个整数);

换句话说,我们有一个名为“ examp”的数据库表,该表具有两列名为“ one”和“ two”的数据。现在假设我们要在该表中插入一条记录。像这样:

插入示例值('Hello,World!',99);

我们可以看到SQLite使用sqlite命令行实用程序实现此INSERT的VDBE程序。首先在一个新的空数据库上启动sqlite,然后创建表。接下来,通过输入“ .explain”命令将sqlite的输出格式更改为旨在与VDBE程序转储一起使用的格式。最后,输入上面显示的[INSERT]语句,但在[INSERT]之前加上特殊关键字[EXPLAIN]。[EXPLAIN]关键字将导致sqlite打印VDBE程序而不是执行它。我们有:

sqlite test_database_1
sqlite> 创建表示例(一个文本,两个int);
sqlite>  .explain
sqlite>解释 插入示例值('Hello,World!',99);
addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- ----------------
0事务0 0                                         
1 VerifyCookie 0 81                                        
2事务1 0                                         
3整数0 0                                         
4 OpenWrite 0 3范例                              
5 NewRecno 0 0                                         
6字符串0 0你好,世界!                      
7整数99 0 99                                 
8 MakeRecord 2 0                                         
9 PutIntKey 0 1                                         
10关闭0 0                                         
11提交0 0                                         
12暂停0 0

正如您在上面看到的,我们的简单插入语句是在12条指令中实现的。前3条指令和后2条指令是标准的序言和结语,因此实际的工作在中间的7条指令中完成。没有跳转,因此程序从上到下执行一次。现在让我们详细查看每条指令。

0交易0 0                                         
1 VerifyCookie 0 81                                        
2交易1 0

指令交易 开始交易。遇到Commit或Rollback操作码时,事务结束。P1是在其上启动事务的数据库文件的索引。索引0是主数据库文件。启动事务时,将在数据库文件上获得写锁定。事务进行期间,没有其他进程可以读取或写入文件。启动事务也会创建回滚日志。必须先启动事务,然后才能对数据库进行任何更改。

指令VerifyCookie会 检查cookie 0(数据库架构版本),以确保它等于P2(上次读取数据库架构时获得的值)。P1是数据库编号(主数据库为0)。这样做是为了确保数据库架构没有被另一个线程更改,在这种情况下,必须重新读取它。

第二个Transaction 指令开始一个事务,并启动数据库1(用于临时表的数据库)的回滚日志。

3整数0 0                                    
4 OpenWrite 0 3示例

指令Integer将整数值P1(0)压入堆栈。0是下面的OpenWrite指令中要使用的数据库的编号。如果P3不为NULL,则它是相同整数的字符串表示形式。之后,堆栈如下所示:

(整数)0

指令OpenWrite在表“ examp”上用句柄P1(在这种情况下为0)打开一个新的读/写游标,该表的根页为P2(在此数据库文件中为3)。游标句柄可以是任何非负整数。但是VDBE在数组中分配游标,该数组的大小比最大游标大一。因此,为了节省内存,最好使用从零开始并连续向上处理的句柄。P3(“示例”)是正在打开的表的名称,但是未使用,仅生成它是为了使代码更易于阅读。该指令从堆栈顶部弹出要使用的数据库编号(0,即主数据库),因此此后堆栈再次为空。

5 NewRecno 0 0

指令NewRecno为光标P1指向的表创建一个新的整数记录号。记录号是当前未在表中用作键的记录号。新的记录号被压入堆栈。之后,堆栈如下所示:

(整数)新记录密钥
6字符串0 0你好,世界!

指令String将其P3操作数压入堆栈。之后,堆栈如下所示:

(字符串)“你好,世界!”
(整数)新记录密钥
7整数99 0 99

指令Integer将其P1操作数(99)压入堆栈。之后,堆栈如下所示:

(整数)99
(字符串)“你好,世界!”
(整数)新记录密钥
8 MakeRecord 2 0

MakeRecord指令将顶部的P1元素弹出堆栈(在这种情况下为2),并将其转换为用于在数据库文件中存储记录的二进制格式。(有关详细信息,请参见文件格式说明。)由MakeRecord指令生成的新记录将推回堆栈。之后,堆栈如下所示:

(记录)“你好,世界!”,99
(整数)新记录密钥
9 PutIntKey 0 1

指令PutIntKey使用前2个堆栈条目将条目写入光标P1指向的表中。如果尚不存在新条目或现有条目的数据被覆盖,则会创建一个新条目。记录数据是顶部堆栈条目,而键是下一个堆栈条目。该指令将堆栈弹出两次。由于操作数P2为1,因此行更改计数增加,并且存储rowid以供sqlite_last_insert_rowid()函数随后返回。如果P2为0,则行更改计数不变。该指令是实际发生插入的地方。

10关闭0 0

关闭”指令关闭先前打开为P1(0,唯一打开的游标)的游标。如果P1当前未打开,则该指令为无操作。

11提交0 0

提交指令使自上次事务处理以来对数据库所做的所有修改实际生效。在启动另一个事务之前,不允许进行其他修改。提交指令删除日志文件并释放数据库上的写锁。如果仍有游标打开,则将继续保持读锁定。

12暂停0 0

停止指令使VDBE引擎立即退出。所有打开的游标,列表,排序等都将自动关闭。P1是sqlite_exec()返回的结果代码。对于正常的停止,该值应为SQLITE_OK(0)。对于错误,它可以是其他一些值。仅当出现错误时才使用操作数P2。每个程序的末尾都有一个隐含的“ Halt 0 0 0”指令,VDBE在准备要运行的程序时会附加该指令。

跟踪VDBE程序执行

如果在没有NDEBUG预处理程序宏的情况下编译SQLite库,则PRAGMA vdbe_trace 会使VDBE跟踪程序的执行。尽管此功能最初是用于测试和调试的,但它对于了解VDBE的运行方式也很有用。使用“ PRAGMA vdbe_trace = ON; ”打开跟踪,使用“ PRAGMA vdbe_trace = OFF ”关闭跟踪。像这样:

sqlite>  PRAGMA vdbe_trace =开;
   0停止0 0
sqlite> 插入到示例值中('Hello,World!',99);
   0事务0 0
   1 VerifyCookie 0 81
   2事务1 0
   3整数0 0
堆栈:i:0
   4 OpenWrite 0 3
   示例5 NewRecno 0 0
堆栈:i:2
   6字符串0 0您好,世界!
堆栈:t [Hello,.World!] i:2
   7整数99 0 99
堆栈:si:99 t [Hello,.World!] i:2
   8 MakeRecord 2 0
堆栈:s [... Hello,.World! .99] i:2
   9 PutIntKey 0 1
  10关闭0 0
  11提交0 0
  12停止0 0

在跟踪模式打开的情况下,VDBE在执行每个指令之前先将其打印出来。执行指令后,将显示堆栈中的前几项。如果堆栈为空,则省略堆栈显示。

在堆栈显示中,大多数条目都显示有前缀,以告诉该堆栈条目的数据类型。整数以“ i: ”开头。浮点值以“ r: ”开头。(“ r”代表“实数”。)字符串以“ s: ”,“ t: ”,“ e: ”或“ z: ”开头。字符串前缀之间的差异是由它们的内存分配方式引起的。z:字符串存储在从malloc()获得的内存中。t:字符串是静态分配的。e:字符串是短暂的。所有其他字符串都带有s:前缀。观察者,这对您没有任何影响,弹出它们时释放free(),以避免内存泄漏。请注意,仅显示字符串值的前10个字符,并且二进制值(例如MakeRecord指令的结果)被视为字符串。可以存储在VDBE堆栈上的唯一其他数据类型是NULL,该数据类型不带前缀显示为简单的“ NULL ”。如果将整数作为整数和字符串放置在堆栈中,则其前缀为“ si: ”。

简单查询

此时,您应该了解VDBE如何写入数据库的基础知识。现在让我们看一下它是如何进行查询的。我们将使用以下简单的SELECT语句作为示例:

选择*从示例;

为此SQL语句生成的VDBE程序如下:

sqlite>解释 选择*从示例;
addr操作码p1 p2 p3                                 
---- ------------ ----- ----- ------------------- --------------------------------
0 ColumnName 0 0一                                
1 ColumnName 1 0二                                
2整数0 0                                         
3 OpenRead 0 3示例                              
4 VerifyCookie 0 81                                        
5快退0 10                                        
6列0 0                                         
7列0 1                                         
8回调2 0                                         
9下一个0 6                                         
10关闭0 0                                         
11暂停0 0

在开始研究此问题之前,让我们简要回顾一下SQLite中查询的工作方式,以便我们了解我们要完成的工作。对于查询结果中的每一行,SQLite将使用以下原型调用回调函数:

int回调(void * pUserData,int nColumn,char * azData [],char * azColumnName []);

SQLite库为VDBE提供了指向回调函数的指针和pUserData指针。(回调和用户数据最初都是作为参数传递给sqlite_exec() API函数的。)VDBE的工作是为nColumnazData []azColumnName []提供值当然,nColumn是结果中的列数。 azColumnName []是一个字符串数组,其中每个字符串都是结果列之一的名称。 azData []是保存实际数据的字符串数组。

0 ColumnName 0 0一                                
1 ColumnName 1 0二

VDBE程序中用于查询的前两个指令涉及为azColumn设置值。所述的ColumnName指令告诉VDBE填补哪些值在用于的每个元素azColumnName [] 数组。每个查询将以一个ColumnName指令开头结果中的每一列,并且稍后在查询中将为每一个对应的列指令进行匹配。

2整数0 0                                         
3 OpenRead 0 3示例                              
4 VerifyCookie 0 81

指令2和3在要查询的数据库表上打开一个读取游标。此功能与INSERT示例中的OpenWrite指令相同,不同之处是这次打开了光标以读取而不是写入。指令4如INSERT示例中所示验证数据库模式。

5倒带0 10

倒带指令初始化一个循环,在“examp”表进行迭代。它将光标P1倒退到其表中的第一个条目。这是“列”和“下一步”指令所必需的,它们使用光标在表中进行迭代。如果该表为空,则跳转到P2(10),这是刚刚经过循环的指令。如果表不为空,请转到以下指令(位于循环主体的开头)6。

6列0 0                                         
7列0 1                                         
8回调2 0

指令6至8构成了循环主体,该循环主体将对数据库文件中的每个记录执行一次。地址6和7的指令分别从第P1个游标获取第P2列,并将其压入堆栈。在此示例中,第一个Column指令将列“ one”的值压入堆栈,第二个Column指令将列“ two”的值压入堆栈。地址8的Callback指令调用callback()函数。回调的P1操作数成为nColumn的值。回调指令从堆栈中弹出P1值,并使用它们填充azData []数组。

9下一个0 6

地址9处的指令实现了循环的分支部分。与地址5的倒带一起形成循环逻辑。这是您应该密切注意的关键概念。在下一步指令游标前进P1到下一个记录。如果光标前进成功,则立即跳转到P2(循环主体的开始6)。如果光标位于末尾,则进入以下指令,从而结束循环。

10关闭0 0                                         
11暂停0 0

程序末尾的Close指令关闭指向表“ examp”的光标。由于程序停止时,VDBE会自动关闭所有游标,因此实际上不必在此处调用“关闭”。但是我们需要一条指令来使“倒带”跳转到,所以我们不妨继续进行下去,并让该指令做一些有用的事情。Halt指令结束VDBE程序。

请注意,此SELECT查询的程序不包含INSERT示例中使用的Transaction和Commit指令。由于SELECT是不会更改数据库的读取操作,因此不需要事务。

稍微复杂一点的查询

上一个示例的关键点是使用Callback指令调用回调函数,以及使用Next指令在数据库文件的所有记录上实现循环。本示例通过演示一个稍微复杂的查询(涉及更多的输出列,其中一些是计算值)和一个WHERE子句来限制这些想法,这些想法实际上是通过回调函数实现的,这些查询包含更多的输出列。考虑以下查询:

选择一,二,一|| 两个AS“都”
从示例
像'H%'的地方

该查询可能有些人为的,但确实可以说明我们的观点。结果将具有名称为“一”,“二”和“两者”的三列。前两列是表中两列的直接副本,而第三结果列是通过将表的第一列和第二列连接而形成的字符串。最后,WHERE子句说,我们只为“ one”列以“ H”开头的结果选择行。这是此查询的VDBE程序的样子:

addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- --------------------------------
0 ColumnName 0 0一个
1 ColumnName 1 0两个
2 ColumnName 2 0都
3整数0 0
4 OpenRead 0 3示例
5 VerifyCookie 0 81
6倒回0 18
7字符串0 0 H%                                      
8列0 0
9功能2 0 ptr(0x7f1ac0)
10 IfNot 1 17
11列0 0
12列0 1
13列0 0
14列0 1
15 Concat 2 0
16回调3 0
17下一个0 7
18关闭0 0
19停止0 0

除了WHERE子句外,此示例的程序结构与前面的示例非常相似,只是带有一个额外的列。现在有3列,而不是以前的2列,并且有3个ColumnName指令。就像前面的示例一样,使用OpenRead指令打开游标。地址6的倒带指令和地址17的下一个指令在表的所有记录上形成循环。最后的Close指令为Rewind指令提供了一些在完成后可以跳转到的指令。所有这些就像在第一个查询演示中一样。

此示例中的Callback指令必须为三个结果列而不是两个结果列生成数据,但与第一个查询中的相同。调用Callback指令时,结果的最左列应为堆栈中的最低列,而最右边的结果列应为堆栈的顶部。我们可以看到堆栈是通过这种方式在地址11到15处建立的。11和12处的Column指令将结果中的前两列的值压入。位于13和14的两个Column指令提取计算第三结果列所需的值,而位于Concat的15的指令将它们连接在一起成为堆栈中的单个条目。

当前示例中唯一真正新颖的是WHERE子句,该子句由地址7到10的指令实现。地址7和8的指令将表和文字中的“ one”列的值压入堆栈字符串“ H%”。地址9处的Function指令从堆栈中弹出这两个值,并将LIKE()函数的结果压回到堆栈中。在如不方便指令弹出堆栈顶部的值,并导致立即跳跃式前进到下一条指令,如果顶部的值是假的(不像文字字符串“ H%”)。有效地执行此跳转将跳过回调,这是WHERE子句的重点。如果比较结果为真,则不执行跳转,并且控制权转至下面的“回调”指令。

注意如何实现LIKE运算符。它是SQLite中用户定义的函数,因此其函数定义的地址在P3中指定。操作数P1是要从堆栈中获取的函数参数的数量。在这种情况下,LIKE()函数采用2个参数。参数以相反的顺序(从右到左)从堆栈中取出,因此要匹配的模式是顶部堆栈元素,下一个元素是要比较的数据。返回值被压入堆栈。

SELECT程序的模板

前两个查询示例说明了每个SELECT程序都将遵循的一种模板。基本上,我们有:

  1. 初始化回调的azColumnName []数组。
  2. 在要查询的表中打开一个游标。
  3. 对于表中的每个记录,请执行以下操作:
    1. 如果WHERE子句的计算结果为FALSE,则跳过随后的步骤并继续进行下一条记录。
    2. 计算结果当前行的所有列。
    3. 调用结果的当前行的回调函数。
  4. 关闭光标。

由于我们考虑了其他复杂性,例如联接,复合选择,使用索引来加快搜索,排序和聚合函数(带或不带GROUP BY和HAVING子句),因此将大大扩展此模板。但是相同的基本思想将继续适用。

UPDATE和DELETE语句

UPDATE和DELETE语句使用与SELECT语句模板非常相似的模板进行编码。当然,主要区别在于结束操作是修改数据库而不是调用回调函数。因为它修改了数据库,所以它也将使用事务。让我们从查看DELETE语句开始:

从示例中删除两个<50的地方;

此DELETE语句将从“示例”表中的“两个”列小于50的所有记录中删除。为此而生成的代码如下:

addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- ----------------
0事务1 0
1事务0 0
2 VerifyCookie 0 178
3整数0 0
4 OpenRead 0 3示例
5倒带0 12
6列0 1
7整数50 0 50
8 Ge 1 11
9 Recno 0 0
10 ListWrite 0 0
11下一个0 6
12关闭0 0
13 ListRewind 0 0
14整数0 0
15 OpenWrite 0 3
16 ListRead 0 20
17不存在0 19
18删除0 1
19转到0 16
20 ListReset 0 0
21关闭0 0
22提交0 0
23暂停0 0

这是程序必须执行的操作。首先,它必须在表“ examp”中找到要删除的所有记录。这是使用循环完成的,与上面的SELECT示例中使用的循环非常相似。找到所有记录后,我们就可以逐一删除它们。请注意,我们无法在找到每条记录后立即将其删除。我们必须先找到所有记录,然后返回并删除它们。这是因为删除操作后,SQLite数据库后端可能会更改扫描顺序。并且,如果扫描顺序在扫描过程中发生更改,则某些记录可能会被多次访问,而其他记录可能根本不会被访问。

因此,DELETE的实现实际上有两个循环。第一个循环(指令5至11)查找要删除的记录,并将其键保存到临时列表中,第二个循环(指令16至19)使用键列表逐个删除记录。

0事务1 0
1事务0 0
2 VerifyCookie 0 178
3整数0 0
4 OpenRead 0 3样本

指令0至4与INSERT示例相同。他们启动主数据库和临时数据库的事务,验证主数据库的数据库架构,并在“示例”表上打开一个读取游标。请注意,游标已打开以供读取,而不是写入。在程序的此阶段,我们将仅扫描表,而不更改表。我们将在指令15中重新打开同一表以供以后编写。

5倒带0 12

与SELECT示例中一样,Rewind 指令将光标后退到表的开头,以备在循环主体中使用。

6列0 1
7整数50 0 50
8 Ge 1 11

WHERE子句由指令6至8实现。where子句的工作是在WHERE条件为false时跳过ListWrite。为此,如果“ two”列(由Column指令提取)大于或等于50,它将跳转到Next指令。

像以前一样,Column指令使用游标P1并将P2列(1,列“ two”)中的数据记录推入堆栈。Integer指令将值50推入堆栈的顶部。经过这两条指令,堆栈看起来像:

(整数)50
(记录)“两个”列的当前记录

运算符比较顶部的堆栈中的两个元件,它们弹出,以及基于所述比较的结果然后分支。如果第二个元素> =顶部元素,则跳转到地址P2(循环末尾的Next指令)。因为P1是true,所以如果任一操作数为NULL(因此结果为NULL),则执行跳转。如果我们不跳,则前进到下一条指令。

9 Recno 0 0
10 ListWrite 0 0

所述Recno中指令压栈的整数,其是前4个字节的关键,由光标指出P1到表中的顺序扫描的当前条目的。所述ListWrite指令写在堆栈的顶部到临时存储列表中的整数和弹出顶部元件。这是此循环的重要工作,存储要删除的记录的键,以便我们可以在第二个循环中将其删除。此ListWrite指令之后,堆栈再次为空。

11下一个0 6
12关闭0 0

Next指令使光标递增以指向光标P0指向的表中的下一个元素,如果成功,则跳转到P2(6,循环主体的开始)。关闭指令关闭光标P1。它不影响临时存储列表,因为它不与游标P1关联。相反,它是一个全局工作列表(可以使用ListPush保存)。

13 ListRewind 0 0

ListRewind指令倒带临时存储列表的开头。这为在第二个循环中使用做好了准备。

14整数0 0
15 OpenWrite 0 3

就像在INSERT示例中一样,我们将数据库编号P1(主数据库为0)压入堆栈,并使用OpenWrite打开表P2(基本页3,“示例”)上的游标P1进行修改。

16 ListRead 0 20
17 NotExists 0 19
18删除0 1
19转到0 16

此循环执行实际的删除。它的组织方式与UPDATE示例中的组织方式不同。ListRead指令的作用与Next在INSERT循环中的作用相同,但是由于失败时它跳转到P2,而成功时Next跳转,所以我们将其放在循环的开始而不是结束。这意味着我们必须在循环的末尾放置一个Goto才能在开始时跳回到循环测试。因此,此循环具有C while(){...}循环的形式,而INSERT示例中的循环具有do {...} while()循环的形式。Delete指令填补了前面示例中回调函数所扮演的角色。

所述ListRead指令读取从临时存储列表中的一个元素,并将其推入堆栈。如果成功,它将继续执行下一条指令。如果由于列表为空而失败,则分支到P2,这是循环之后的指令。之后,堆栈如下所示:

当前记录的(整数)键

注意ListRead和Next指令之间的相似性。两种操作均根据以下规则进行:

将下一个“事物”压入堆栈,然后通过“或”跳至P2,具体取决于是否有下一个“事物”要压入。

Next和ListRead之间的一个区别是他们对“事物”的看法。Next指令的“内容”是数据库文件中的记录。ListRead的“事物”是列表中的整数键。另一个区别是,如果没有下一个“事物”,是跳还是掉。在这种情况下,Next将失败,并且ListRead会跳转。稍后,我们将看到其他使用相同原理运行的循环指令(NextIdx和SortNext)。

所述NotExists指令弹出堆栈顶部元件和使用它作为一个整数键。如果表P1中不存在带有该键的记录,则跳转到P2。如果确实存在一条记录,则转到下一条指令。在这种情况下,P2将我们带到循环末尾的Goto,然后从头跳回到ListRead。可以将其编码为P2为16,在循环开始时使用ListRead,但是生成此代码的SQLite解析器并未进行该优化。

删除确实这个循环的工作; 它从堆栈中弹出一个整数键(由前面的ListRead放置在那里),并删除具有该键的游标P1的记录。因为P2为true,所以行更改计数器增加。

跳转跳回循环的开始。这是循环的结尾。

20 ListReset 0 0
21关闭0 0
22提交0 0
23暂停0 0

该指令块将清理VDBE程序。这些指示中的三个并不是必需的,而是由SQLite解析器从其代码模板生成的,这些代码模板旨在处理更复杂的情况。

ListReset指令清空临时存储列表。当VDBE程序终止时,此列表将自动清空,因此在这种情况下没有必要。关闭指令关闭光标P1。同样,这是由VDBE引擎在完成运行该程序后完成的。提交成功结束当前事务,并使该事务中发生的所有更改都保存到数据库中。最后的暂停也没有必要,因为在准备运行时将其添加到每个VDBE程序中。

UPDATE语句的工作方式与DELETE语句非常相似,不同的是,UPDATE语句不是删除记录,而是将其替换为新的记录。考虑以下示例:

在两个<50的情况下,更新示例集one ='('||一||')'。

此语句不是删除“ two”列小于50的记录,而只是将“ one”列放在括号中。用于实现此语句的VDBE程序如下:

addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- ----------------
0事务1 0                                         
1事务0 0                                         
2 VerifyCookie 0 178                                            
3整数0 0                                         
4 OpenRead 0 3示例                              
5倒带0 12                                        
6列0 1                                         
7整数50 0 50                                 
8 Ge 1 11                                        
9 Recno 0 0                                         
10 ListWrite 0 0                                         
11下一个0 6                                              
12关闭0 0                                         
13整数0 0                                         
14 OpenWrite 0 3                                              
15 ListRewind 0 0                                         
16 ListRead 0 28                                             
17 Dup 0 0                                         
18 NotExists 0 16                                             
19字符串0 0(                                  
20列0 0                                         
21 Concat 2 0                                         
22字符串0 0)                                  
23 Concat 2 0                                         
24列0 1                                         
25 MakeRecord 2 0                                         
26 PutIntKey 0 1                                         
27转到0 16                                             
28 ListReset 0 0                                         
29关闭0 0                                         
30提交0 0                                         
31暂停0 0

该程序与DELETE程序基本相同,不同之处在于第二个循环的主体已被一系列指令(从地址17到26)代替,该指令序列可更新记录而不是删除记录。该指令序列中的大多数应该已经为您所熟悉,但是有一些细微的差别,因此我们将对其进行简要介绍。另请注意,第二个循环之前和之后的某些指令的顺序已更改。这就是SQLite解析器选择使用其他模板输出代码的方式。

当我们进入第二个循环的内部时(在指令17中),堆栈包含一个整数,这是我们要修改的记录的键。我们将需要两次使用此密钥:一次是获取记录的旧值,第二次是写回修改后的记录。因此,第一个指令是Dup,用于在堆栈顶部复制键。Dup指令将复制堆栈中的任何元素,而不仅仅是顶部元素。您可以使用P1操作数指定要复制的元素。当P1为0时,堆栈的顶部被复制。当P1为1时,下一个元素在堆栈复制中向下。依此类推。

复制密钥后,下一条指令NotExists将弹出堆栈一次,并使用弹出的值作为密钥来检查数据库文件中记录的存在。如果没有该键的记录,它将跳回到ListRead以获取另一个键。

指令19至25构造了一个新的数据库记录,该记录将用于替换现有记录。这与我们在INSERT描述中看到的相同类型的代码,将不再赘述。指令25执行后,堆栈如下所示:

(记录)新数据记录
(整数)键

PutIntKey指令(在有关INSERT的讨论中也进行了描述)将一个条目写入数据库文件,该文件的数据是堆栈的顶部,其键是堆栈的下一个,然后将堆栈弹出两次。PutIntKey指令将使用相同的键覆盖现有记录的数据,这就是我们想要的。覆盖不是INSERT的问题,因为使用INSERT时,密钥是由NewRecno指令生成的,该指令可确保提供以前未使用过的密钥。

创建并删除

至少从VDBE的角度来看,使用CREATE或DROP创建或销毁表或索引实际上与从特殊的“ sqlite_master”表进行INSERT或DELETE相同。sqlite_master表是为每个SQLite数据库自动创建的特殊表。看起来像这样:

创建表sqlite_master(
  输入TEXT,即“表格”或“索引”
  名称TEXT,-此表或索引的名称
  tbl_name TEXT,-对于索引:关联表的名称
  sql TEXT-原始CREATE语句的SQL文本
)

SQLite数据库中的每个表(“ sqlite_master”表本身除外)和每个命名索引在sqlite_master表中都有一个条目。您可以像其他任何表一样使用SELECT语句查询该表。但是不允许您使用UPDATE,INSERT或DELETE直接更改表。使用CREATE和DROP命令必须对sqlite_master进行更改,因为添加或销毁表和索引时,SQLite还必须更新其某些内部数据结构。

但是从VDBE的角度来看,CREATE的工作原理很像INSERT,而DROP的工作原理就像DELETE。当SQLite库打开到现有数据库时,它要做的第一件事是SELECT,以从sqlite_master表的所有条目中读取“ sql”列。“ sql”列包含最初生成索引或表的CREATE语句的完整SQL文本。该文本被反馈到SQLite解析器中,并用于重建描述索引或表的内部数据结构。

使用索引来加快搜索速度

在上面的示例查询中,必须将要查询的表的每一行都从磁盘中装入并进行检查,即使只有很小比例的行最终出现在结果中。在一张大桌子上这可能需要很长时间。为了加快速度,SQLite可以使用索引。

SQLite文件将密钥与某些数据相关联。对于SQLite表,已设置数据库文件,以使键为整数,而数据为表的一行信息。SQLite中的索引会逆转这种安排。索引键是(某些)要存储的信息,索引数据是整数。要访问具有某些特定内容的表行,我们首先在索引表中查找内容以找到其整数索引,然后使用该整数在表中查找完整记录。

请注意,SQLite使用b树,这是一种排序的数据结构,因此,当SELECT语句的WHERE子句包含相等性或不相等性测试时,可以使用索引。如果有以下查询,则可以使用索引(如果有):

选择*从示例,其中两个== 50;
选择*从两个小于50的示例中;
选择*从示例中两个输入(50,100);

如果存在一个将“ examp”表的“ two”列映射为整数的索引,则SQLite将使用该索引查找exap中所有列(第二列)或所有行的值为50的整数键小于50,依此类推。但是以下查询不能使用索引:

选择*从示例中选择两个%50 == 10;
选择*从示例中,两个&127 == 3;

请注意,即使有可能,SQLite解析器也不会始终生成使用索引的代码。以下查询当前将不使用索引:

选择*从示例中,两个+10 == 50;
SELECT * FROM例如,在两个== 50或两个== 100的情况下;

为了更好地理解索引的工作方式,让我们先来看一下索引的创建方式。让我们继续进行操作,并将一个索引放置在examp表的两列上。我们有:

CREATE INDEX EXEKP_IDX1 ON EXAMP(两个);

上面的语句生成的VDBE代码如下所示:

addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- ----------------
0事务1 0                                         
1事务0 0                                         
2 VerifyCookie 0 178                                            
3整数0 0                                         
4 OpenWrite 0 2                                         
5 NewRecno 0 0                                         
6字符串0 0索引                              
7字符串0 0exep_idx1                         
8字符串0 0示例                              
9 创建索引0 0 ptr(0x791380)
10重复                      0 0                                         
11整数0 0                                         
12 OpenWrite 1 0                                         
13字符串0 0创建索引EXAMP p_idx1 ON示例(tw
14 MakeRecord 5 0                                         
15 PutIntKey 0 0                                         
16整数0 0                                         
17 OpenRead 2 3示例                              
18倒带2 24                                             
19记录2 0                                         
20列2 1                                         
21 MakeIdxKey 1 0 n                                  
22 IdxPut 1 0索引列不是唯一     
23下一个2 19                                             
24关闭2 0                                         
25关闭1 0                                         
26整数333 0                                         
27 SetCookie 0 0                                         
28关闭0 0                                         
29提交0 0                                         
30暂停0 0

请记住,每个表(sqlite_master除外)和每个命名索引在sqlite_master表中都有一个条目。由于我们正在创建新索引,因此必须向sqlite_master添加一个新条目。这是由指令3到15处理的。将一个条目添加到sqlite_master的工作原理与其他任何INSERT语句一样,因此在此不再赘述。在此示例中,我们要集中精力用有效数据填充新索引,这发生在指令16至23上。

16整数0 0                                         
17 OpenRead 2 3示例

发生的第一件事是我们打开要索引的表以进行读取。为了构造表的索引,我们必须知道该表中的内容。索引3和4已经打开了用于使用游标0进行写入的索引。

18快退2 24                                             
19记录2 0                                         
20列2 1                                         
21 MakeIdxKey 1 0 n                                  
22 IdxPut 1 0索引列不是唯一     
23下2 19

指令18到23在被索引表的每一行上实现了循环。对于每个表格行,我们首先在指令19中使用Recno提取该行的整数键,然后在指令20中使用Column获取“ two”列的值。MakeIdxKey指令在21处将数据转换为“ two”列中的数据(位于堆栈顶部)插入有效的索引键。对于单列索引,这基本上是无操作的。但是,如果对MakeIdxKey的P1操作数大于一个,则会从堆栈中弹出多个条目,并将其转换为单个索引键。该IdxPut实际创建索引条目的是22处的指令。IdxPut从堆栈中弹出两个元素。堆栈的顶部用作从索引表中获取条目的键。然后,将在堆栈上排第二的整数添加到该索引的整数集中,并将新记录写回到数据库文件中。请注意,如果有两个或两个以上表条目的两列具有相同的值,则同一索引条目可以存储多个整数。

现在让我们看看如何使用该索引。考虑以下查询:

选择*从示例,其中两个== 50;

SQLite生成以下VDBE代码来处理此查询:

addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- --------------------------------
0 ColumnName 0 0一个                                
1 ColumnName 1 0两个                                
2整数0 0                                         
3 OpenRead 0 3                               例题4 VerifyCookie
0 256                                            
5整数0 0                                         
6 OpenRead 1 4exmise_idx1                         
7整数50 0 50                            
8 MakeKey 1 0 n                                  
9 MemStore 0 0                                         
10 MoveTo 1 19                                             
11 MemLoad 0 0                                         
12 IdxGT 1 19                                             
13 IdxRecno 1 0                                         
14 MoveTo 0 0                                         
15列0 0                                         
16列0 1                                         
17回调2 0                                         
18下一页1 11                                        
19关闭0 0                                         
20关闭1 0                                         
21暂停0 0

SELECT以熟悉的方式开始。首先,对列名进行初始化,然后打开要查询的表。从指令5和6开始的情况变得不同,在指令5和6中也打开了索引文件。指令7和8生成一个值为50的键。9处的MemStore指令将索引键存储在VDBE存储器位置0中。VDBE存储器用于避免从堆栈深处获取值,可以做到这一点。 ,但使程序难以生成。以下地址10处的MoveTo指令 将键从堆栈中弹出,并使用该键将索引光标移动到索引的第一行。这将初始化游标,以在以下循环中使用。

指令11到18通过在指令8中获取的键在所有索引记录上实现了一个循环。所有使用此键的索引记录在索引表中都是连续的,因此我们遍历它们并从中获取相应的表键。指数。然后使用该表键将光标移动到表中的该行。循环的其余部分与非索引SELECT查询的循环相同。

循环从 11处的MemLoad指令开始,该指令将索引键的副本推回堆栈中。位于12的指令IdxGT将键与光标P1指向的当前索引记录中的键进行比较。如果当前光标位置的索引键大于我们要查找的索引,则跳出循环。

位于13的指令IdxRecno将来自索引的表记录号压入堆栈。下面的MoveTo将其弹出,并将表格光标移动到该行。接下来的3条指令以与未索引情况相同的方式选择列数据。列指令获取列数据,并调用回调函数。最后一条Next指令将索引游标(而不是表游标)前进到下一行,然后在剩余索引记录的情况下分支回到循环的开始。

由于索引用于查找表中的值,因此使索引和表保持一致很重要。现在,examp表上有一个索引,每当在exampp表中插入,删除或更改数据时,我们都必须更新该索引。记住上面的第一个示例,我们能够使用12条VDBE指令在“示例”表中插入新行。现在该表已建立索引,需要19条指令。SQL语句是这样的:

插入示例值('Hello,World!',99);

生成的代码如下所示:

addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- ----------------
0事务1 0                                         
1事务0 0                                         
2 VerifyCookie 0 256                                            
3整数0 0                                         
4 OpenWrite 0 3示例                              
5整数0 0                                         
6 OpenWrite 1 4exep_idx1                         
7 NewRecno 0 0                                         
8字符串0 0你好,世界!                      
9整数99 0 99                                 
10重复2 1                                         
11
重复1 1                                          12 MakeIdxKey 1 0 n                                  
13 IdxPut 1 0                                         
14 MakeRecord 2 0                                         
15 PutIntKey 0 1                                         
16关闭0 0                                         
17关闭1 0                                         
18提交0 0                                         
19停止0 0

此时,您应该对VDBE有足够的了解,可以自己弄清楚上述程序的工作方式。因此,在本文中我们将不再进一步讨论。

加盟

在联接中,两个或多个表被组合以生成单个结果。结果表包含要连接的表中所有可能的行组合。实现此目的最简单,最自然的方法是使用嵌套循环。

回想一下上面讨论的查询模板,其中有一个循环搜索表的每个记录。在一个连接中,除了嵌套循环外,我们基本上具有相同的东西。例如,要连接两个表,查询模板可能看起来像这样:

  1. 初始化回调的azColumnName []数组。
  2. 打开两个游标,对每个要查询的两个表之一。
  3. 对于第一个表中的每个记录,请执行以下操作:
    1. 对于第二个表中的每个记录,请执行以下操作:
      1. 如果WHERE子句的计算结果为FALSE,则跳过随后的步骤并继续进行下一条记录。
      2. 计算结果当前行的所有列。
      3. 调用结果的当前行的回调函数。
  4. 关闭两个光标。

该模板可以工作,但是由于我们现在正在处理O(N 2)循环,因此它可能会很慢。但是通常可以得出结论,可以将WHERE子句分解为术语,并且其中一个或多个这些术语将仅涉及第一个表中的列。发生这种情况时,我们可以将WHERE子句测试的一部分从内部循环中分解出来,并获得很多效率。因此,更好的模板如下所示:

  1. 初始化回调的azColumnName []数组。
  2. 打开两个游标,对每个要查询的两个表之一。
  3. 对于第一个表中的每个记录,请执行以下操作:
    1. 评估仅涉及第一个表中列的WHERE子句的术语。如果任何一项为假(意味着整个WHERE子句必须为假),则跳过此循环的其余部分,继续进行下一条记录。
    2. 对于第二个表中的每个记录,请执行以下操作:
      1. 如果WHERE子句的计算结果为FALSE,则跳过随后的步骤并继续进行下一条记录。
      2. 计算结果当前行的所有列。
      3. 调用结果的当前行的回调函数。
  4. 关闭两个光标。

如果可以使用索引来加快两个循环中任一循环的搜索速度,则可能会提高速度。

SQLite始终以与表出现在SELECT语句的FROM子句中相同的顺序构造循环。最左边的表成为外部循环,最右边的表成为内部循环。从理论上讲,在某些情况下可以对循环进行重新排序,以加快连接的评估速度。但是SQLite不会尝试这种优化。

您可以在以下示例中看到SQLite如何构造嵌套循环:

创建表examp2(三个整数,四个整数);
选择*从exmapp,exmap2中,两个<50并且四个==两个;
addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- ----------------
0列名0 0例题                          
1列名1 0例题                          
2 2列名2 0例题2.three                       
3列名3 0例题2.four                        
4整数0 0                                         
5 OpenRead 0 3示例                              
6 VerifyCookie 0 909                                            
7整数0 0                                         
8 OpenRead 1 5EAMP2                             
2倒带0 24                                             
10列0 1                                         
11整数50 0 50                                 
12 Ge 1 23                                             
13倒带1 23                                             
14列1 1                                         
15列0 1                                         
16 Ne 1 22                                        
17列0 0                                         
18列0 1                                         
19列1 0                                         
20列1 1                                         
21回调4 0                                         
22下一个1 14                                             
23下一个0 10                                        
24关闭0 0                                         
25关闭1 0                                         
26暂停0 0

表examp的外循环由指令7至23实现。内循环由指令13至22实现。请注意,WHERE表达式的“ two <50”术语仅涉及第一张表中的列,并且可以从其中删除。内循环。SQLite这样做并在指令10到12中实现“ two <50”测试。“ four == two”测试由内部循环中的指令14到16实现。

SQLite不会对联接中的表强加任何限制。它还允许表与其自身联接。

ORDER BY子句

出于历史原因和效率考虑,所有排序当前都在内存中完成。

SQLite使用一组特殊的指令来实现ORDER BY子句,以控制称为排序器的对象。在查询的最内层循环中,通常会有一个Callback指令,而是构造了一个包含回调参数和键的记录。该记录被添加到排序器中(在链接列表中)。查询循环完成后,将对记录列表进行排序,然后遍历该列表。对于列表中的每个记录,都会调用回调。最后,关闭分类器,并释放内存。

我们可以在以下查询中看到正在运行的过程:

选择*从示例顺序按一个DESC,两个;
addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- --------------------------------
0 ColumnName 0 0一个                                
1 ColumnName 1 0两个                                
2整数0 0                                         
3 OpenRead 0 3示例                              
4 VerifyCookie 0 909                                            
5倒带0 14                                             
6列0 0                                         
7列0 1个                                         
8 SortMakeRec 2 0                                              
9列0 0                                         
10列0 1                                         
11 SortMakeKey 2 0 D +                                 
12 SortPut 0 0                                              
13下一个0 6                                              
14关闭0 0                                              
15排序0 0                                              
16 SortNext 0 19                                             
17 SortCallback 2 0                                              
18转到0 16                                             
19 SortReset 0 0                                         
20暂停0 0

只有一个分类器对象,因此没有打开或关闭该对象的指令。它在需要时会自动打开,而在VDBE程序停止时会关闭。

查询循环是从指令5到指令13建立的。指令6到指令8建立了一条记录,其中包含用于一次调用回调的azData []值。排序键由指令9到11生成。指令12将调用记录和排序键组合到单个条目中,并将该条目放入排序列表中。

指令11的P3自变量特别受关注。排序键是通过在P3的每个字符串前面添加一个字符并串联所有字符串而形成的。排序比较功能将查看此字符,以确定排序顺序是升序还是降序,以及是否按字符串或数字进行排序。在此示例中,第一列应按降序排序为字符串,因此其前缀为“ D”,第二列应按升序进行数字排序,因此其前缀为“ +”。升序字符串排序使用“ A”,而降序数字排序使用“-”。

查询循环结束后,要查询的表在指令14处关闭。这是尽早完成的操作,以便允许其他进程或线程访问该表(如果需要)。在查询循环内建立的记录列表按15处的指令排序。指令16至18遍历记录列表(现在按排序顺序)并为每个记录调用一次回调。最后,在指令19处关闭分类器。

聚合函数以及GROUP BY和HAVING子句

为了计算聚合函数,VDBE实施了特殊的数据结构和用于控制该数据结构的指令。数据结构是一组无序的存储桶,其中每个存储桶都有一个密钥和一个或多个存储位置。在查询循环中,GROUP BY子句用于构造键,并且具有该键的存储桶成为焦点。如果先前不存在,则使用密钥创建一个新存储桶。一旦对存储区进行了聚焦,就可以使用存储区的存储位置来累加各种聚合函数的值。查询循环终止后,每个存储桶都会被访问一次,以生成一行结果。

一个例子将有助于阐明这个概念。考虑以下查询:

选择三,最小(三+四)+平均(四) 
从EXAMP2
GROUP BY 3;

为该查询生成的VDBE代码如下:

addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- --------------------------------
0 ColumnName 0 0三                              
1 ColumnName 1 0 min(三+四)+ avg(四)          
2 AggReset 0 3                                              
3 AggInit 0 1 ptr(0x7903a0)                      
4 AggInit 0 2 ptr(0x790700)                      
5整数0 0                                         
6 OpenRead 0 5示例2
7                                             VerifyCookie 0909                             
8倒带0 23                                             
9列0 0                                         
10 MakeKey 1 0 n                                  
11 AggFocus 0 14                                             
12列0 0                                         
13 AggSet 0 0                                         
14列0 0                                         
15列0 1                                         
16添加0 0                                         
17整数1 0                                         
18 AggFunc 0 1 ptr(0x7903a0)                      
19列0 1                                         
20整数2 0                                         
21 AggFunc 0 1 ptr(0x790700)                      
22下一个0 9                                              
23关闭0 0                                              
24 AggNext 0 31                                        
25 AggGet 0 0                                              
26 AggGet 0 1                                              
27 AggGet 0 2                                         
28添加0 0                                         
29回调2 0                                         
30转到0 24                                             
31 Noop 0 0                                         
32暂停0 0

感兴趣的第一指令是 AggReset在2. AggReset指令初始化集桶的是空集,并指定每个桶中为P2可用存储器时隙的数目。在此示例中,每个存储桶将容纳3个内存插槽。这不是很明显,但是如果您仔细查看程序的其余部分,则可以找出这些插槽中的每个插槽的用途。

内存插槽该内存插槽的预期用途
0“三”列-桶的关键
1个最小“三+四”值
2个所有“四个”值的总和。这用于计算“ avg(four)”。

查询循环由指令8至22实现。GROUP BY子句指定的聚合键由指令9和10计算。指令11使适当的存储桶成为焦点。如果具有给定密钥的存储桶尚不存在,则创建一个新存储桶,并且控制权转至初始化该存储桶的指令12和13。如果存储桶已经存在,则跳转到指令14。聚集函数的值由11和21之间的指令更新。指令14至18更新内存插槽1,以保存下一个值“ min(3 + 4 )”。然后,通过指令19到21更新“四”列的总和。

查询循环完成后,在指令23处关闭表“ examp2”,以便释放其锁,并由其他线程或进程使用。下一步是遍历所有聚合存储桶,并为每个存储桶输出结果的一行。这是通过循环在指令24到30处完成的。在24处的AggNext指令使下一个存储桶成为焦点,如果已经检查了所有存储桶,则跳转到循环结束。在指令25到27中按顺序从聚合器存储区中提取结果的3列。最后,在指令29中调用回调。

总而言之,任何具有聚合函数的查询都是通过两个循环实现的。第一个循环扫描输入表并将汇总信息计算到存储桶中,第二个循环扫描所有存储桶以计算最终结果。

聚合查询实际上是两个连续的循环的认识使我们更容易理解SQL查询语句中WHERE子句和HAVING子句之间的区别。WHERE子句是对第一个循环的限制,而HAVING子句是对第二个循环的限制。您可以通过在示例查询中添加WHERE和HAVING子句来查看此信息:

选择三,最小(三+四)+平均(四) 
从EXAMP2
在三个>四个
按三组
平均(四)<10;
addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- --------------------------------
0 ColumnName 0 0三                              
1 ColumnName 1 0 min(三+四)+ avg(四)          
2 AggReset 0 3                                              
3 AggInit 0 1 ptr(0x7903a0)                      
4 AggInit 0 2 ptr(0x790700)                      
5整数0 0                                         
6 OpenRead 0 5示例2
7                                             VerifyCookie 0909                             
8倒带0 26                                             
9列0 0                                         
10列0 1                                         
11 Le 1 25                                             
12列0 0                                         
13 MakeKey 1 0 n                                  
14 AggFocus 0 17                                             
15列0 0                                         
16 AggSet 0 0                                         
17列0 0                                         
18列0 1                                         
19加0 0                                         
20整数1 0                                         
21 AggFunc 0 1 ptr(0x7903a0)                      
22列0 1                                         
23整数2 0                                         
24 AggFunc 0 1 ptr(0x790700)                      
25下一个0 9                                              
26关闭0 0                                              
27 AggNext 0 37                                             
28 AggGet 0 2                                         
29整数10 0 10                                 
30 Ge 1 27                                             
31 AggGet 0 0                                         
32 AggGet 0 1                                         
33 AggGet 0 2                                         
34添加0 0                                         
35回调2 0                                         
36转到0 27                                             
37 Noop 0 0                                         
38暂停0 0

在最后一个示例中生成的代码与前面的示例相同,除了添加了两个用于实现额外WHERE和HAVING子句的条件跳转之外。WHERE子句由查询循环中的指令9到11实现。HAVING子句由输出循环中的指令28至30实现。

在表达式中使用SELECT语句作为术语

非常有名的“结构化查询语言”告诉我们SQL应该支持嵌套查询。而且,实际上,支持两种不同的嵌套。任何返回单行单列结果的SELECT语句都可以用作另一个SELECT语句的表达式中的术语。并且,可以将返回单列多行结果的SELECT语句用作IN和NOT IN运算符的右侧操作数。我们将以第一种嵌套的示例开始本节,在该示例中,将单行单列SELECT用作另一个SELECT表达式中的术语。这是我们的示例:

选择*从示例
在两个位置!=(从exep2中选择三个)
            其中4 = 5);

SQLite处理此问题的方法是,首先运行内部SELECT(针对exmap2的那个),然后将其结果存储在专用存储单元中。然后,当SQLite评估外部SELECT时,它将用此私有存储单元的值替换为内部SELECT。代码如下:

addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- ----------------
0字符串0 0                                         
1存储器0 1                                         
2整数0 0                                         
3 OpenRead 1 5示例
2 4 VerifyCookie                              0 909                                            
5倒带1 13                                             
6列1 1                                         
7整数5 0 5                                  
8 Ne 1 12                                        
9列1 0                                         
10 MemStore 0 1                                         
11转到0 13                                             
12下一个1 6                                              
13关闭1 0                                         
14 ColumnName 0 0一个                                
15 ColumnName 1 0两个                                
16整数0 0                                         
17 OpenRead 0 3示例                              
18快退0 26                                             
19列0 1                                         
20 MemLoad 0 0                                         
21 Eq 1 25                                             
22列0 0                                         
23列0 1                                         
24回调2 0                                         
25下一个0 19                                             
26关闭0 0                                         
27暂停0 0

前两个指令将专用存储单元初始化为NULL。指令2到13对examp2表实现了内部SELECT语句。请注意,不是将结果发送到回调或将结果存储在排序器上,而是将指令10的查询结果压入存储单元,并通过指令11的跳转放弃循环。在11处的指令跳转是残留的,永远不会执行。

外部SELECT由指令14至25实现。特别地,包含嵌套选择的WHERE子句由指令19至21实现。您可以看到内部选择的结果已由指令20加载到堆栈上并被使用。被有条件跳到21

当子选择的结果是标量时,可以使用单个私有存储单元,如前面的示例所示。但是,当子选择的结果是向量时,例如当子选择是IN或NOT IN的右侧操作数时,则需要另一种方法。在这种情况下,子选择的结果存储在临时表中,并使用Found或NotFound运算符测试该表的内容。考虑以下示例:

选择*从示例
在两个输入位置(从EXAMP2选择三个);

为实现此最后一个查询而生成的代码如下:

addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- ----------------
0 OpenTemp 1 1                                         
1整数0 0                                         
2 OpenRead 2 5示例2 3 VerifyCookie                             
0 909                                            
4倒带2 10                                        
5列2 0                                         
6 IsNull -1 9                                              
7字符串0 0                                         
8 PutStrKey 1 0                                         
9下一个2 5                                              
10关闭2 0                                         
11 ColumnName 0 0一个                                
12 ColumnName 1 0两个                                
13整数0 0                                         
14 OpenRead 0 3示例                              
15倒带0 25                                             
16列0 1                                         
17 NotNull -1 20                                        
18弹出1 0                                         
19转到0 24                                             
20未找到1 24                                             
21列0 0                                         
22列0 1                                         
23回调2 0                                         
24下一个0 16                                             
25关闭0 0                                         
26暂停0 0

内部SELECT的结果存储在其中的临时表是由OpenTemp 指令(位于0 )创建的。此操作码用于仅在单个SQL语句期间存在的表。即使主数据库是只读的,瞬态游标也始终以读写方式打开。关闭游标时,临时表将自动删除。P2值为1表示光标指向BTree索引,该索引没有数据,但可以具有任意键。

内部的SELECT语句由指令1到10实施。此代码所做的全部工作是在临时表中为examp2表的每一行创建一个条目,并为“三”列提供一个非NULL值。每个临时表条目的键是望子2的“三”列,并且该数据是空字符串,因为它从未使用过。

外部SELECT由指令11至25实现。特别是,包含IN运算符的WHERE子句由16、17和20的指令实现。指令16将当前行的“ two”列的值压入到堆栈和指令17进行检查以查看它是否为非NULL。如果成功,执行将跳至20,在此处进行测试以查看堆栈顶部是否与临时表中的任何键匹配。其余代码与之前显示的相同。

复合SELECT语句

SQLite还允许使用运算符UNION,UNION ALL,INTERSECT和EXCEPT将两个或多个SELECT语句作为对等体连接。这些复合选择语句是使用临时表实现的。每个操作员的实现略有不同,但是基本思想是相同的。例如,我们将使用EXCEPT运算符。

从示例中选择两个
除了
从exalp2中选择四个;

最后一个示例的结果应该是examp表中“ two”列的每个唯一值,除了exam2 2的“ four”列中的所有值都被删除了。实现此查询的代码如下:

addr操作码p1 p2 p3                                      
---- ------------ ----- ----- ------------------- ----------------
0 OpenTemp 0 1                                         
1 KeyAsData 0 1                                              
2整数0 0                                         
3 OpenRead 1 3示例                              
4 VerifyCookie 0 909                                            
5倒带1 11                                        
6列1 1                                         
7 MakeRecord 1 0                                         
8字符串0 0                                         
9 PutStrKey 0 0                                         
10下一个1 6                                              
11关闭1 0                                         
12整数0 0                                         
13 OpenRead 2 5exep2                             
14倒带2 20                                        
15列2 1                                         
16 MakeRecord 1 0                                         
17 NotFound 0 19                                             
18删除0 0                                         
19下一个2 15                                             
20关闭2 0                                         
21列名0 0四                               
22倒带0 26                                             
23列0 0                                         
24回调1 0                                         
25下一个0 23                                             
26关闭0 0                                         
27暂停0 0

用来建立结果的瞬态表由指令0创建。然后执行三个循环。指令5至10的循环实现了第一个SELECT语句。第二条SELECT语句由指令14至19的循环实现。最后,指令22至25的循环读取瞬态表并为结果中的每一行调用一次回调。

在此示例中,指令1特别重要。通常,Column指令从SQLite文件条目的数据中的较大记录中提取列的值。指令1在临时表上设置一个标志,以便Column代替将SQLite文件条目的键视为数据,并从键中提取列信息。

这是将要发生的事情:第一个SELECT语句将构造结果的行,并将每一行保存为临时表中条目的键。暂态表中每个条目的数据都不会被使用,因此我们用一个空字符串填充它。第二条SELECT语句也构造行,但是第二条SELECT构造的行从瞬态表中删除。这就是为什么我们希望将行存储在SQLite文件的键中,而不是存储在数据中,以便可以轻松地定位和删除它们。

让我们更仔细地看一下这里发生的事情。第一个SELECT由循环在指令5至10处实现。指令5通过倒回其光标来初始化循环。指令6从“示例”中提取“二”列的值,指令7将其转换为一行。指令8将空字符串压入堆栈。最后,指令9将行写入临时表。但是请记住,PutStrKey操作码将堆栈的顶部用作记录数据,将堆栈的下一个用作键。对于INSERT语句,由MakeRecord操作码生成的行是记录数据,而记录键是NewRecno操作码创建的整数。但是这里的角色是相反的,MakeRecord创建的行是记录键,而记录数据只是一个空字符串。

第二个SELECT由指令14至19实现。指令14通过倒回其光标来初始化循环。根据指令15和16在表“ examp2”的“四”列中创建一个新的结果行。但是,如果不是使用PutStrKey将新行写入临时表中,则可以调用Delete将其从临时表中删除,而不必使用PutStrKey它存在。

复合选择的结果通过指令22到25的循环被发送到回调例程。关于此循环,没有什么新的或值得注意的事情,除了以下事实之外:23处的Column指令将从记录中提取一个列。键而不是记录数据。

概括

本文回顾了SQLite的VDBE用于实现SQL语句的所有主要技术。尚未显示的是,这些技术中的大多数都可以结合使用以生成用于适当复杂的查询语句的代码。例如,我们显示了如何在简单查询中完成排序,并显示了如何实现复合查询。但是我们没有给出在复合查询中进行排序的示例。这是因为对复合查询进行排序不会引入任何新概念:它仅在同一VDBE程序中结合了两个先前的想法(排序和复合)。

有关SQLite库功能的其他信息,请读者直接查看SQLite源代码。如果您了解本文中的内容,那么在遵循源代码方面应该不会有太多困难。认真研究SQLite内部知识的学生可能还希望仔细研究此处记录的VDBE操作码。大多数操作码文档都是使用脚本从源代码中的注释中提取的,因此您也可以直接从vdbe.c源文件中获取有关各种操作码的信息。如果您已经成功阅读了此书,那么理解其余内容应该没有什么困难。

如果您在文档或代码中发现错误,请随时进行修复和/或通过drh@hwaci.com与作者联系 。始终欢迎您解决错误或提出建议。