小巧、快速、可靠。
请选择三个。
为什么 SQLite 使用字节码

1. 简介

每个 SQL 数据库引擎的工作原理大致相同:它首先将输入的 SQL 文本转换为“预处理语句”。然后它“执行”预处理语句以生成结果。

预处理语句是一个对象,它表示完成输入 SQL 所需的步骤。或者,换句话说,预处理语句是 SQL 语句转换为计算机更容易理解的形式。

在 SQLite 中,预处理语句是 sqlite3_stmt 对象 的实例。在其他系统中,预处理语句通常是一个内部数据结构,应用程序程序员无法直接访问。其他 SQL 数据库引擎的开发人员不一定将这些对象称为“预处理语句”。但是,无论它们叫什么,这样的对象都存在。本文将使用术语“预处理语句”。

有无数种方法可以实现预处理语句。本文将探讨两种最常见的方法

  1. 字节码 → 输入 SQL 被转换为虚拟机语言,然后由虚拟机解释器运行。这是 SQLite 使用的技术。

  2. 对象树 → 输入 SQL 被转换为表示要执行的处理的对象树。通过遍历这棵树来执行 SQL。这是 MySQL 和 PostgreSQL 使用的技术。

这些预处理语句表示形式各有优缺点。本文的目的是阐述一些优缺点。

1.1. 如何提供反馈

本文是从 SQLite 的原始作者的角度撰写的。如果您不同意本文中提出的任何观点,欢迎您在 SQLite 论坛 上提出更正和/或相反的观点。您也可以直接向作者发送电子邮件。

1.2. “字节码”的定义

SQLite 生成的 字节码 可能与许多读者认为的字节码略有不同。例如,Java 虚拟机WebAssembly 使用的字节码几乎完全由低级操作组成,类似于物理 CPU 的实现:基本数学运算符、比较、条件跳转以及在不同内存位置之间移动内容的指令。SQLite 字节码也包含这些低级指令。但是 SQLite 字节码还包含一些特定于数据库引擎需求的高级操作。以下只是一些示例

换句话说,SQLite 使用的“字节码”与其说是 CPU 指令集,不如说是要按特定顺序运行的一组数据库基本操作。

1.3. “抽象语法树”或“AST”的定义

抽象语法树”或 AST 是一种数据结构,它描述了某种形式语言中的程序或语句。在我们的上下文中,形式语言是 SQL。AST 通常实现为对象的树,其中每个对象代表整个 SQL 语句的一小部分。AST 通常来自形式语言的解析器。通常的技术是使用 LALR(1) 解析器。使用这种解析器,每个终结符都包含将成为 AST 叶子的元数据,每个非终结符都包含将成为整个 AST 的子分支的元数据。随着解析器“缩减”语法规则,会分配 AST 的新节点并连接到子节点。解析完成后,语法的起始符号将保存 AST 的根。

AST 是对象的树。但是 AST 并非预处理语句的合适形式。生成后,AST 需要先经过各种转换才能执行。需要解析符号。需要检查语义规则。需要应用优化,将输入的 SQL 语句转换为不同的形式,以便更快地执行。最后,需要将 AST 转换为更适合执行的备用表示形式。

有些人将用作 MySQL 和 PostgreSQL 的可执行形式的对象树称为 AST。这可能是对术语“AST”的误用,因为在对象树准备好执行时,它已经发生了如此大的变化,以至于它与原始 SQL 文本几乎没有相似之处。这种混淆部分源于原始预处理语句对象和原始 AST 都是对象的树。通常的技术是将直接来自解析器的原始 AST 逐个进行转换,经过多次传递,直到最后完全转换为不再严格为 AST 但可以计算以生成结果的对象树。在这个过程中,没有明确的点表明对象树何时停止是 AST 而变成了预处理语句。由于 AST 和预处理语句之间没有明确的界限,人们通常将以对象树形式表示的预处理语句称为“AST”,即使该描述并不精确。

1.4. 数据流编程

数据流编程 是一种编程风格,其中单个节点专门负责整个计算的一小部分。每个节点都接收来自其他节点的输入,并将输出发送到其他节点。因此,节点形成了一个有向图,将输入传递到输出。

“数据流程序”可能是比“AST”更好的描述,用于描述 SQL 数据库引擎用作预处理语句的对象树。

2. 编译成字节码的优势

SQLite 编译成字节码,SQLite 开发人员对此方法非常满意。以下是一些原因

2.1. 字节码更容易理解

可以轻松打印操作码的扁平列表,以准确查看 SQL 语句是如何实现的。当您在 SQL 语句前加上“EXPLAIN”关键字时,SQLite 中就会发生这种情况:该结果不是实际运行 SQL,而是会列出用于实现该 SQL 的字节码。

