小。速度很快。可靠。
选择任意三个选项。

SQLite的虚拟数据库引擎

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

如果您想知道SQLite库的内部工作方式,首先,您需要对虚拟数据库有扎实的了解发动机或VDBE。VDBE发生在处理流(请参阅体系结构图)所以它似乎触及了图书馆的大部分。偶数代码中不直接与VDBE交互的部分通常扮演配角。VDBE真的是苏莱特。

本文简要介绍了VDBE工作原理,特别是各种VDBE指令(记录在案在这里)一起工作用数据库做一些有用的事情。风格是教程,从简单的任务开始,努力解决更多复杂的问题。沿途我们将参观最多SQLite库中的子模块。完成本教程后,您应该非常了解SQLite的工作原理并准备开始研究实际的源代码。

前期工作

VDBE实现了在中运行程序的虚拟计算机它的虚拟机语言。每个项目的目标是查询或更改数据库。朝着这个方向,机器VDBE实现的语言专门设计用于搜索、读取和修改数据库。

VDBE语言的每条指令都包含一个操作码和标记为P1、P2和P3的三个操作数。操作数P1是任意的整数。P2是一个非负整数。P3是指向数据的指针结构或以零结尾的字符串,可能为null。只有少数VDBE指令使用所有三个操作数。许多说明仅用于一个或两个操作数。大量指令的使用根本没有操作数,而是获取其数据并存储其结果在执行堆栈上。每个指令的详细信息在单独的操作码描述文档。

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

如果你做过汇编语言编程或以前使用过任何类型的抽象机器,所有这些细节你应该很熟悉。所以让我们跳进去开始寻找一些代码。

将记录插入数据库

我们从一个可以使用VDBE程序解决的问题开始这只是几个指令。假设我们有一个SQL创建的表如下:

CREATE TABLE examp(一个文本,两个int);

换句话说,我们有一个名为“examp”的数据库表,它有两个名为“一”和“二”的数据列。现在假设我们想插入一个记录到这个表中。这样地:

插入examp值(“你好,世界!”,99);

我们可以看到SQLite用于实现此功能的VDBE程序使用INSERT方石英命令行实用程序。第一次启动向上的方石英在一个新的空数据库上,然后创建表。接下来更改的输出格式方石英以一种形式通过输入“.explain”命令。最后,输入上面显示的[INSER]语句,但在用特殊关键字[EXPLAIN]插入。[EXPLAIN]关键字将导致方石英打印VDBE程序,而不是执行它。我们已经:

sqlite测试数据库1
方形>CREATE TABLE examp(一个文本,两个int);
方形>.解释
方形>解释插入示例值(“你好,世界!”,99);
addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0事务0 0
1验证Cookie 0 81
2事务1 0
3整数0 0
4 OpenWrite 0 3示例
5新Recno 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

如上所述,我们的简单insert语句是在12条指令中实现。前3条和后2条指令是一个标准的序言和尾声,所以真正的工作是在中间完成的7说明。没有跳转,所以程序只执行一次从上到下。现在让我们详细了解每一条指令。

0事务处理0 0
1验证Cookie 0 81
2事务1 0

说明书交易开始交易。提交或回滚时事务结束遇到操作码。P1是数据库文件的索引事务已启动。索引0是主数据库文件。A写当事务启动时,会在数据库文件上获得锁。当事务处于正在进行中。启动事务还会创建回滚日志。A类必须先启动事务,然后才能对数据库。

说明书验证Cookie检查cookie 0(数据库架构版本)以确保其相等到P2(上次读取数据库模式时获得的值)。P1是数据库编号(主数据库为0)。这样做是为了确保数据库架构没有被其他线程更改,在在这种情况下,必须重新阅读。

第二个交易指令启动事务并启动的回滚日志数据库1,用于临时表的数据库。

3整数0 0
4 OpenWrite 0 3示例

说明书整数推动将整数值P1(0)添加到堆栈中。这里0是要在以下OpenWrite指令中使用的数据库。如果P3不是NULL,则它是相同整数的字符串表示。之后堆栈如下所示:

(整数)0

说明书OpenWrite(打开写入)打开表“examp”上带有句柄P1(本例中为0)的新读/写光标,其根页面为P2(在该数据库文件中为3)。光标句柄可以是任何非负整数。但VDBE在数组中分配游标数组的大小比最大游标大一倍。所以为了节省内存,最好使用以零和开头的句柄连续向上工作。这里P3(“examp”)是表被打开,但这是未使用的,并且生成它只是为了使代码更容易阅读。此指令弹出要使用的数据库编号(0,主数据库),因此之后堆栈再次为空。

5新Recno 0 0

说明书NewRecno公司创建光标P1指向的表的新整数记录号。这个记录编号是一个当前未在表中用作键的编号。新的记录号被推送到堆栈上。之后堆栈看起来像这个:

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

说明书字符串推动其P3操作数放到堆栈上。之后,堆栈如下所示:

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

说明书整数推动将其P1操作数(99)放到堆栈上。之后堆栈看起来像这个:

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

说明书制作记录持久性有机污染物堆栈顶部的P1元素(本例中为2)并将其转换为用于在数据库文件中存储记录的二进制格式。(请参阅文件格式的描述详细信息。)MakeRecord指令生成的新记录是推回到堆栈上。之后,堆栈如下所示:

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

