使用 SQLite 嵌入 SQL 数据库

作者:Michael Owens

SQLite 是一个强大的嵌入式关系数据库管理系统,它是一个紧凑的 C 库,由 D. Richard Hipp 开发。它为 SQL92 的大型子集、多表和索引、事务、视图、触发器以及大量的客户端接口和驱动程序提供支持。该库是自包含的,并且用不到 25,000 行 ANSI C 代码实现,可以免费用于任何目的。它快速、高效且可扩展,并且可以在从 ARM/Linux 到 SPARC/Solaris 的各种平台和硬件架构上运行。此外,它的数据库格式在具有不同字节顺序的机器之间是二进制兼容的,并且可以扩展到 2 太字节(241 字节)的大小。

Hipp 在与通用动力公司的一个团队合作开发一个用于美国海军 DDG 级驱逐舰的程序时,构思了 SQLite 的想法。该程序在 HP-UX 上运行,并使用 Informix 数据库。当他们开始这个项目时,他们很快发现 Informix 相当难用。一旦启动并运行,它运行良好,但经验丰富的 DBA 可能需要整整一天的时间才能安装或升级它。

当时,该团队正在使用 Linux 和 PostgreSQL 进行开发工作。PostgreSQL 需要的管理工作少得多,但他们仍然希望能够生成一个可以在任何地方运行的独立程序,而不管主机平台上安装了什么其他软件。2000 年 1 月,Hipp 和一位同事讨论了编写一个简单的嵌入式 SQL 数据库引擎的想法,该引擎将使用 GDBM 作为其后端,并且不需要任何安装或管理支持。后来,在资金中断期间,Hipp 开始独自编写它,SQLite 1.0 版本很快就诞生了。

通用动力公司立即开始使用 SQLite 代替 PostgreSQL。SQLite 允许他们生成一个独立的、可执行文件,可以快速且轻松地安装在可穿戴计算机和笔记本电脑上,以便在贸易展览和销售会议上展示。Informix 仍然用于舰载操作;然而,负责该程序持续维护的海军办公室最近一直在给 Hipp 发送电子邮件,寻求帮助在 HP 9000 上编译 SQLite。因此,情况可能正在发生变化。

重大变化发生在 2.0 版本中。1.0 版本使用 GDBM 进行存储,GDBM 使用无序键(又名哈希)。这限制了 SQLite 可以做的事情。此外,GDBM 是在 GPL 下发布的,这让一些人对其尝试感到犹豫。2001 年 1 月,Hipp 开始着手开发自己的基于 B*Tree 的后端,以取代 GDBM。新的 B*Tree 子系统按键顺序存储记录,这允许进行优化,例如对数时间 MIN() 和 MAX() 函数以及具有不等式约束的索引查询。它还支持事务。最终结果是一个功能更强大的数据库。2.0 版本于 2001 年 9 月发布到公共领域。

SQLite 从 2.0 版本开始流行起来。数十人开始写信告诉他们如何在商业和免费产品中使用它。甚至有人与 Hipp 签订合同以获得技术支持或自定义修改。据 Hipp 称,至少有一个广泛使用的 Windows 程序在其最新版本中合并了修改后的 SQLite 版本。SQLite 也被 Duke Energy 以及美国军方的其他部门使用,除了最初启发它的海军项目之外。

自从一年半前公开发布以来,SQLite 一直在快速地增加功能和用户。快速浏览 SQLite Wiki 就会发现更多应用程序,它们的开发人员发现了 SQLite 并将其用于他们的软件中。它甚至有自己的 Apache 模块 (mod_auth_sqlite),这似乎是其自身成功的标志。作为 PySQLite(SQLite 的 Python 扩展)的创建者,Gerhard Häring 和我对不到一年内超过 3,000 次的下载量感到惊讶。目前,SQLite 是 <@url>freshmeat.net 上评分最高的数据库引擎。

架构

SQLite 具有优雅的模块化设计。它可以分为八个主要子系统(图 1),其中一些子系统采用了相当有趣的 relational 数据库管理方法。

Embedding an SQL Database with SQLite

图 1. SQLite 架构

在图表的顶部是解析器和词法分析器。SQLite 包括其自身高度优化的解析器生成器,称为 Lemon 解析器,它可以生成快速、高效的代码,并且凭借其新颖的设计,它尤其能抵抗内存泄漏。底部是一个基于 Knuth 的优化 B 树实现,它运行在可调页缓存之上,有助于最大限度地减少磁盘寻道。页缓存反过来又在操作系统抽象层上运行,这有助于使库更具可移植性。

库的核心是虚拟数据库引擎。VDBE 执行与数据操作相关的所有操作,并且是信息在客户端和存储之间传递的经纪人。在许多方面,它是 SQLite 的核心。VDBE 在 SQL 解析后开始发挥作用。代码生成器获取解析树并将其转换为一个小程序,该小程序由一系列以 VDBE 的虚拟机语言表示的指令组成。VDBE 逐个执行每个指令,最终完成 SQL 语句中指定的任何请求。

