一、背景
SQLite 是一个软件库 ,可将应用程序生成的高级磁盘 I/O 请求转换为可由操作系统执行的低级 I/O 操作。应用程序使用SQL 语言构造高级 I/O 请求 。SQLite 将每个高级 SQL 语句转换为许多低级 I/O 请求的序列(打开文件、从文件中读取几个字节、将几个字节写入文件等),这些请求完成请求的工作SQL。
应用程序可以通过直接调用操作系统 I/O 例程或使用键/值存储引擎(如 Berkeley DB或 RocksDB(仅举两个))来完成其所有磁盘 I/O。但是使用基于 SQL 语言的更高级别的接口也有一些优势。
SQL 是一种非常高级的语言。几行 SQL 可以替代数百或数千行过程代码。SQL 从而减少了开发和维护应用程序所需的工作量,从而有助于减少应用程序中的错误数量。
SQL 和 SQLite 是 事务性的。使用事务存储系统可以更轻松地推断应用程序的行为,并编写健壮的应用程序,即使面对软件错误、硬件故障或断电。
SQLite 通常 比直接低级 I/O 更快。这是违反直觉的。人们会预料到像 SQLite 这样的高级接口会施加运行时惩罚。而且,从理论上讲,这是正确的。但在实践中,基于 SQL 的系统(如 SQLite)进行了如此多的幕后优化,应用程序开发人员永远没有时间创建和维护,以至于基于 SQL 的系统最终提供了净性能增益。
1.1. SQLite 与大多数其他 SQL 数据库不同
除了 SQLite 之外,还有许多基于 SQL 的数据库管理系统可用。常见选项包括 MySQL、PostgreSQL 和 SQL-Server。所有这些系统都使用 SQL 语言与应用程序进行通信,就像 SQLite 一样。但是这些其他系统在重要方面不同于 SQLite。
SQLite 是无服务器的软件库,而其他系统是基于客户端-服务器的。对于 MySQL、PostgreSQL、SQL-Server 等,应用程序将包含一些 SQL 的消息发送到单独的服务器线程或进程。该单独的线程或进程执行请求的 I/O,然后将结果发送回应用程序。但是 SQLite 没有单独的线程或进程。SQLite 在与应用程序相同的地址空间中运行,使用相同的程序计数器和堆存储。SQLite 没有进程间通信(IPC)。当应用程序将 SQL 语句发送到 SQLite(通过调用适当的 SQLite 库子例程)时,SQLite 在与调用者相同的线程中解释 SQL。当 SQLite API 例程返回时,它不会留下任何独立于应用程序运行的后台任务。
SQLite 数据库是磁盘上的单个普通文件(具有明确定义的文件格式)。对于其他系统,“数据库”通常是隐藏在文件系统晦涩目录中的大量单独文件,甚至分布在多台机器上。但是对于 SQLite,一个完整的数据库只是一个普通的磁盘文件。
2. SQL 是一种编程语言
理解 SQL 数据库引擎如何工作的最好方法是将 SQL 视为一种编程语言,而不是一种“查询语言”。每个 SQL 语句都是一个单独的程序。应用程序构造 SQL 程序源文件并将它们发送到数据库引擎。数据库引擎将 SQL 源代码编译成可执行形式,运行该可执行文件,然后将结果发送回应用程序。
虽然 SQL 是一种编程语言,但它与 C、Javascript、Python 或 Go 等其他编程语言的不同之处在于,SQL 是一种 声明式语言 ,而其他语言是 命令式语言。这是一个重要的区别,它对用于将程序源文本翻译成可执行格式的编译器的设计有影响。但是,这些细节不应影响 SQL 实际上只是另一种编程语言这一事实。
2.1. 编程语言处理步骤
所有编程语言都分两步处理:
将程序源文本翻译成可执行格式。
运行在上一步中生成的可执行文件以执行所需的操作。
所有编程语言都使用这两个基本步骤。主要区别在于可执行格式。
C++ 和 Rust 等“编译型”语言将源文本翻译成可以由底层硬件直接执行的机器码。存在对 SQL 执行相同操作的 SQL 数据库系统 - 它们将每个 SQL 语句直接翻译成机器代码。但这种方法并不常见,也不是 SQLite 采用的方法。
Java、Perl、Python 和 TCL 等其他语言通常会将程序源文本翻译成字节码。然后这个字节码通过解释器运行,解释器读取字节码并执行所需的操作。SQLite 使用这种字节码方法。如果您在 SQLite 中的任何 SQL 语句之前使用“ EXPLAIN ”关键字,它将向您显示生成的字节码而不是运行字节码。
另一种方法是将程序源文本翻译成内存中的对象树。这棵树是“可执行文件”。解释通过遍历树来运行可执行文件。这是 MySQL、PostgreSQL 和 SQL-Server 使用的技术。
当然,并非每种语言都完全符合上述类别之一。这适用于 SQL 数据库引擎和更熟悉的命令式编程语言。Javascript 以使用混合执行模型而闻名,在该模型中,代码最初被编译成对象树,但可能会进一步翻译(使用即时编译)为更高效的字节码或机器代码,作为提升性能的一种方式表现。
可执行格式实际上最终只是一个实现细节。关键是所有语言都有一个将程序翻译成可执行格式的编译器步骤和一个执行编译程序的运行步骤。
2.2. 编译 SQLite 程序
当将 SQL 程序提交给 SQLite 时,第一步是将源文本拆分为“标记”。令牌可能是:
- 语言关键字,如“SELECT”或“UPDATE”。
- 表或列或变量的标识符。
- 标点符号,如“,”或“==”或“;”。
- 文字值:数字或字符串常量。
- 空格或注释。
空格和注释标记被丢弃。所有其他标记都被送入 LALR(1) 解析器,该解析器 分析输入程序的结构并为输入程序生成 抽象语法树 (AST) 。
解析器将 AST 转发给代码生成器。代码生成器是 SQLite 的核心,也是大部分魔法发生的地方。代码生成器解析 AST 中的符号名称——将输入 SQL 中的列名和表名与数据库的实际列名和表名相匹配。代码生成器还对 AST 进行各种转换以“优化”它。最后,代码生成器选择合适的算法来实现 AST 请求的操作,并构造字节码来执行这些操作。
代码生成器生成的字节码称为“准备好的语句”。将 SQL 源文本转换为准备好的语句类似于通过调用 gcc 或 clang 将 C++ 程序转换为机器代码。人类可读的源文本(SQL 或 C++)进入,机器可读的可执行文件(字节码或机器代码)出来。
3.延伸阅读
Atomic Commit文档描述 了SQLite 如何实现事务。
字节码引擎文档提供了有关 SQLite 使用的字节码格式以及如何查看和解释 SQLite 准备语句的更多信息 。
SQLite 查询规划 器和 下一代查询规划器文档进一步详细介绍了 SQLite 用于实现 SQL 语句的算法,以及它如何为每个单独的 SQL 语句选择合适的算法。