虚拟机?是的,但不是Java虚拟机。在本文中,我将重点为您提供对SQLite的“虚拟数据库引擎”或VDBE的基本了解。
我的“从SQLite挤压性能”系列主要是为Android工程师设计的,但这篇文章将特别深入到SQLite本身,讨论的主题对所有使用它的开发人员都适用。
我以前的印象是SQLite像解释器一样解析和运行语句,但事实并非如此。在寻找SQLite版本的MySQL的EXPLAIN时,我偶然发现了描述SQLite操作方式的文档:
SQLite的工作原理是将SQL语句转换为字节码,然后在虚拟机中运行该字节码。-SQLite。组织
SQLite虚拟机被称为“虚拟数据库引擎”,简称“VDBE”。
您可能已经知道SQLite中字节码程序更常见的说法是:“prepared statement”。此外,就像大多数程序一样:准备好的语句可以接受输入(?变量)。
字节码程序是一个二进制指令列表,每个指令由一个操作码和参数值组成。每个操作码对应于VDBE知道如何处理的特定命令,并且在处理时可以对虚拟机中寄存器组中包含的数据进行操作。根据官方文档,寄存器的数量是有限的,但可能相当大,这取决于SQLite在编译时的配置方式。
在本文的剩余部分中,我们将探索SQLite如何将SQL语句处理成字节码程序,然后我将向您展示如何检查语句编译成的字节码。
当您要求SQLite准备一条语句时,您精心编制的SQL将被分解(解析)、分析(计划查询),并分解(编译)为SQLite的VDBE能够执行的字节码程序。
就像任何编程语言一样,SQL从一堆文本开始。要从一个文本字符串中获取SQLite可以理解的内容,需要对该文本进行分解和理解。这就是我们所说的解析。
SQLite的解析方法在整个官方文档中都非常清楚。例如,如果你曾经在sqlite工作过。org在寻找如何编写INSERT语句时,您可能已经看到了一个图表,其中解释了SQLite解析器的部分工作原理:
像上面这样的图表是一种可视化SQLite理解的SQL语法的Backus Naur Form(BNF)描述的方法。当您知道要查找什么时,阅读语法图是一个非常简单的过程:
带小写字母和加高字母的椭圆代表可重用的语法子句(它们的语法图将在文档中的页面下方进一步列出)。
连接椭圆的箭头告诉您构造有效语句所需的关键字、子句和标记的出现顺序。
在将语句解析为其组成部分后,SQLite需要决定如何执行该语句。
对于任何给定的SQL语句,可能有数百、数千甚至数百万种不同的算法来执行该操作。所有这些算法都会得到正确的答案,尽管有些算法会比其他算法运行得更快。查询计划器是一种人工智能,它试图为每个SQL语句选择最快、最有效的算法。-SQLite。组织
理解SQLite如何决定执行语句的最佳方式是一个足够大的主题,足以证明它自己的观点是正确的。然而,对于本文来说,重要的是要知道在解析和编译之间有一个优化步骤。
最后:在SQLite确定了如何以最佳方式运行语句后,它会列出一系列描述整个操作的低级字节码指令。从字面上看,这个指令列表是一个将在VDBE上运行的程序。SQLite编译的程序的一个更流行的名称是“prepared statement”。
每个字节码指令由一个操作码(指令名)和最多5个参数(输入值或寄存器引用)组成。在现代SQLite版本中,有100多种不同类型的指令。它们在简单的控制流指令(如Eq:“如果两个寄存器具有相同的值,则跳转到一条指令”)和更特定于数据库的指令(如ResultRow:)之间运行,后者在当前位置向数据库光标提供数据,指向已加载到VDBE寄存器中的值。
一旦编译成字节码,一个准备好的语句就不需要被解析或再次执行查询计划过程。这正是为什么重复使用准备好的语句比不重复使用它们更快的原因。
现在您已经知道了原始语句是如何变成准备好的语句的,您可能想知道是否可以检查编译的字节码程序。
SQLite提供了一种机制,可以用来检查它为任何语句生成的字节码。为了查看字节码,只需在语句前面加上EXPLAIN。从文件中:
当EXPLAIN关键字出现时,它会使语句表现为一个查询,返回如果EXPLAIN关键字不存在,它将用于执行命令的虚拟机指令序列。-SQLite。组织
解释';s输出是一系列行,其中每行都是准备好的语句字节码中的一条指令,每行有8列:
最后一列:comment很可能是空的,除非您自己编译SQLite并设置-DSQLITE_ENABLE_EXPLAIN_COMMENTS选项。
让我们使用EXPLAIN来查看字节码中的一些语句。因为我的安装没有打开评论,所以我将不使用它。每个示例都从使用EXPLAIN时收到的输出开始,然后是VDBE在字节码程序中的流程介绍。
(注意:这里显示的所有字节码都来自SQLite 3.16.2,在其他版本中可能不相同。)
sqlite>;解释选择";你好,世界";;地址操作码p1 p2 p3 p4 p5--------------------------0初始0 1 0 0 0 1字符串8 0 1 0 hello world 00 2结果1 0 0 0 0 3暂停0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Init 0 1 0-表示程序的开始,并移动到地址1。
字符串8 0 1 0和#39;你好,世界';-“hello world”存储在寄存器1中。
ResultRow 1 1 0-让光标知道寄存器1包含一行输出。
sqlite>;解释创建表博客(标题文本不为空、作者文本不为空、发布日期整数、正文文本);地址操作码p1 p2 p3 p4 p5-------------------------0初始化0 27 0 0 0 0 0 0 0 0 1读Cookie 0 3 2 0 0 2如果3 5 0 0 0 0 0 3设置Cookie 0 2 4 0 0 4设置Cookie 0 5 1 0 0 5创建表0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 9插入0 3 1 0 8 10关闭0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 12空0 4 0 0 0 13打开写入1 0 5 0 0 0 0 0 0 0 0 0 0 0 014 SeeKrowid1 16 1 00 15 Rowid 1 5 0 0 0 16 IsNull 5 24 0 0 17 String8 0 6 0 table 00 18 String8 0 7 0 blog 00 19 String8 0 blog 00 20 Copy 2 9 0 00 21 String8 0 10 0 CREATE table blog(标题文本不为NULL,作者文本不为NULL,发布日期整数,正文)00 22 MakeRecord 6 11 BBBDB 00 23 Insert 1 11 5 00 24 SetCookie 0 1 00 25ParseSchema 0 tbl_name=';博客';还有打字。。。26暂停0 0 00 27事务处理0 1 0 0 01 28转到0 1 0 00
事务0 1 0 01-在当前数据库上启动写事务,并检查数据库架构是否为预期版本。
ReadCookie 0 3 2-从数据库中读取cookie#2(数据库格式),并将其值存储在寄存器3中。什么是“饼干”?Cookie是SQLite使用的实际数据库文件中存在的值。
如果3 5 0-如果寄存器3中的值不为零,则跳转到地址5。解释:如果已经配置了数据库格式(假设还没有),我们将跳过在数据库上设置一些cookie。
SetCookie 0 2 4-将4写入当前数据库的cookie编号2。这会将数据库文件格式设置为4。
SetCookie 0 5 1-将1写入当前数据库的cookie编号5。Cookie数字5表示数据库文件的文本编码格式。通过将其值设置为1,我们选择UTF-8。
CreateTable 0 2 0-创建一个新表,并将其位置放在寄存器2的数据库文件中。
OpenWrite 0 1 0 5-打开名为0的读/写游标,在根页为1的表上有5列。解释:第1页包含sqlite_主表,我们将向其中添加一条定义博客表的记录。
NewRowid 0 1 0-获取一个新的rowid值并将其放入寄存器1中。
Blob 6 3 0-有一个长度为6字节的空Blob,我们将存储在寄存器3中。
插入0 3 1 08-使用光标0,使用寄存器1中的值作为键将寄存器3中的数据写入表中,并增加表的行数。解释:我们正在将最近加载到寄存器3中的blob作为新行写入表中,并使用我们构造的rowid作为其键。
OpenWrite 1 10 5-打开名为1的读/写游标,在根页面为1的表上有5列。解释:将光标重新打开到sqlite_主表中。
请参阅克罗维德1 16 1-如果光标不包含寄存器1中值的rowid,请使用光标1跳转到地址16。否则继续前进。(让我们继续,因为我们刚刚对寄存器1中的值写入了一些内容)
Rowid 1 5 0-将光标1的Rowid值存储在寄存器5中。
IsNull 5 24 0-如果寄存器5中的值为NULL,则跳转到地址24。解释:如果由于某种原因,光标没有指向sqlite_主表中的一行,我们将跳转到程序的末尾退出。(假设该值不是NULL)
复制2 9 0-将值从寄存器2复制到寄存器9。解释:我们将在地址5的指令中创建的新表的数据库文件页位置从寄存器2复制到9,因为我们将在sqlite_主表的插入中使用它。
字符串8 0 10 0和#39;创建表格……”将CREATE TABLE语句存储到寄存器10中。
记录6 5 11和#39;BBBDB';-使用寄存器6到10(6+(5–1))创建一个表记录,并将对该记录的引用存储在寄存器11中。BBBDB字符串表示,记录中的前三列和最后一列的类型关联应为“blob”,第四列应为数字。解释:我们终于在sqlite_master中为我们的博客表构建了行。
插入1115-使用游标1,使用我们存储在寄存器5中的rowid作为其键,写入寄存器11指向的记录数据。
SetCookie 0 1-将架构cookie的值设置为1。模式cookie是cookie编号1,它表示数据库模式的当前版本。
语法模式0";tbl_name=';博客'" — 使用p4 param值作为WHERE子句解析sqlite_master中的所有模式条目。(这会产生另一个对VDBE的调用)
暂停0-终止程序,错误代码为0(成功!)。
最后,让我们看看向新创建的blog表添加一行需要什么。
sqlite>;解释在博客(标题、作者、发布日期、正文)中插入的价值观(';冬天来了';,';奈德·斯塔克';,日期(';现在';)'第七季播出'); 地址操作码p1 p2 p3 p4 p5--------------------------------------------------0初始0 12 0 00 1 OpenWrite 0 2 0 4 00 2 NewRowid 0 1 0 0 0 3 String8 0 0 2 0冬季即将到来00 4 String8 0 3 0 Ned Stark 0 0 0 0 5函数0 1 6 4日期(-1)01 6 String8 0 5它在第七季到来。00 7 HaltIfNull 12992博客。标题01 8 HaltIfNull 1299 2 3博客。作者01 9 MakeRecord 2 4 7 BBDB 00 10插入0 7 1博客1b 11暂停0 0 00 12事务0 1 0 01 13 String8 0 6 0 now 00 14 Goto 0 1 0 00
事务0 1 1 0 01-在当前数据库上启动写事务,并检查数据库架构是否为预期版本。记住:在CREATE TABLE字节码程序的地址24处,1被存储为模式版本;在本说明中,我们只是确保它仍然是1。
OpenWrite 0 2 0 4-打开一个名为0的读/写游标,在根位于第2页的表上有4列。记住:blog表是在数据库第2页上创建的,它有4列。
NewRowid 0 1 0-获取表的新rowid并将其存储在寄存器1中。
String8 0 2 0和#39;冬天来了';-商店';冬天来了';在寄存器2中。
Function0 1 6 4 date(-1)01-使用寄存器6处的值作为其唯一参数调用date函数,并将结果存储在寄存器4中。
弦8 0 5 0和#39;第七季播出' — 商店';第七季播出' 在登记册5中。
HaltIfNull 1299 2和#39;博客头衔和#39;01-如果寄存器2中的值为空,则以错误代码1299结束程序。解释:博客上的标题列被定义为NOTNULL,所以我们需要验证我们要存储的值是否为NOTNULL。如果是,我们将使用SQLITE_CONSTRAINT_NOTNULL代码出错。
HaltIfNull 1299 2 3和#39;博客作者';01-如果寄存器3中的值为空,则以错误代码1299结束程序。解释:与标题一样,作者栏也被定义为非空。
创纪录2 4 7';BBDB';-使用寄存器2到5(2+(4–1))创建一个表记录,并在寄存器7中存储对该记录的引用。BBDB字符串表示记录应该由两个blob、一个数字和一个blob(按顺序)组成。
插入0 7 1和#39;博客';1b-将寄存器7指向的记录以及寄存器1中的键插入blog表。p5的1b值是一个位掩码,表示VDBE应该计算更改的行数,并存储最新插入行的id以供以后访问。解读:这就是奈德·斯塔克的博客帖子被添加到表格中的地方。
停下,我们结束了!退出,错误代码为0(成功)。
如果你还和我在一起,干得好!如果您想了解更多关于SQLite的VDBE和字节码的信息,请尝试自己查看以下解释结果:
创建另一个表,向其中添加与第一个表中的数据相关的数据,并执行SELECT查询,将两个表连接在一起。