SQLite字节码引擎(十二)

返回:SQLite—系列文章目录   

上一篇:SQLite的架构(十一)

下一篇:SQLite 4.9的 OS 接口或“VFS”(十三)  

1、 摘要

SQLite 的工作原理是将 SQL 语句转换为字节码和 然后在虚拟机中运行该字节码。本文档 描述字节码引擎的工作原理。

本文档介绍SQLite内部结构。所提供的信息 使用 SQLite 进行常规应用程序开发不需要此处。 本文档适用于想要更深入地研究的人 SQLite的内部操作。

字节码引擎不是 SQLite 的 API。详 关于字节码引擎从一个版本到下一个版本的更改。 使用 SQLite 的应用程序不应依赖于任何细节 在本文档中找到。

2. 引言

SQLite 的工作原理是将每个 SQL 语句转换为字节码和 然后运行该字节码。 SQLite中的预处理语句大多只是所需的字节码 实现相应的 SQL。sqlite3_prepare_v2() 接口 是将 SQL 转换为字节码的编译器。 sqlite3_step() 接口是运行 准备好的语句中包含的字节码。

字节码虚拟机是 SQLite 的核心。 想要了解SQLite内部如何运作的程序员 必须熟悉字节码引擎。

从历史上看,SQLite 中的字节码引擎称为 “虚拟数据库引擎”或“VDBE”。本网站使用这些条款 “字节码引擎”、“VDBE”、“虚拟机”和“字节码虚拟” 机器“可以互换,因为它们的意思都是一样的。

本文还使用了术语“字节码程序”和 “准备好的语句”可以互换,因为它们大多是一回事。

2.1. VDBE源代码

字节码引擎的源代码位于 vdbe.c 源代码中 文件。本文档中的操作码定义是派生的 来自该源文件中的注释。这 源代码注释是规范的信息来源 关于字节码引擎。如有疑问,请参阅源代码。

除了主 vdbe.c 源代码文件外,还有 源代码树中的其他帮助程序代码文件,其名称均为 以“vdbe”开头 - “Virtual DataBase Engine”的缩写。

请记住,操作码的名称和含义经常从 一个版本的 SQLite 到下一个版本。因此,如果您正在研究 SQLite 的 EXPLAIN 输出,则应参考本文档的版本 (或 vdbe.c 源代码) 对应于运行 EXPLAIN 的 SQLite 版本。 否则,操作码的描述可能不准确。 本文档源自 SQLite 版本 3.45.2 入住日期为 2024-03-12 的 D8CD6D49B46A3

2.2. 指令格式

SQLite中的字节编码程序由一条或多条指令组成。 每条指令都有一个操作码和 五个操作数,分别命名为 P1、P2、P3、P4 和 P5。P1、P2 和 P3 操作数是 32 位有符号整数。这些操作数通常是指 寄存 器。对于在 b 树游标上操作的指令, P1 操作数通常是游标编号。 对于跳转指令,P2 通常是跳转目的地。 P4 可以是 32 位有符号整数、64 位有符号整数、A 64 位浮点值、字符串文本、Blob 文本、 指向排序序列比较函数的指针,或 指向应用程序定义的 SQL 实现的指针 功能,或其他各种东西。P5 是一个 16 位无符号整数 通常用于举旗。P5 标志的位有时会影响 操作码以微妙的方式。例如,如果 P5 操作数的 SQLITE_NULLEQ (0x0080) 位 在 Eq 操作码上设置,则 NULL 值比较 彼此相等。否则,NULL 值会比较不同 彼此之间。

某些操作码使用所有五个操作数。某些操作码使用 一两个。某些操作码不使用任何操作数。

字节码引擎在指令编号 0 上开始执行。 执行将继续进行,直到看到 Halt 指令,或者直到 程序计数器大于 最后一条指令,或直到出现错误。 当字节码引擎停止时,所有内存 它分配的已释放,并且它可能释放所有数据库游标 已经开放了,已经关闭了。如果执行由于 错误,则终止任何待处理事务并进行更改 回滚到数据库。

ResultRow 操作码导致 字节码引擎暂停,相应的 sqlite3_step() 调用返回SQLITE_ROW。在调用 ResultRow 之前,字节编码程序将 已将查询的单行结果加载到序列中 的寄存器。C 语言 API(如 sqlite3_column_int() 或 sqlite3_column_text() )从中提取查询结果 寄存 器。字节码引擎在下一条指令中恢复 在下一次调用的 ResultRow 之后 更改为 sqlite3_step()。