说明书PutInt密钥使用将项写入到由指向的表中的前2个堆栈项光标P1。如果新条目尚不存在,或覆盖现有条目的数据。记录数据为顶部堆栈条目,键是下一个条目。堆栈弹出按此指示进行两次。因为操作数P2是1,所以行更改计数递增,并存储rowid以供后续返回sqlite_last_insert_rowid()函数。如果P2为0,则行更改计数为未修改。此指令是插入实际发生的位置。

10关闭0 0

说明书关闭关闭光标以前作为P1打开(0,唯一打开的光标)。如果P1不是当前打开,此指令为no-op。

11提交0 0

说明书提交导致所有自上次以来对数据库所做的修改交易实际生效。没有其他修改直到启动另一个事务。Commit指令删除日志文件并释放数据库上的写锁。如果仍有游标打开,则继续保持读取锁定。

12停止0 0

说明书暂停导致VDBE立即退出发动机。所有打开的光标、列表、排序等都是自动关闭。P1是sqlite_exec()返回的结果代码。对于正常暂停,这应该是SQLITE_OK(0)。对于错误,可以是其他一些价值。操作数P2仅在出现错误时使用。在每个程序,VDBE在准备运行程序时附加该程序。

跟踪VDBE程序执行

如果在没有NDEBUG预处理器的情况下编译SQLite库宏观,然后是PRAGMAvdbe_trace(vdbe_trace)使VDBE跟踪程序的执行。尽管如此该功能最初用于测试和调试,它还可以有助于了解VDBE的操作方式。使用“PRAGMA vdbe_trace=开;“打开跟踪并"PRAGMA vdbe_trace=关闭“关闭跟踪。这样地:

sqlite公司>PRAGMA vdbe_trace=开;
0停止0 0
方形>插入examp值(“你好,世界!”,99);
0事务0 0
1验证Cookie 0 81
2事务1 0
3整数0 0
堆栈:i:0
4 OpenWrite 0 3示例
5新Recno 0 0
堆栈:i:2
6字符串0 0你好,世界!
堆栈:t[Hello,.World!]i:2
7整数99 0 99
堆栈:si:99 t[你好,世界!]i:2
8 MakeRecord 2 0
Stack:s[…你好,.World!.99]i:2
9 PutIntKey 0 1
10关闭0 0
11提交0 0
12停止0 0

在跟踪模式打开的情况下,VDBE打印之前的每个指令执行它。执行指令后,前几个将显示堆栈中的条目。忽略堆栈显示如果堆栈为空。

在堆栈显示中,大多数条目都带有前缀它告诉堆栈项的数据类型。整数开始带有“i:“.浮点值以”回复:".(“r”代表“实数”。)字符串以其中之一开头"秒:", "电话:", "电子邮箱:“或”兹:". 字符串前缀之间的差异是由内存已分配。字符串存储在获取的内存中malloc()。t:字符串是静态分配的。e:字符串是短暂的。所有其他字符串都有s:前缀。这对你来说没什么区别,但这对VDBE至关重要,因为z: 字符串需要传递给自由()当他们弹出以避免内存泄漏。注意,只有前10个显示字符串值的字符和二进制值(例如MakeRecord指令的结果)如下视为字符串。唯一可以存储的其他数据类型VDBE堆栈上为NULL,显示时不带前缀简单地说“无效的“。如果在堆栈为整数和字符串,其前缀为“硅:".

简单查询

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

从examp中选择*;

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

方形>解释SELECT*FROM examp;
addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0 ColumnName 0 0 one
1 ColumnName 1 0二
2整数0 0
3 OpenRead 0 3示例
4验证Cookie 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提供指向回调函数的指针p用户数据指针。(回调和用户数据都是最初作为参数传入sqlite_exec()API函数。)VDBE的工作是n列,azData[], az列名称[].n列当然,是结果中的列数。az列名称[]是字符串数组,其中每个字符串都是名称结果列之一。azData[]是一个字符串数组实际数据。

0 ColumnName 0 0 one
1 ColumnName 1 0二

VDBE程序中用于查询的前两条指令是与设置值有关az柱.这个字段名说明说明VDBE要为每个元素填充的值az列名称[]数组。每个查询都将以一条ColumnName指令开始列,并且将有一个匹配的column指令每一个都在后面的查询中。

2整数0 0
3 OpenRead 0 3示例
4验证Cookie 0 81

指令2和3在数据库表上打开一个读取游标要查询的。这与INSERT示例,但此时打开光标进行读取而不是为了写作。指令4验证数据库架构为在INSERT示例中。

5倒带0 10

这个重绕指令初始化在“examp”表上迭代的循环。它将光标P1倒回到其表中的第一个条目。这是专栏和接下来的指令,使用光标遍历表。如果表格为空,则跳转到P2(10),这是刚才的指令通过循环。如果表格不为空,请执行以下操作第6条指令,这是循环体的开始。

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

指令6到8构成了循环的主体对数据库文件中的每个记录执行一次。这个地址6的指令和7分别从P1-th光标中取出P2-th列并将其推到堆栈。在本例中,第一条Column指令将堆栈中“一”列和第二列的值指令正在推送列“2”的值。这个回拨地址8的指令调用callback()函数。回调的P1操作数变为的值n列。回调指令从中弹出P1值堆栈并使用它们填充azData[]数组。