字节码适合这样做,因为字节码程序可以轻松地表示为表格。在 SQLite 字节码中,每个指令都有一个操作码和五个操作数。因此,可以将预处理语句呈现为对六列表的查询。

对象树表示形式更难以以人类可读的形式发布。构成树的对象往往大相径庭,因此很难找到一致且简单的表格表示形式来显示这些对象。任何您想出的表格表示形式几乎肯定会超过六列,可能更多。将对象树呈现为表格的问题非常难,以至于据我所知,没有人这样做。因此,就我所知,没有对象树数据库引擎提供与 SQLite 提供的“EXPLAIN”输出一样详细的信息。

2.2. 字节码更容易调试

字节码在 SQL 语句的前端解析和分析以及后端评估之间提供了清晰的界限。当出现问题(答案不正确和/或性能低下)时,开发人员可以检查字节码,以快速确定故障的来源是前端分析还是后端数据存储部分。

在 SQLite 的调试版本中,PRAGMA vdbe_trace=ON; 命令会导致字节码执行的跟踪信息出现在控制台上。

2.3. 字节码可以增量运行

用字节码编写的 SQL 语句可以增量评估。例如,可以运行一个语句,直到它只生成其第一行输出。然后,该语句会暂停,直到再次执行。不需要在检查第一行输出之前完成语句的运行。

在对象树设计中,这更难实现。当预处理语句是对象树时,执行通常通过遍历树来完成。要在计算的中间暂停语句,意味着将堆栈展开到调用方,同时保存足够的状态以从上次停止的地方恢复评估。这不是不可能做到,但非常困难,以至于我从未见过实际实现。

大多数 SQL 数据库引擎实际上并不需要增量执行预处理语句,因为大多数 SQL 数据库引擎都是客户端/服务器。在客户端/服务器引擎中,单个 SQL 语句被发送到服务器,然后完整的回复会一次性通过网络返回。因此,每个语句都一次性运行到完成。但 SQLite 不是客户端/服务器。SQLite 是一个库,它在与应用程序相同的地址空间中运行,并使用与应用程序相同的堆栈。能够轻松可靠地执行 SQL 语句的增量执行对于 SQLite 非常重要。

2.4. 字节码更小

SQLite 生成的字节码通常比来自解析器的相应 AST 更小。在对 SQL 文本进行初始处理(在调用 sqlite3_prepare() 和类似函数时),AST 和字节码同时存在于内存中,因此内存使用量会更高。但这只是一个短暂的状态。AST 会很快被丢弃,其内存也会被回收,甚至在 sqlite3_prepare() 函数返回之前,因此最终 预处理语句 在其字节码表示形式中消耗的内存比作为 AST 时更少。这很重要,因为对 sqlite3_prepare() 的调用是短暂的,但预处理语句通常会被缓存以备将来重用,并在内存中持续很长时间。

2.5. 字节码更快

相信一个准备好的语句的字节码表示运行得更快,因为在计算的每个步骤中需要做出的决定更少。对上一句中“相信”的强调→很难通过实验验证这一说法,因为没有人投入多年努力来生成准备好的语句的等效字节码和对象树表示,以查看哪一个实际上运行得更快。我们知道SQLite 非常快,但我们没有与其他 SQL 数据库进行很好的并排比较,因为其他数据库花费大量时间进行客户端/服务器消息处理,而且很难将消息往返开销与实际处理时间区分开。

3. 编译成对象树的优势

SQLite 开发人员认为字节码方法是最好的,至少对于 SQLite 试图解决的用例来说是最好的,但对象树方法处理 SQL 确实比字节码有一些优势。总是有权衡取舍。

3.1. 查询规划决策可以推迟到运行时

当一个准备好的语句是字节码时,一旦字节码生成,算法就固定了,无法在不完全重写字节码的情况下进行后续更改。对象树准备好的语句并非如此。对象树更容易动态修改。查询计划是可变的,并且可以在运行时根据查询的进度进行调整。因此,查询可以动态自我调整。

3.2. 数据流程序易于并行化

在数据流程序中,每个处理节点可以分配给不同的线程。需要某种线程安全的排队机制,用于将中间结果从一个节点传输到下一个节点。但通常不需要在程序的每个节点中使用同步原语。节点调度很简单:当节点有数据可用并且其输出队列中有空间时,它就变得可以运行。

对于旨在运行大型分析查询(OLAP)的大型多核服务器的数据库引擎来说,这是一个重要的考虑因素。SQLite 的主要关注点是物联网上的事务处理(OLTP),因此在 SQLite 中不太需要将准备好的语句表示为数据流程序。

此页面最后修改于 2024-05-09 17:38:03 UTC