2.3. 寄存器

每个字节码程序都有一个固定的(但可能很大)的 寄存 器。单个寄存器可以保存各种对象:

  • 一个 NULL 值
  • 带符号的 64 位整数
  • IEEE 双精度(64 位)浮点数
  • 任意长度的字符串
  • 任意长度的 BLOB
  • RowSet 对象(请参阅 RowSetAddRowSetRead 和 RowSetTest 操作码)
  • A Frame 对象(由子程序使用 - 参见 Program)

寄存器也可以是“未定义的”,这意味着它没有价值 完全。Undefined 不同于 NULL。取决于编译时间 选项,尝试读取未定义的寄存器通常会导致 运行时错误。如果代码生成器 (sqlite3_prepare_v2()) ever 生成一个准备好的语句,读取一个未定义的寄存器, 这是代码生成器中的一个错误。

寄存器的编号以 0 开头。 大多数操作码至少引用一个寄存器。

单个预处理语句中的寄存器数是固定的 在编译时。当以下情况下,所有寄存器的内容都会被清除 准备好的语句将被重置定稿

内部 Mem 对象存储单个寄存器的值。 API 中公开的抽象sqlite3_value对象实际上是 只是一个 Mem 对象或寄存器。

2.4. B树光标

准备好的语句可以有 零个或多个打开的游标。每个游标都由 小整数,通常是操作码的 P1 参数 使用光标。 可以在同一索引或表上打开多个游标。 所有游标都独立运行,甚至光标指向同一 索引或表。 虚拟机与数据库交互的唯一方式 文件通过光标。 虚拟机中的说明可以创建新游标 (例如:OpenRead 或 OpenWrite), 从游标()读取数据, 将光标移动到表中的下一个条目 (例如:下一个上一个),依此类推。 所有光标都是自动的 当准备好的语句被重置定稿时关闭。

2.5. 子程序、协程和子程序

字节码引擎没有用于存储返回地址的堆栈 子例程。退货地址必须存储在寄存器中。 因此,字节码子例程不是可重入的。

Gosub 操作码将当前程序计数器存储到 然后,寄存器 P1 跳转到地址 P2。返回操作码跳转 以解决 P1+1。因此,每个子例程都与两个整数相关联: 子例程中入口点的地址和寄存器编号 用于保存退货地址。

Yield 操作码将程序计数器的值交换为 寄存器 P1 中的整数值。此操作码用于实现 协程。协程通常用于实现来自 根据需要提取哪些内容。

触发器需要可重入。 由于字节码 子例程不可重入,必须使用不同的机制来 实现触发器。每个触发器都使用单独的字节码实现 程序具有自己的操作码、程序计数器和寄存器集。程序操作码调用触发器子程序。程序说明 为每次调用分配并初始化一个新的寄存器集 子程序,因此子程序可以是可重入的和递归的。子程序使用 Param 操作码来访问寄存器中的内容 调用字节码程序。

2.6. 自更改代码

某些操作码是自动更改的。 例如,Init 操作码(始终是第一个操作码 在每个字节码程序中)递增其 P1 操作数。后续 Once 操作码将其 P1 操作数与 Init 操作码,以确定一次性初始化 应跳过以下代码。 另一个例子是 String8 操作码,它转换其 P4 操作数从 UTF-8 转换为正确的数据库字符串编码,然后 将自身转换为 String 操作码。

3. 查看字节码

SQLite 解释的每个 SQL 语句都会生成一个程序 对于虚拟机。但是,如果 SQL 语句以 关键字 EXPLAIN 虚拟机不会执行 程序。相反,程序的指令将被返回, 每行一条指令, 就像查询结果一样。此功能对于调试和 用于了解虚拟机的运行方式。例如:

sqlite3 ex1.db
sqlite> explain delete from tbl1 where two<20;
addr  opcode         p1    p2    p3    p4             p5  comment      
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     12    0                    00  Start at 12  
1     Null           0     1     0                    00  r[1]=NULL    
2     OpenWrite      0     2     0     3              00  root=2 iDb=0; tbl1
3     Rewind         0     10    0                    00               
4       Column         0     1     2                    00  r[2]=tbl1.two
5       Ge             3     9     2     (BINARY)       51  if r[2]>=r[3] goto 9
6       Rowid          0     4     0                    00  r[4]=rowid   
7       Once           0     8     0                    00               
8       Delete         0     1     0     tbl1           02               
9     Next           0     4     0                    01               
10    Noop           0     0     0                    00               
11    Halt           0     0     0                    00               
12    Transaction    0     1     1     0              01  usesStmtJournal=0
13    TableLock      0     2     1     tbl1           00  iDb=0 root=2 write=1
14    Integer        20    3     0                    00  r[3]=20      
15    Goto           0     1     0                    00