9下一个0 6

地址9处的指令实现循环。它与地址5处的倒带一起构成循环逻辑。这是一个你应该密切关注的关键概念。这个下一步指令使光标前进P1到下一条记录。如果光标前进成功,则跳转紧邻P2(6,循环体的开始)。如果光标是在最后,然后执行以下指令结束循环。

10关闭0 0
11停止0 0

程序末尾的Close指令关闭指向表“examp”的光标。其实没有必要在此处调用Close,因为所有光标都将自动关闭程序停止时由VDBE执行。但我们需要指示让“倒带”跳到,这样我们就可以继续了指导做一些有用的事情。Halt指令结束VDBE程序。

请注意,此SELECT查询的程序不包含INSERT示例中使用的事务和提交指令。因为SELECT是一个读取操作,它不会改变数据库不需要交易。

稍微复杂一些的查询

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

选择one,two,one||two作为“both”来自examp其中一个像“H%”

这个查询可能有点做作,但它确实有助于说明我们的观点。结果将包含三列名称“一”、“二”和“二”。前两列是直接的表中两列的副本和第三个结果列是由第一个和表的第二列。最后WHERE子句表示我们将只为结果,其中“一”列以“H”开头。以下是VDBE程序在此查询中的外观:

addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0 ColumnName 0 0 one
1 ColumnName 1 0二
2 ColumnName 2 0两者
3整数0 0
4 OpenRead 0 3示例
5验证Cookie 0 81
6倒带0 18
7字符串0 0 H%
8列0 0
9功能2 0 ptr(0x7f1ac0)
10如果不是1 17
11列0 0
12第0列1
13列0 0
14列0 1
15混凝土20
16回拨3 0
17下一个0 7
18关闭0 0
19停止0 0

除了WHERE子句之外这个示例与前面的示例非常相似,只是使用了一个额外的列。现在有3列,而不是以前的2列,并且有三条ColumnName指令。使用OpenRead指令打开光标,就像前面的示例。地址6处的倒带指令和下一步,地址17在表的所有记录上形成一个循环。末尾的Close指令用于倒回指令完成后要跳转的内容。所有这与第一个查询演示中的情况类似。

本例中的Callback指令必须生成三个结果列而不是两个结果列的数据,但在其他情况下与第一个查询中相同。当回调指令则结果的最左侧列应为堆栈中最低的和最右边的结果列应该成为堆栈的顶部。我们可以看到正在设置堆栈地址11到15都是这样。列说明位于11和12推送结果中前两列的值。第13列和第14列中的两个列指令引入所需的值计算第三个结果列和Concat指令15将它们连接在一起成为堆栈上的单个条目。

当前示例唯一真正新的地方是是WHERE子句,由位于的指令实现地址7到10。地址7和8的指令推送将表中“一”列的值放到堆栈中和文字字符串“H%”。这个功能地址9的指令从堆栈中弹出这两个值并推送LIKE()的结果函数返回堆栈。这个如果不是指令弹出顶部堆栈如果最高值为false(与文字字符串“H%”不同)。有效地执行此跳转可以跳过回调,这就是要点WHERE子句的。如果结果是的比较是真的,跳跃是不采取和控制的一直到下面的回调指令。

注意LIKE操作符是如何实现的。它是用户定义的函数,因此其函数定义的地址为P3中规定。操作数P1是从堆栈中取出。在这种情况下,LIKE()函数需要2论据。参数以相反的顺序从堆栈中取出(从右到左),因此要匹配的模式是顶部堆栈元素,并且下一个元素是要比较的数据。推送返回值放在堆栈上。

SELECT程序模板

前两个查询示例演示了一种模板随后将执行每个SELECT程序。基本上,我们有:

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

此模板将根据我们的考虑进行大幅扩展附加的复杂性,如连接、复合选择、使用索引以加快搜索、排序和聚合功能有或没有GROUP BY和HAVING子句。但同样的基本理念将继续适用。

UPDATE和DELETE语句

UPDATE和DELETE语句使用模板进行编码这与SELECT语句模板非常相似。主要当然,区别在于最终操作是修改而不是调用回调函数。因为它可以修改它还将使用事务的数据库。开始吧通过查看DELETE语句:

从示例中删除,其中2<50;

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

地址操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0事务1 0
1事务0 0
2验证Cookie 0 178
3整数0 0
4 OpenRead 0 3示例
5倒带0 12
6列0 1
7整数50 0 50
8锗11
9记录0 0
10列表写入0 0
11下一个0 6
12关闭0 0
13列表回放0 0
14整数0 0
15 OpenWrite 0 3
16列表读取0 20
17不存在0 19
18删除0 1
19转到0 16
20列表重置0 0
21关闭0 0
22提交0 0
23停止0 0

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

因此,DELETE的实现实际上是两个循环。第一个循环(说明5至11)定位要删除的记录并将其密钥保存到临时列表中,然后执行第二个循环(说明16至19)使用密钥列表删除记录1一个。

0事务1 0
1事务0 0
2验证Cookie 0 178
3整数0 0
4 OpenRead 0 3示例

指令0到4与INSERT示例中的相同。他们开始了主数据库和临时数据库的事务,验证数据库主数据库的模式,并在表上打开读取游标“示例”。请注意,打开光标是为了阅读,而不是写作。在这个阶段,我们只会扫描表格,不更改它。我们稍后将重新打开同一个表以进行写入,在说明15。