VDBE 的机器语言由 128 个操作码组成,所有操作码都围绕数据库管理。有用于打开表、搜索索引、存储和删除记录以及许多其他数据库操作的操作码。VDBE 中的每个指令都由一个操作码和最多三个操作数组成。有些指令使用所有三个操作数;有些指令不使用任何操作数。这一切都取决于指令的性质。例如,Open 指令(它在表上打开游标)使用所有三个操作数。第一个操作数 (P1) 包含游标将通过其标识的 ID。第二个操作数 (P2) 指的是表的根(或第一个)页面的位置,第三个操作数是表的名称。另一方面,Rollback 指令根本不需要操作数。VDBE 为了执行回滚而需要知道的唯一事情是是否要执行回滚。

对于任何给定的 SQL 语句,您可以使用 SQLite shell 中的 explain 命令查看生成的 VDBE 程序。清单 1 显示了一个简单的示例。

清单 1. explain 简单查询的结果

explain 不仅对于更好地了解 VDBE 的工作原理很有用,而且对于查询优化等实际问题也很有用。VDBE 本身确实是一个主题。幸运的是,对于那些感兴趣的人来说,它有很好的文档记录,并且其操作理论及其操作码在 SQLite 网站上都有非常详细的介绍。

关于物理存储,每个数据库都存储在单个文件中。也就是说,构成单个数据库的所有数据库对象(视图、触发器、索引、表、模式等等)都驻留在定义 SQLite 数据库的一个文件中。数据库文件由大小均匀的页面组成。页面大小在数据库创建时设置,有效大小范围为 512b-4Gb。默认情况下,SQLite 使用 1Kb 的页面大小,这似乎提供了最佳的整体性能。

事务是使用第二个文件(称为日志)实现的,该文件仅在数据库有一个或多个活动连接时存在。每个数据库恰好有一个日志文件。它保存了在事务过程中更改的原始(未修改的)页面。当事务提交时,日志页面不再需要,并被立即丢弃。回滚是通过将页面从日志文件恢复到数据库文件来执行的。日志文件的使用确保了数据库始终可以在崩溃中幸存下来并恢复到一致的状态。崩溃后第一个连接到数据库的客户端会触发先前事务的回滚。具体来说,当客户端连接时,SQLite 尝试创建一个新的日志文件,但发现以前的文件已存在。当这种情况发生时,它推断必须发生了崩溃,并继续将旧日志文件的内容复制回数据库,有效地将其恢复到崩溃前的原始状态。然后,它允许客户端开始工作。

简单的 API,多种语言

SQLite 具有极其易于使用的 API,只需要三个函数即可执行 SQL 和检索数据。它是可扩展的,允许程序员以 C 回调的形式定义自定义函数和聚合。C API 是脚本接口的基础,其中一个(Tcl 接口)包含在发行版中。开源社区开发了大量其他客户端接口、适配器和驱动程序,使得可以在其他语言和库中使用 SQLite。

使用 C API 只需要三个步骤。基本上,您调用 sqlite_open() 来连接到数据库,在其中您提供文件名和访问模式。然后,您实现一个回调函数,SQLite 为其从数据库检索的每个记录调用该函数。接下来,调用 sqlite_exec(),提供一个包含您要执行的 SQL 的字符串和一个指向您的回调函数的指针。除了检查错误代码之外,就这些了。清单 2 说明了一个基本示例。

清单 2. 基本 C API 示例

与其他数据库客户端库不同,此模型的优点之一是回调函数。与您等待结果集的其他客户端 API 不同,SQLite 将您直接置于结果收集过程的中间,在事情发生时处于核心位置。因此,您在获取数据和直接影响检索过程方面发挥着更积极的作用。您可以聚合数据,也可以在需要时中止记录检索。重点是,由于数据库是嵌入式的,您的应用程序本质上既是服务器又是客户端,SQLite 通过使用其回调接口充分利用了这一点。

除了标准的 C API 之外,扩展 API 使获取记录更加容易,它使用 sqlite_get_table(),这不需要回调函数。此函数的行为更像传统的客户端库,它接受 SQL 并返回一个行集。扩展 API 的一些功能是扩展 SQL 的函数,通过添加您自己的函数和聚合,这将在本文后面讨论。

最后,如果出于某种原因您需要 ODBC 接口,我很高兴地通知您,Christian Werner 编写了一个可用的接口。他的 ODBC 驱动程序可以在 www.ch-werner.de/sqliteodbc 找到。

自动递增列

虽然 SQLite 本身不支持序列,但它确实具有自动递增键和 MySQL 等效的 mysql_insert_id()。主键可以通过将其声明为 INTEGER PRIMARY KEY 来设置为自动递增。可以通过调用 sqlite_last_insert_rowid() 获取该字段的最后插入记录的值。

BLOB