任何应用程序都可以运行 EXPLAIN 查询来获取类似于 以上。 但是,不会生成显示循环结构的缩进 通过SQLite核心。命令行 shell 包含额外的逻辑 用于缩进循环。 此外,EXPLAIN 输出中的“comment”列 仅当使用 -DSQLITE_ENABLE_EXPLAIN_COMMENTS 选项编译 SQLite 时才提供。

当使用 SQLITE_DEBUG 编译时选项编译 SQLite 时, 额外的 PRAGMA 命令可用于调试和 探索VDBE的运作。例如,可以启用vdbe_trace编译指示,以使每个 VDBE 操作码的反汇编为 在执行操作码时打印在标准输出上。这些调试 编译指示包括:

4. 操作码

目前有 189 个 虚拟机定义的操作码。 下表描述了所有当前定义的操作码。 此表是通过扫描源代码自动生成的 从文件 vdbe.c.

切记:VDBE 操作码不是接口的一部分 SQLite 的定义。操作码的数量及其名称和含义 从SQLite的一个版本更改为下一个版本。 下表中显示的操作码对 SQLite 有效 版本 3.45.2 入住日期为 2024-03-12 的 D8CD6D49B46A3

操作码名称 描述
可中止 验证是否可以发生中止。断言此时是否中止可能会导致数据库损坏。此操作码仅在调试中出现 建立。

如果没有写入,或者有写入,则中止是安全的主动声明日记。

将寄存器 P1 中的值与寄存器 P2 中的值相加并将结果存储在寄存器P3中。 如果任一输入为NULL,则结果为NULL。
添加 Imm 将常量P2添加到寄存器P1中的值中。 结果始终为整数。

要强制任何寄存器为整数,只需添加 0。

亲和力 将亲和力应用于从P1 开始的一系列P2 寄存器。

P4是一个长度为 P2 字符的字符串。的第N个字符 string 指示应用于第N个的列关联存储单元在范围内。