5倒带0 12

与SELECT示例中一样重绕指令将光标倒回表的开头,准备就绪它用于循环体。

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

WHERE子句由指令6到8实现。where子句的任务是跳过ListWrite,如果where条件为false。为此,它跳到Next指令如果“两”列(由column指令提取)为大于或等于50。

如前所述,Column指令使用光标P1并推送数据将P2列(1,“2”列)中的记录记录到堆栈上。整数指令将值50推送到堆栈顶部。在这些之后堆栈看起来像两条指令:

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

这个Ge公司操作员比较前两个元素,弹出它们,然后根据结果进行分支进行比较。如果第二个元素>=顶部元素,则跳转到地址P2(循环末尾的Next指令)。因为P1为true,所以如果任一操作数为NULL(因此结果为NULL),然后跳转。如果我们不跳,就跳到下一个说明。

9记录0 0
10列表写入0 0

这个Recno公司指令推送到堆栈一个整数,它是当前键的前4个字节光标P1指向的表的顺序扫描中的条目。这个列表写入指令写入堆栈顶部的整数放入临时存储列表并弹出顶部元素。这是此循环的重要工作,用于存储要删除的记录的键,以便我们可以在第二次删除它们循环。在此ListWrite指令之后,堆栈再次为空。

11下一个0 6
12关闭0 0

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

13列表回放0 0

这个列表回放指令将临时存储列表倒回到开头。这为它做好了准备用于第二个循环。

14整数0 0
15 OpenWrite 0 3

与INSERT示例中一样,我们将数据库编号P1(0,主数据库),并使用OpenWrite打开表上的光标P1P2(基本页3,“示例”)进行修改。

16列表读取0 20
17不存在0 19
18删除0 1
19转到0 16

此循环执行实际删除。它的组织方式与UPDATE示例中的一个。ListRead指令起作用Next在INSERT循环中所做的,但因为它在上跳到P2失败,成功后再跳,我们把它放在循环的开始而不是结局。这意味着我们必须在在开始时跳回循环测试的循环。所以这个循环的形式为C while(){…}循环,而INSERT中的循环示例的形式为do{…}同时()循环。Delete指令填充回调函数在前面的示例中所扮演的角色。

这个列表已读指令读取元素并将其推送到堆栈上。如果成功,则继续执行下一条指令。如果是这样失败,因为列表为空,它分支到P2,即循环后的指令。之后,堆栈看起来像:

当前记录的(整数)键

注意ListRead和Next指令之间的相似性。这两种操作都根据此规则工作:

将下一个“东西”推到堆栈上并通过或跳到P2,取决于是否有下一个“东西”需要推动。

Next和ListRead之间的一个区别是他们对“事物”的想法。Next指令的“内容”是数据库文件中的记录。ListRead的“Things”是列表中的整数键。另一个区别就是如果没有下一个“东西”,是跳下去还是摔倒。在这个情况下,Next失败,ListRead跳转。稍后,我们会看到使用同样的原则。

这个不存在指令弹出顶层堆栈元素,并将其用作整数键。如果记录带有表P1中不存在该键,然后跳转到P2。如果有记录存在,然后执行下一个指令。在这种情况下,P2取我们转到循环末尾的Goto,它跳回ListRead一开始。这可能被编码为P2为16ListRead位于循环的开头,但生成这段代码没有进行优化。