您可以在 SQLite 列中存储二进制数据,但限制是它只能存储到第一个 NULL 字符。为了存储二进制数据,您必须首先对其进行编码。一种可能性是 URL 样式编码;另一种是 base64。如果您没有特别的偏好,SQLite 通过两个实用函数使您的生活变得轻松:sqlite_encode_binary() 和 sqlite_decode_binary()。

线程安全

SQLite 的线程安全性与您一样。答案或多或少围绕 sqlite_open() 返回的 SQLite 连接句柄展开。这是不应该在执行上下文之间共享的;每个线程都应该获得自己的句柄。如果您仍然希望线程共享它,请使用互斥锁保护它。同样,连接句柄不应在 UNIX fork() 调用之间共享。这更多的是常识而不是其他任何东西。底线:线程或进程,获取您自己的连接句柄,一切都应该没问题。

SQLite 使用 pragma 的概念来控制运行时行为。Pragma 是使用 SQL 语法设置的参数。有一些用于性能调优的 pragma,例如设置缓存大小以及是否使用同步写入。有些用于调试,例如跟踪解析器和 VDBE,还有一些用于控制传递给客户端回调函数的信息量。一些 pragma 具有控制其范围的选项,一个变体仅持续当前会话,另一个变体永久生效。

当且仅当列声明为 BLOB、CHAR、CLOB 或 TEXT 类型时,SQLite 才按字典顺序对列进行排序。否则,它按数字排序。SQLite 曾经完全根据列的值来决定如何对列进行排序。如果它“看起来”像一个数字,那么它就按数字排序,否则按字典顺序排序。邮件列表中出现了大量关于此的讨论,最终将其改进为今天使用的规则,这些规则允许您通过模式中声明的类型来控制比较方法。

脚本接口

如前所述,已经为 SQLite 开发了许多客户端接口。为了让您体验一下,清单 3 中说明了先前 C 示例的 Python 版本,清单 4 中显示了其 Perl 版本。这再简单不过了。SQLite 也可以从 shell 中使用,这使得它适合于系统管理任务。清单 5 中提供了我们的股票示例的 shell 版本。

清单 3. Python 示例

清单 4. Perl 示例

清单 5. Shell 示例

最后,因为我不是 Java、Tcl、Ruby、Delphi、Lua、Objective C、PHP、Visual Basic、.NET、Mono、DBExpress、wxWindows、Euphoria 或 REXX 程序员,我将不得不将像您这样的人推荐到 SQLite Wiki,以找到您各自的接口。有关与 SQLite 对话的首选方式,请参阅 cvs.hwaci.com:2080/sqlite/wiki?p=SqliteWrappers

扩展 SQLite

SQLite 包含一个很好的 C 框架,您可以在其中创建自己的函数和聚合,这些函数和聚合可以从 SQL 中调用。一些包装器(例如 Python 包装器)允许您使用此功能以扩展语言实现它们。SQL,例如 INSERT INTO orders purchase_date values CURRENT_TIME(),只需编写一个回调函数,该函数类似于清单 6。然后,注册该函数并按清单 7 所示使用它。

清单 6. CURRENT_TIME() 的实现

清单 7. 使用 CURRENT_TIME()

SQLite 的所有内置函数(例如 avg()、min()、max() 和 sum()),除了神奇的 typeof() 之外,都是使用此 API 实现的。用户定义的聚合可以同样容易地添加。执行类似于 SELECT variance(age) from population 的操作使用与创建函数非常相似的方法。然而,这留给读者作为练习。提示:文件 func.c 包含一些很好的例子。与函数一样,SQLite 也使用 API 来实现其聚合。

管理

对于管理,SQLite 提供了一个直观的实用程序程序,方便地命名为 sqlite,MySQL 和 PostgreSQL 的用户会感到非常熟悉。它具有 shell 和命令行模式。在 shell 中,您可以查看数据库的表、模式和索引,以及在命令行和外部文件中执行 SQL。它还具有一些用于查看数据和 VDBE 输出的不错模式。

虽然可以在 shell 中完成加载和卸载数据,但在命令行上甚至更容易。给定一个包含有效 DDL/DML 的文件(称之为 dump.sql),您可以将其加载到数据库中(称之为 db),如下所示

sqlite db < dump.sql

如果数据库 db 不存在,这将创建它。转储数据库的反向过程将是

sqlite db .dump > dump.sql
SQLite 功能强大。其广泛的应用、易用性、可移植性、速度、可扩展性、小巧的体积和简洁的代码库使其成为所有程序员都应该拥有的库。鉴于其许可,简直没有理由不使用它。SQLite 项目一直在寻找新的用户和开发人员,并且欢迎新的想法和引人入胜的讨论。我希望您像我一样喜欢学习和使用它。

资源

Embedding an SQL Database with SQLite
Michael Owens (mike@mikesclutter.com) 是一位从化学工程师转行成为程序员的人。他在德克萨斯州达拉斯/沃斯堡的一家房地产公司工作,使用 Linux 和开源软件开发内部软件。他是 PySQLite 的创建者和共同开发者。
加载 Disqus 评论