AggFinal(英语:AggFinal P1是作为聚合累加器的内存位置 或窗口函数。执行终结器函数 将结果存储在 P1 中。

P2是步进函数采用的参数数,并且P4是指向此函数的FuncDef 的指针。The P2 此操作码不使用参数。它只是为了消除歧义 可以采用不同数量的参数的函数。这 P4 参数仅在以下情况下需要以前未调用 step 函数。

AggInverse 执行聚合的xInverse函数。 该函数具有P5参数。P4是指向指定函数的FuncDef结构。寄存器P3是蓄电池。

P5参数取自寄存器P2及其接班人。

Agg步骤 执行聚合的xStep函数。 该函数具有P5参数。P4是指向指定函数的FuncDef结构。寄存器P3是蓄电池。

P5参数取自寄存器P2及其接班人。

Agg步骤1 执行 xStep (if P1==0)或 xInverse(if P1!=0) 函数 骨料。该函数具有P5参数。P4 是指向 指定函数的 FuncDef 结构。寄存器 P3 是 蓄电池。

P5 参数取自寄存器 P2 及其 接班人。

此操作码最初编码为 OP_AggStep0。在第一次评估时, 存储在 P4 中的 FuncDef 被转换为sqlite3_context和 操作码已更改。这样,初始化的 sqlite3_context只发生一次,而不是在每次调用 step 函数。

Agg值 调用 xValue()函数并将结果存储在寄存器 P3 中。

P2是步进函数采用的参数数,并且P4是指向此函数的FuncDef的指针。The P2此操作码不使用参数。它只是为了消除歧义可以采用不同数量的参数的函数。这 P4 参数仅在以下情况下需要以前未调用step 函数。

取寄存器 P1和 P2中值的逻辑AND,并取将结果写入寄存器 P3。

如果 P1 或 P2 为 0(false),则结果为 0,即使另一个输入为 NULL。一个NULL和 true或两个NULL给出一个 NULL 输出。

自动提交 将数据库自动提交标志设置为 P1(1 或 0)。如果 P2为true,则滚动返回任何当前处于活动状态的 btree 事务。如果有任何活动 VM(除了这个),则ROLLBACK失败。如果出现以下情况,则 COMMIT 失败有使用共享缓存的活动写入VM或活动 VM。

此指令会导致 VM 停止。

开始Subrtn 标记可内联输入的子例程的开头或者可以使用 Gosub 调用。子例程应由具有 P1 操作数的 Return 指令终止与此操作码的P2操作数相同,并且P3设置为1。 如果子例程是内联输入的,则 Return 将简单地失败。但是,如果使用 Gosub 输入子例程,则返回将跳回 Gosub 之后的第一条指令。

此例程的工作原理是将 NULL加载到P2寄存器中。当返回地址寄存器包含一个 NULL,返回指令为 一个简单地落入下一个指令的无操作(假设返回操作码的 P3 值为1)。因此,如果子程序是内联输入,则 Return 将导致内联执行继续。但是,如果子例程是通过 Gosub 输入的,则 Return 将导致返回到 Gosub 之后的地址。

此操作码与 Null 相同。它有一个不同的名称 只是为了使字节码更易于阅读和验证。

比特和 取寄存器 P1和 P2中值的按位 AND,并取将结果存储在寄存器 P3 中。 如果任一输入为 NULL,则结果为 NULL。
比特不是 将寄存器P1的内容解释为整数。存储将P1值的1补码转换为寄存器 P2。如果 P1 成立 一个 NULL,然后在 P2 中存储一个 NULL。
BitOr 取寄存器 P1 和 P2 中值的按位 OR 并 将结果存储在寄存器 P3 中。 如果任一输入为 NULL,则结果为 NULL。
斑点 P4 指向一个 P1 字节长的数据块。存储此内容 寄存器 P2 中的 blob。如果 P4 是 NULL 指针,则构造 在 P2 中长度为 P1 字节的零填充 Blob。
强制寄存器 P1 中的值为 P2 定义的类型。

  • P2=='A' → BLOB
  • P2=='B' →文本
  • P2=='C' → 数字
  • P2=='D' → INTEGER
  • P2=='E' → 实数

此例程不会更改 NULL 值。它保持 NULL。

检查站 检查点数据库 P1.如果 P1 当前不在,则这是无操作的 WAL 模式。参数 P2 是 SQLITE_CHECKPOINT_PASSIVE、FULL、 重新启动或截断。如果检查点返回,则将 1 或 0 写入 mem[P3] 分别SQLITE_BUSY与否。在 WAL检查点后进入mem[P3+1]和页数 在检查点之后被检查的 WAL 中 完成到 mem[P3+2]。但是,在错误时,mem[P3+1] 和 mem[P3+2] 初始化为 -1。
清楚 删除其根页的数据库表或索引的所有内容 在数据库文件中由 P1 给出。但是,与 Destroy 不同的是,不要 从数据库文件中删除表或索引。

如果 P2==0,则要清除的表位于主数据库文件中。如果 P2==1,则要清除的表位于辅助数据库文件中 用于存储使用 CREATE TEMPORARY TABLE 创建的表。

如果 P3 值不为零,则行更改计数递增 按要清除的表中的行数。如果 P3 大于 比零,则存储在寄存器 P3 中的值也递增 按要清除的表中的行数。

Смотритетакже

关闭 关闭之前以 P1 身份打开的光标。如果 P1 不是 目前开放,此指令是无操作的。
ClrSubtype 从寄存器 P1 中清除子类型。
CollSeq(英语:CollSeq) P4 是指向 CollSeq 对象的指针。如果下次调用用户函数 或聚合调用 sqlite3GetFuncCollSeq(),此排序规则序列将 被退回。这由内置的 min()、max() 和 nullif() 使用 功能。

如果 P1 不为零,则它是一个寄存器,后续 min() 或 如果当前行不是最小值或 max() aggregate 将设置为 1 最大。通过此指令,P1寄存器初始化为0。

实现上述函数时使用的接口 检索此操作码设置的排序规则序列不可用 公然地。只有内置函数才能访问此功能。

将游标 P1 指向的数据解释为使用 MakeRecord 指令。(有关其他操作码,请参阅 MakeRecord 操作码 有关数据格式的信息。提取 P2 列 从这个记录。如果小于 (P2+1) 值,提取 NULL。

提取的值存储在寄存器 P3 中。

如果记录包含的字段少于 P2,则提取 NULL。或 如果 P4 参数是 P4_MEM则使用 P4 参数的值作为 结果。

如果在 P5 中设置了 OPFLAG_LENGTHARG 位,则保证结果 仅由 length() 函数或等效函数使用。内容 不加载大型 Blob,从而节省 CPU 周期。如果 设置OPFLAG_TYPEOFARG位ÿ

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

界忆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值