这个删除做这个工作吗回路;它从堆栈中弹出一个整数键(由并删除具有该键的光标P1的记录。因为P2为true,所以行更改计数器递增。

这个转到跳回到开头循环的。这是循环的结束。

20列表重置0 0
21关闭0 0
22提交0 0
23停止0 0

此指令块清除VDBE程序。其中三个指令实际上不是必需的,而是由SQLite生成的从其代码模板中解析器,该模板旨在处理更多复杂案例。

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

UPDATE语句的工作方式与DELETE语句非常类似,但他们没有删除记录,而是用一个新的记录替换它。考虑这个例子:

UPDATE examp SET one='('||one||')'WHERE twee<50;

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

addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0事务1 0
1事务0 0
2验证Cookie 0 178
3整数0 0
4 OpenRead 0 3示例
5倒带0 12
6列0 1
7整数50 0 50
8锗11
9记录0 0
10列表写入0 0
11下一个0 6
12关闭0 0
13整数0 0
14 OpenWrite 0 3
15列表回放0 0
16列表读取0 28
17重复0 0
18不存在0 16
19字符串0 0(
20列0 0
21混凝土20
22字符串0 0)
23凹形2 0
24第0列1
25 MakeRecord 2 0
26 PutIntKey 0 1
27转到0 16
28列表重置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指令以前从未使用过的。

创建和删除

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

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

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

但从VDBE的角度来看,CREATE是有效的INSERT和DROP的工作原理非常类似于DELETE。当SQLite库打开到现有数据库时,它所做的第一件事是使用SELECT来读取“sql”sqlitemaster表所有条目中的列。“sql”列包含最初生成索引或表。此文本反馈到SQLite解析器用来重建描述索引或表的内部数据结构。

使用索引加快搜索速度

在上面的示例查询中,表的每一行必须从磁盘加载并检查查询的内容,即使只有结果中包含一小部分行。这个可以在一张大桌子上花很长时间。为了加快速度,SQLite可以使用索引。

SQLite文件将关键字与一些数据相关联。对于SQLite表中,将设置数据库文件,使键为整数数据是表中一行的信息。SQLite中的索引颠倒了这种安排。索引键是正在存储的信息和索引数据是一个整数。要访问具有特定内容,我们首先在索引表中查找内容以查找它的整数索引,然后我们使用该整数查找完成表中的记录。

注意,SQLite使用作为排序数据结构的b树,因此,当SELECT语句的WHERE子句包含相等或不相等的测试。类似以下查询可以使用索引(如果可用):

从示例中选择*,其中两个==50;从示例中选择*,其中2<50;从示例中选择*,其中两个IN(50,100);

如果存在映射“examp”的“two”列的索引表转换为整数,然后SQLite将使用该索引查找整数examp中第二列值为50的所有行的键,或所有小于50的行,等等。但以下查询无法使用索引:

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

请注意,SQLite解析器并不总是生成代码来使用索引,即使可以这样做当前使用索引:

从示例中选择*,其中2+10==50;从示例中选择*,其中2==50或2==100;

为了更好地理解指数是如何工作的,让我们首先看看如何它们是被创建的。让我们继续对这两个进行索引examp表的列。我们有:

在examp上创建索引examp_idx1(两个);

上述语句生成的VDBE代码类似于以下内容:

addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0事务1 0
1事务0 0
2验证Cookie 0 178
3整数0 0
4 OpenWrite 0 2
5新Recno 0 0
6字符串0 0索引
7字符串0 0 examp_idx1
8字符串0 0 examp
9 CreateIndex 0 0 ptr(0x791380)
10重复0 0
11整数0 0
12 OpenWrite 1 0
13字符串0 0 CREATE INDEX examp_idx1 ON examp(tw
14制作记录5 0
15 PutIntKey 0 0
16整数0 0
17 OpenRead 2 3示例
18倒带2 24
19记录20
20第2列1
21 MakeIdx密钥10 n
22 IdxPut 10索引列不唯一
23下一步2 19
24关闭20
25关闭10
26整数333 0
27 SetCookie 0 0
28关闭0 0
29提交0 0
30停止0 0

请记住,每个表(sqlite_master除外)和每个命名的索引在sqlitemaster表中有一个条目。因为我们正在创造作为一个新索引,我们必须向sqlitemaster添加一个新条目。这是按说明3至15处理。将条目添加到sqlite_master工作原理与其他INSERT语句一样,因此我们不再赘述关于这里。在这个示例中,我们希望重点放在填充包含有效数据的新索引,发生在指令16到23

16整数0 0
17 OpenRead 2 3示例

首先发生的事情是,我们打开表编入索引以供阅读。为了构造表的索引,我们必须知道那张桌子里有什么。索引已经通过指令3和4打开以使用光标0进行写入。

18倒带2 24
19记录20
20第2列1
21 MakeIdx密钥10 n
22 IdxPut 1 0个索引列不是唯一的
23下一步2 19

指令18到23在正在索引的表。对于每个表行,我们首先提取整数键,然后获取使用说明20中的column将“两”列改为column。这个MakeIdx密钥21时的指令转换“two”列(位于堆栈顶部)中的数据到有效的索引键中。对于单列上的索引,这是基本上没有。但如果MakeIdxKey的P1操作数堆栈中会弹出多个条目并转换为单个索引键。这个Idx放置22岁的指导是什么实际创建索引项。IdxPut从堆栈。堆栈顶部用作从索引表。然后将堆栈上的第二个整数添加到该索引的整数集,并且新记录被写回数据库文件。注释同一索引项可以存储多个整数是两个或多个具有相同值的表条目列。

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

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

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

addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0 ColumnName 0 0 one
1列名称1 0 2
2整数0 0
3 OpenRead 0 3示例
4验证Cookie 0 256
5整数0 0
6打开读取1 4 example_idx1
7整数50 0 50
8 MakeKey 1 0 n
9 MemStore 0 0
10移至1 19
11内存负载0 0
12 IdxGT 1 19
13 IdxRecno 10号
14移至0 0
15列0 0
16列0 1
17回拨2 0
18下一步11
19关闭0 0
20关闭10
21停止0 0

SELECT以一种熟悉的方式开始。第一列名称被初始化,正在查询的表被打开。从说明5和6开始,情况有所不同,其中索引文件也将打开。说明7和8制作值为50的键。这个MemStore(内存存储)9家店铺的指示VDBE内存位置0中的索引键。VDBE内存用于避免从堆栈深处获取值,这是可以做到的,但使程序更难生成。以下说明移动到在地址10处,拔出钥匙堆栈并使用将索引光标移动到索引的第一行那把钥匙。这将初始化光标,以便在以下循环中使用。

指令11到18对所有索引记录执行循环使用指令8获取的密钥。所有索引具有该键的记录在索引表中是连续的,所以我们走并从索引中获取相应的表键。然后使用此表键将光标移动到表中的该行。循环的其余部分与无索引SELECT的循环相同查询。

循环以MemLoad(内存加载)位于11的指令,它将索引键的副本推回到堆栈。说明书Idx燃气轮机12点将键与指向的当前索引记录中的键进行比较光标P1。如果当前光标位置的索引键大于然后跳出循环。

说明书Idx Recno公司在13将索引中的表记录号推送到堆栈上。这个接下来的MoveTo会弹出它并将表光标移动到该行。这个接下来的3条指令以与非-索引大小写。Column指令获取列数据调用回调函数。最后的Next指令推进将游标(而不是表游标)索引到下一行,然后进行分支如果还有索引记录,则返回循环的开始。

由于索引用于查找表中的值,索引和表保持一致很重要。现在examp表上有了一个索引,我们将有每当插入、删除数据或在examp表中进行了更改。记住上面的第一个例子我们可以使用在“example”表中插入新行12 VDBE指令。现在这个表已经编入索引,19需要说明。SQL语句如下:

插入示例值(“你好,世界!”,99);

生成的代码如下所示:

addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0事务1 0
1事务0 0
2验证Cookie 0 256
3整数0 0
4 OpenWrite 0 3示例
5整数0 0
6 OpenWrite 1 4示例_idx1
7新Recno 0 0
8字符串0 0你好,世界!                      
9整数99 0 99
10重复2 1
11重复11
12 MakeIdx密钥10 n
13 Idx放置10
14制作记录20
15 PutIntKey 0 1
16关闭0 0
17关闭1 0
18提交0 0
19停止0 0

此时,您应该充分了解VDBE,以便自己弄清楚上述程序是如何工作的。所以我们会的本文不再进一步讨论。

连接

在联接中,将两个或多个表组合在一起以生成单个结果。结果表由每种可能的组合组成正在联接的表中的行。最简单、最自然实现这一点的方法是使用嵌套循环。

回忆一下上面讨论的查询模板,其中有一个搜索表中每个记录的单个循环。在连接中,我们有基本相同的东西,除了那里是嵌套循环。例如,要连接两个表,查询模板可能如下所示:

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

这个模板可以工作,但由于我们正在处理O(N2)循环。但它经常起作用指出WHERE子句可以被分解为术语,而WHERE或这些术语中更多的只涉及第一个表中的列。当这种情况发生时,我们可以将WHERE子句测试的一部分内部循环并获得了很大的效率。所以更好的模板会是这样的:

  1. 初始化az列名称[]回调的数组。
  2. 打开两个游标,分别指向被查询的两个表。
  3. 对于第一个表中的每条记录,请执行以下操作:
    1. 计算仅涉及中的列的WHERE子句的术语第一张桌子。如果任何术语为假(即整体WHERE子句必须为false),然后跳过此循环的其余部分继续下一条记录。
    2. 对于第二张表中的每条记录,请执行以下操作:
      1. 如果WHERE子句的计算结果为FALSE,则跳过以下步骤遵循并继续下一条记录。
      2. 计算结果当前行的所有列。
      3. 为结果的当前行调用回调函数。
  4. 关闭两个光标。

如果可以使用索引加快速度,则可能会出现额外的加速搜索任意或两个循环。

SQLite总是按照与表出现在SELECT语句的FROM子句中。这个最左侧的表成为外部循环,最右侧的表成为内部循环。理论上,可以重新排序在某些情况下循环以加快加入。但SQLite并没有尝试这种优化。

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

创建表格示例2(三个int,四个int);从examp、examp2中选择*,其中2<50且4==2;
addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0 ColumnName 0 0示例.one
1 ColumnName 1 0示例.two
2 ColumnName 2 0示例2.three
3 ColumnName 3 0示例2.four
4整数0 0
5 OpenRead 0 3示例
6验证Cookie 0 909
7整数0 0
8 OpenRead 1 5示例2
9倒带0 24
10列0 1
11整数50 0 50
12锗123
13倒带1 23
14第1列1
15列0 1
16内1 22
17第0列0
18列0 1
19第10列
20第1列1
21回拨4 0
22下一个1 14
23下一个0 10
24关闭0 0
25关闭10
26停止0 0

表examp上的外部循环由指令实现7至23。内部循环是指令13到22。请注意,WHERE表达式的“2<50”术语包括只有第一个表中的列可以被分解内部循环。SQLite做到了这一点,并实现了“2<50”按照说明10至12进行测试。“四==二”测试是通过内部循环中的指令14到16执行。

SQLite不会对加入。它还允许将表与自身联接。

ORDER BY子句

出于历史原因和效率考虑,目前所有分拣在内存中完成。

SQLite使用一个特殊的用于控制一个称为分类器的对象的一组指令。查询的最内层循环,通常回调指令,而是构造一条记录包含回调参数和键。这个记录添加到分拣机(在链接列表中)。查询循环之后完成后,对记录列表进行排序,并遍历此列表。对于列表中的每个记录都会调用回调。最后,分拣机关闭,内存被释放。

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

从examp ORDER中选择*,按一个DESC,两个;
addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0 ColumnName 0 0 one
1列名称1 0 2
2整数0 0
3 OpenRead 0 3示例
4验证Cookie 0 909
5倒带0 14
6列0 0
7列0 1
8分类MakeRec 2 0
9列0 0
10列0 1
11 SortMakeKey 2 0 D+
12排序放入0 0
13下一个0 6
14关闭0 0
15排序0 0
16排序下一个0 19
17排序回调2 0
18转到0 16
19排序重置0 0
20停止0 0

只有一个分拣机对象,因此没有打开的指令或关闭。需要时自动打开,关闭当VDBE程序停止时。

查询循环是根据指令5到13构建的。说明书6到8构建一条记录,其中包含单个调用回调。排序键由指令生成9至11。指令12将调用记录与将关键字排序为单个条目,并将该条目放在排序列表中。

指令11的P3参数特别有趣。这个排序键是通过在每个字符串前面加上一个P3字符而形成的并连接所有字符串。排序比较功能将查看此字符以确定排序顺序是否为升序或降序,以及是否按字符串或数字排序。在这个例子中,第一列应该作为字符串排序按降序排列,因此其前缀为“D”,第二列应按升序以数字排序,因此其前缀为“+”。提升字符串排序使用“A”,数字降序排序使用“-”。

查询循环结束后,正在查询的表将在关闭说明14。这是尽早完成的,以便允许其他流程或线程来访问该表(如果需要)。记录列表在查询循环中构建的是按指令排序的第15页。说明16至18浏览记录列表(现在已按排序顺序)并为调用一次回调每个记录。最后,分拣机按指示19关闭。

聚合函数与GROUP BY和HAVING子句

为了计算聚合函数,VDBE实现了一个特殊的数据结构和控制该数据结构的指令。数据结构是一组无序的桶,其中每个桶有一个键和一个或多个内存位置。在查询中循环中,GROUP BY子句用于构造键和bucket有了这把钥匙,我们就能聚焦。使用创建一个新铲斗键(如果以前不存在)。一旦铲斗进入焦点,存储桶的内存位置用于累加各种聚合函数的值。查询之后循环终止,每个bucket被访问一次以生成一个结果的单行。

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

选择三,最小值(三+四)+平均值(四)来自示例2三人一组;

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

addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0列名称0 0三
1 ColumnName 1 0 min(三+四)+avg(四)
2 AggReset 0 3
3聚合初始化0 1 ptr(0x7903a0)
4 AggInit 0 2 ptr(0x790700)
5整数0 0
6 OpenRead 0 5示例2
7验证Cookie 0 909
8倒带0 23
9列0 0
10 MakeKey 1 0 n
11聚合焦点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整数20
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回拨20
30转到0 24
31努普0 0
32停止0 0

感兴趣的第一条指令是AggReset(聚合重置)第2页。AggReset指令将桶集初始化为空集并指定每个中可用的内存插槽数铲斗为P2。在本例中,每个存储桶将容纳3个内存插槽。这并不明显,但如果你仔细看一下程序的其余部分您可以找出每个插槽的用途。

内存插槽此内存插槽的预期用途
0“三”列——桶的钥匙
1最小“三+四”值
2所有“四”个值的总和。这用于计算“平均值(四)”。

查询循环由指令8到22实现。计算GROUP by子句指定的聚合键按照说明9和10。说明11导致适当的bucket将成为焦点。如果具有给定键的bucket不存在,则创建一个新的bucket,控制权下降到指令12和13,这些指令初始化bucket。如果bucket已经存在,则跳转到指令14.聚合函数的值由指令更新11到21之间。指令14至18更新内存插槽1保存下一个值“min(三+四)”。然后是“四”栏根据说明19至21进行更新。

查询循环完成后,表“examp2”在指令23,以便释放其锁由其他线程或进程使用。下一步是循环覆盖所有聚合桶并输出结果的一行每个水桶。这是由指令24中的循环完成的至30。24的AggNext指令将产生下一个bucket或跳转到循环的末尾已经检查过了。从中获取结果的3列在指令25至27处按顺序排列聚合器桶。最后,在指令29处调用回调。

总之,使用聚合函数实现任何查询通过两个循环。第一个循环扫描输入表并计算将信息聚合到bucket中,然后第二个循环扫描所有桶计算最终结果。

聚合查询实际上是两个连续查询的实现循环使我们更容易理解SQL查询语句中的WHERE子句和HAVING子句。这个WHERE子句是对第一个循环和HAVING的限制子句是对第二个循环的限制。你可以看到这个通过向示例查询中添加WHERE和HAVING子句:

选择三,最小值(三+四)+平均值(四)来自示例2其中三个>四个按三分组平均值(四)<10;
addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0 ColumnName 0 0三
1列名称1 0分钟(三+四)+平均值(四)
2 AggReset 0 3
3聚合初始化0 1 ptr(0x7903a0)
4 AggInit 0 2 ptr(0x790700)
5整数0 0
6 OpenRead 0 5示例2
7验证Cookie 0 909
8倒带0 26
9列0 0
10列0 1
11乐125
12第0列0
13 MakeKey 1 0 n
14聚合焦点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整数20
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锗1 27
31 AggGet 0 0
32 AggGet 0 1
33 AggGet 0 2
34加0 0
35回拨20
36转到0 27
37努普0 0
38停止0 0

最后一个例子中生成的代码与前一个,但添加了两个使用的条件跳转实现额外的WHERE和HAVING子句。WHERE(地点)子句由查询中的指令9到11实现循环。HAVING子句通过指令28到实现输出回路中为30。

在表达式中将SELECT语句用作术语

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

从examp中选择*哪里有两个=(从示例2中选择三个其中四=5);

SQLite处理此问题的方法是首先运行内部SELECT(针对examp2的那个)并将结果存储在私有内存中单元格。然后SQLite替换这个私有内存的值计算外部SELECT时用于内部SELECT的单元格。代码如下所示:

addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0字符串0 0
1 MemStore 0 1
2整数0 0
3 OpenRead 1 5示例2
4验证Cookie 0 909
5倒带1 13
6第1列1
7整数5 0 5
8 Ne 1 12号机组
9第10列
10 MemStore 0 1
11转到0 13
12下一步16
13关闭10
14 ColumnName 0 0一
15 ColumnName 10二
16整数0 0
17 OpenRead 0 3示例
18倒带0 26
19第0列1
20内存负载0 0
21等式1 25
22列0 0
23列0 1
24回拨20
25下一个0 19
26关闭0 0
27停止0 0

私有内存单元被第一个初始化为NULL两条指令。说明2至13实现内部针对examp2表的SELECT语句。请注意将结果发送到回调或将结果存储在分拣机上,查询结果被指令推送到存储单元10,指令11处的跳转放弃了循环。11的指令跳转是残留的,永远不会执行。

外部SELECT由指令14到25实现。特别是,包含嵌套select的WHERE子句按照说明19至21执行。你可以看到内部选择的结果通过指令加载到堆栈中20并由21处的条件跳转使用。

当子选择的结果是标量时,单个专用内存可以使用单元格,如前所示例子。但当子选择的结果是向量时当子选择是IN或NOT IN的右操作数时,需要一种不同的方法。在这种情况下,子选择的结果是存储在瞬态表中以及该表的内容使用Found或NotFound运算符进行测试。考虑一下这个例子:

从示例中选择*其中两个输入(从示例2中选择三个);

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

addr操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0打开温度1 1
1整数0 0
2 OpenRead 2 5示例2
3验证Cookie 0 909
4倒带2 10
5第20列
6 IsNull-1 9
7字符串0 0
8 PutStrKey 1 0
9下一步2 5
10关闭20
11 ColumnName 0 0一
12 ColumnName 1 0二
13整数0 0
14 OpenRead 0 3示例
15倒带0 25
16列0 1
17不为空-1 20
18岁人口10
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结果所在的瞬态表存储是由打开温度0处的指令。此操作码用于存在于仅单个SQL语句的持续时间。瞬时光标总是打开读/写,即使主数据库是只读的。瞬态当光标关闭时,表会自动删除。P2值为1表示光标指向BTree索引,该索引没有数据,但可以具有任意密钥。

内部SELECT语句由指令1到10实现。这些代码所做的只是在临时表中为每个examp2表中“three”列的值为非NULL的行。每个临时表条目的关键是examp2的“三”列数据是一个空字符串,因为它从未被使用过。

外部SELECT由指令11到25实现。特别是,实现了包含IN运算符的WHERE子句根据第16、17和20节的指示。指令16将堆栈上当前行的“两”列和指令17检查它是否为非NULL。如果成功,执行跳到20,测试堆栈顶部是否匹配任何键在临时表中。其余代码与之前显示过。

复合SELECT语句

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

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

最后一个示例的结果应该是每个唯一的值examp表中“two”列的在examp2的“四”列中删除。要实现的代码该查询如下:

地址操作码p1 p2 p3
----  ------------  -----  -----  -----------------------------------
0打开温度0 1
1 KeyAsData 0 1
2整数0 0
3打开阅读1 3示例
4验证Cookie 0 909
5倒带11
6第1列1
7 MakeRecord 1 0
8字符串0 0
9 PutStrKey 0 0
10下一步16
11关闭10
12整数0 0
13 OpenRead 2 5示例2
14倒带2 20
15第2列1
16制作记录10
17未找到0 19
18删除0 0
19下一步2 15
20关闭20
21 ColumnName 0 0四
22倒带0 26
23列0 0
24回拨10
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从“examp”中提取“two”列的值指令7将其转换为一行。指令8推堆栈上的空字符串。最后,指令9写入行放入临时表中。但请记住,PutStrKey操作码使用堆栈的顶部作为记录数据,堆栈上的下一个作为键。对于INSERT语句MakeRecord操作码是记录数据,记录键是整数由NewRecno操作码创建。但在这里,角色颠倒了MakeRecord创建的行是记录键,记录数据是只是一个空字符串。

第二个SELECT由指令14到19实现。指令14通过倒回光标来初始化循环。从表“examp2”的“四”列创建一个新的结果行通过指令15和16。但不是使用PutStrKey来编写将新行添加到临时表中,我们改为调用Delete来删除如果存在,则从临时表中删除。

复合选择的结果被发送到回调例程通过指令22到25处的循环。没有什么新鲜事或者这个循环值得注意,除了Column第23条指令将从记录键中提取一列而不是记录数据。

总结

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

有关SQLite库的其他信息函数,则引导读者查看SQLite源代码直接编码。如果你理解本文中的内容,你应该不会有太大的困难,在遵循来源。认真学习SQLite内部的学生可能会还想仔细研究VDBE操作码如文件所述在这里。大多数操作码文档是从源代码中的注释中提取的使用脚本编写代码,以便您还可以获取有关直接从维德比。c(c)源文件。如果你已经成功阅读了这么多,你应该很少难以理解其余部分。

如果您在文档或代码中发现错误,请随时修改和/或联系作者drh@hwaci.com。您的错误修复或欢迎提出建议。

此页面上次修改时间2022-01-08 05:02:57联合技术公司