最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
SQLite 应当有(Rust 风格)版本
时间:2026-07-16 17:43:49 编辑:袖梨 来源:一聚教程网




SQLite 是一个令人惊叹的数据库引擎。我将它用作大量嵌入式项目的数据库,我认为将其称为本地数据存储的行业标准并不夸张。有些服务器软件甚至使用它;例如,lobste.rs 现在在 SQLite 上运行。
与传统的 RDBMS(关系数据库管理系统)不同,SQLite 不是一个单独的进程;它是一个作为库的 RDBMS,这意味着您的软件仍然是独立的。与传统文件格式不同,您不需要编写自定义序列化器和解析器。从某些方面来说,这是两全其美的。
但只有一个大问题。它的默认值都是错误的。
错误的默认值#1:默认情况下忽略外键约束
你没看错。外键约束可以说是我们确保数据库保持一致并且没有悬空引用的主要工具。
作为快速入门,SQL 外键约束如下所示:
所有其他 RDBMS 的典型行为是帖子的 user_id 列必须始终引用有效用户的 ID。如果不提供有效的用户 ID,则无法创建新帖子;如果不删除用户的帖子,则无法删除用户,以免出现外键约束违规错误。
据我所知,默认情况下唯一不强制执行此操作的 RDBMS 是 SQLite。
SQLite 重用 ROWID 的倾向使情况变得更糟。您会看到,在此示例中,那些 INTEGER PRIMARY KEY 行成为表 ROWID 的别名,ROWID 是分配给 SQLite 中表的每一行的唯一整数 ID。分配 ROWID 的算法有点复杂(更多详细信息请参阅 SQLite 文档),但在某些情况下会导致 ID 重用。这意味着悬空引用很容易导致对错误列的引用,这比悬空引用更糟糕,因为一切看起来都很好。您甚至在查找过程中都不会收到错误。
只需看看我们的玩具数据库模式中假设的操作序列:
修复方法是使用编译指示启用foreign_keys:
如果我们一开始就这样做,那么有缺陷的 DELETE 就会产生错误:
错误的默认值#2:列可以存储错误的数据类型
SQLite 有一个简单的类型系统:值可以是 NULL 、 INTEGER 、 REAL (又名双精度浮点数)、 TEXT 或 BLOB (又名二进制数据)。因此,可以定义列来保存任何这些类型的值。
但是,定义为 INTEGER 列的列不仅限于整数;还可以是整数。相反,SQLite 认为它“使用 INTEGER 亲和力”。这本质上意味着:
- 如果您尝试插入 TEXT 值,并且它是整数的有效字符串表示形式,则会将其转换为整数并按原样存储。
- 如果您尝试插入 TEXT 值,并且它是实数的有效字符串表示形式,则会将其转换为实数(也称为双精度浮点数)并按原样存储。
- 否则,该值将按原样存储。
其他相似性有不同但更简单的规则:
- 具有 BLOB 关联性的列按原样存储值。
- 具有 TEXT 亲和性的列按原样存储 BLOB 、 TEXT 和 NULL 值,但将数值转换为 TEXT 。
- 具有 REAL 亲和力的列的工作方式与具有 INTEGER 亲和力的列类似,只是整数值会转换为 REAL 。
这在实践中是这样的:
我认为我不需要解释为什么数据库对数据验证如此粗心是一个坏主意。如果 SQLite 是一个显式动态类型文档数据库,那是一回事,但事实并非如此。 SQLite 通过其语法规则询问我,“您想在该列中输入什么类型”。
我曾经不得不清理一个项目,其中一些代码意外地将字符串“1”和“0”写入旨在存储布尔值(1和0)的列。这不是一个有趣的调试故事。
幸运的是,SQLite 有严格表的概念,这使得 SQLite 在将错误类型插入列时会产生类型错误:
不幸的是,没有编译指示可以在全局范围内使所有表变得严格。所以你必须记住手动为每个表添加严格标签。
有一些反对严格表格的论点,我想在这里介绍一下。
SQLite 的作者写过他们对“灵活类型”的偏好。就我个人而言,我觉得这是一篇非常奇怪的文章。它没有提供任何示例来说明为什么将 BLOB 插入到 INTEGER 列中会很有用。它所做的只是说明为什么有时拥有一个可以存储任何类型值的列很有用。严格表有一个解决方案;它称为 ANY 数据类型。您仍然可以创建接受任何值的列,只需明确说明即可。
lobste.rs 上的用户“zie”提供了更好的论点。您会看到,SQLite 中的严格表不仅仅强制执行类型。它们还更改了类型说明符的解析规则。
非严格 SQLite 表使用以下规则来确定列的类型(来自 SQLite 的文档):
- 如果声明的类型包含字符串“INT”,则为它分配 INTEGER 关联性。
- 如果列的声明类型包含任何字符串“CHAR”、“CLOB”或“TEXT”,则该列具有 TEXT 关联性。请注意,类型 VARCHAR 包含字符串“CHAR”,因此被分配了 TEXT 关联性。
- 如果列的声明类型包含字符串“BLOB”或者未指定类型,则该列具有亲和性 BLOB。
- 如果列的声明类型包含任何字符串“REAL”、“FLOA”或“DOUB”,则该列具有 REAL 关联性。
- 否则,关联性为 NUMERIC。
此规则与 SQLite 的松散类型相结合的结果是,您可以为列提供类型名称,例如 DATETIME 或 KEY_VALUE_SET 或 COLOR ,并拥有一个数据库连接器/包装器,它自动知道如何序列化和反序列化具有自定义类型的列。如果不出意外的话,这些自定义类型名称可以作为有用的文档。
我必须承认,只要将默认值从非严格表更改为严格表,而不进行进一步更改,就会放弃这种有点俏皮的怪癖。但是,我认为自定义类型别名会为我们提供更好的服务。
如果我们可以写这样的东西:
然后在严格的表中使用 KEY_VALUE_SET 作为类型名称,我想每个人都会很高兴。我可能会开始大量使用这样的功能来记录我的专栏中预期的数据模式。在现实世界的模式中,您不可避免地会遇到必须由应用程序代码解析的 TEXT 列。
顺便说一句,如果我们可以将 CHECK 约束与自定义类型关联起来,那就太好了。
更新:lobste.rs 上的“masklinn”指出 SQL 99 标准已经指定了类型别名,称为 CREATE DOMAIN 。这也已经支持约束。所以实际上,SQLite 只需要添加对标准 CREATE DOMAIN 语句的支持。
错误的默认值 #3:并发写入器出现 SQLITE_BUSY 错误
SQLite 允许多个并发读取器,但一次只能有一个写入器。默认情况下,如果有两个进程尝试同时获取写锁,其中一个进程将立即收到 SQLITE_BUSY 错误。
这不是我期望的行为。我希望 SQLite 等待锁解锁,直到超时。毕竟它正在执行磁盘 IO,因此我已经假设写入可能会很慢来构建代码。
默认行为导致我编写了现实世界的错误,系统有时会崩溃。我已经手动编写了重试循环来修复它。
解决方法是使用编译指示设置 busy_timeout:
这使得 SQLite 在出现 SQLITE_BUSY 错误之前尝试获取锁长达 5 秒。
我直到最近才了解到这个设置。这似乎是一个显而易见的默认设置,令我惊讶的是事实并非如此。
更新:我应该在这里添加一条注释,说明为什么需要支持并发编写器。
在正常操作期间,通常最好的方法是构建软件,使所有写入都由单个进程(最好是单个线程)完成。并发作家永远不会快。但也有不典型的情况。也许您需要在命令行上使用 sqlite3 工具以交互方式手动清理数据库。也许您有用于不常见管理任务的脚本,但您不需要为其编写前端。我认为这些都是完全合法的,而且是相当常见的用例。我认为使用 SQLite 的默认设置很糟糕,这种使用有可能引发意外的 SQLITE_BUSY 错误,从而导致软件崩溃。
糟糕的默认值 4:性能
关于 SQLite 中的性能调优有很多话要说。如果配置正确,它可以成为真正快速的 RDBMS,能够填补我们通常为 PostgreSQL 或 MySQL 等大型服务器保留的角色。
但默认情况下,它的性能并不好。比我聪明的人在这方面写了很多文章,如果您对此主题感兴趣,我推荐 Sylvain Kerkour 的 Optimizing SQLite forservers。
但最严重的默认错误是 SQLite 的预写日志 (WAL) 默认被禁用。可以通过以下方式启用它:
在大多数情况下,WAL 可以显着提高写入速度。此外,它还允许我们通过更改其他设置来大幅减少磁盘同步量,而无需冒数据损坏的风险:
请参阅 SQLite 文档,了解同步的具体作用。
解决方案:版本?
为什么这些默认值仍然是默认值,经常被引用的原因当然是向后兼容性。现在更改默认值可能会破坏许多旧软件,并使人们害怕将来升级 SQLite,以防它再次破坏所有内容,就像我害怕升级 Python 一样,因为每次“升级”都会破坏我使用的一堆软件。尽量不让你的家属受伤,这是一个值得称赞且罕见的目标。
然而,我认为解决方案很简单:添加一个“超级编译指示”来更改所有错误的默认值。我建议如下:
应该是至少以下一组编译指示的别名:
并且还将严格模式设置为表的默认模式。
这应该是一个很好的中间立场,可以避免破坏向后兼容性,但可以让数据库引擎向前发展,而不会被自己的历史所困扰。
版本的想法是直接从 Rust 版本中窃取的。基于年份的版本比 JavaScript 的“use strict”的优势;随着时间的推移,合理的默认值可能会发生变化。也许像 Hctree 的 WAL2 这样的东西会在 2034 年进入主分支,所以也许 PRAGMA Edition = 2034 有一天会设置 PRAGMA Journal_mode = WAL2 。
无论如何,仅此而已。我认为 SQLite 应该有一个包含更新的默认值集的版本系统。我是否遗漏了任何事情,导致这是一个坏主意?或者更多的实用建议应该添加到我想象的“2026 版本”中?
相关文章
- 深夜自动贩卖机巷 07-16
- 东京天际线下的两位晚礼服女士 07-16
- 日本女性泳池畔肖像 07-16
- 无痛单词怎么更改单词背景 无痛单词更改单词背景方法 07-16
- 福昕PDF编辑器怎样设置PDF页眉页脚 07-16
- Visio营销图表绘制技巧 07-16