游乐游手机版
首页/数据库/文章详情

SQLite或推出Rust风格版本

时间:2026-07-16 17:50
SQLite默认值存在外键约束忽略、列类型宽松、并发写入器报错及性能不佳等问题,建议引入类似Rust的版本化机制,通过超级编译指示如PRAGMAEdition=2026一次性启用外键、忙超时、WAL日志及严格模式等合理默认值,既保持向后兼容又推动引擎进步。

SQLite 是一款令人惊叹的数据库引擎,被广泛用作嵌入式项目中的本地数据存储方案,可以说已经达到了行业标准级别。有些服务器软件甚至直接运行在 SQLite 之上——例如 lobste.rs 目前就在使用。与传统关系数据库管理系统(RDBMS)不同,SQLite 并非独立进程,而是以库形式嵌入的 RDBMS。这意味着你的应用依然保持干净独立,无需额外管理一个数据库服务进程。同时,相比传统文件格式,你也不必自行编写序列化器和解析器。从某些角度来看,这简直是两全其美的选择。

但有一个大问题——它的默认配置几乎全是陷阱。

错误的默认值 #1:默认情况下忽略外键约束

你没看错。外键约束是保证数据库数据一致性、避免悬空引用的核心工具。快速回顾一下,一个典型的外键约束定义如下:

所有其他 RDBMS 的默认行为是:帖子的 user_id 列必须始终引用一个有效的用户 ID。如果未提供有效的用户 ID,就无法创建新帖子;如果不先清理掉用户的帖子,就无法删除用户,否则会抛出外键违规错误。据我所知,唯一默认不强制这种约束的主流 RDBMS 就是 SQLite。

更糟糕的是,SQLite 倾向于重用 ROWID。在这个例子中,INTEGER PRIMARY KEY 的行实际上是表 ROWID 的别名——一个为每行分配的唯一整数 ID。ROWID 的分配算法略显复杂(详见 SQLite 文档),但在某些情况下会导致 ID 重用。这意味着悬空引用很容易变成引用到错误的列,这比单纯的悬空引用更危险:一切看起来都正常,甚至排查错误时也不会报错。

看看这个假设的 toy 数据库模式中的操作序列:

修复方法很简单,用 PRAGMA foreign_keys = ON 编译指示启用外键约束。如果一开始就这样做,上面那个有问题的 DELETE 操作就会直接报错。

错误的默认值 #2:列可以存储错误的数据类型

SQLite 的类型系统很简洁:值可以是 NULLINTEGERREAL(双精度浮点)、TEXTBLOB(二进制数据)。但令人头疼的是,定义了类型的列并非只能存储该类型,而是“带有某种亲和力”。这意味着什么?

  • 如果你插入一个 TEXT 值,并且这个字符串恰好是整数的有效表示,它就会被转换成整数存进去。
  • 如果是实数的有效表示,就转换成实数(双精度浮点)存进去。
  • 否则,原样存储。

其他亲和力的规则也各有不同:BLOB 亲和力原样存储;TEXT 亲和力原样存储 BLOBTEXTNULL,但会把数值转为 TEXTREAL 亲和力与 INTEGER 类似,只是整数值会转成 REAL

这在实践中的表现就是——一个定义为 INTEGER 的列,你居然可以塞进去一个字符串,只要它看起来像数字?数据库对数据验证如此粗心,显然是个坏主意。如果 SQLite 是一个显式动态类型的文档数据库,那倒也罢了,可它不是——它明明在语法层问“你想在这一列放什么类型”,结果却不严格校验。

曾经有一个项目需要清理,代码不小心把字符串 "1""0" 写进了本应存储布尔值(整数 1 和 0)的列里。这可不是一个有趣的调试故事。

好在 SQLite 后来引入了“严格表”(Strict Tables)的概念——当你试图插入错误类型时它会报错。但不幸的是,没有一个全局编译指示可以把所有表都变成严格模式,你必须在每个建表语句中手动加上 STRICT 标签。

关于严格表也有一些反对意见。SQLite 的作者写过一篇偏爱“灵活类型”的文章,说实话读起来挺奇怪的——它没有给出任何实际例子说明把 BLOB 塞进 INTEGER 列有啥用,只是强调某些场景下需要能存任意类型的列。严格表其实有解决方案:用 ANY 数据类型。你仍然可以创建接受任意值的列,只需显式声明即可。

lobste.rs 上的用户 zie 提供了一个更好的论点:严格表不仅仅强制类型,还改变了类型说明符的解析规则。非严格表使用以下规则确定列的亲和性(来自 SQLite 文档):

  1. 如果声明的类型包含字符串 INT,则分配 INTEGER 亲和力。
  2. 如果包含 CHARCLOBTEXT,则分配 TEXT 亲和力(比如 VARCHAR 包含 CHAR 所以是 TEXT)。
  3. 如果包含 BLOB 或未指定类型,则分配 BLOB 亲和力。
  4. 如果包含 REALFLOADOUB,则分配 REAL 亲和力。
  5. 否则,亲和力为 NUMERIC

这条规则加上松散类型的结果是:你可以给列起类似 DATETIMEKEY_VALUE_SETCOLOR 这样的“类型名称”,然后数据库连接器/包装器自动知道如何序列化和反序列化自定义类型。至少这些自定义类型名可以作为有用的文档。

必须承认,如果只是简单地把默认表从非严格改成严格,而没有任何额外改动,就会丢失这种有点讨喜的怪癖。但自定义类型别名可能是一个更好的方向——如果我们能写 KEY_VALUE_SET 并在严格表里直接用它,相信每个人都会很高兴。这个功能可以大量用来记录列中预期的数据模式,因为现实世界的模式里总有一些必须由应用代码解析的 TEXT 列。如果还能把 CHECK 约束和自定义类型关联起来,那就更完美了。

补充一点:lobste.rs 上的 masklinn 指出,SQL 99 标准其实已经规定了类型别名——CREATE DOMAIN,并且支持约束。SQLite 只需要增加对这一标准语句的支持即可。

错误的默认值 #3:并发写入器出现 SQLITE_BUSY 错误

SQLite 允许多个并发读取器,但一次只能有一个写入器。默认情况下,如果有两个进程同时尝试获取写锁,其中一个会立刻收到 SQLITE_BUSY 错误。这不是人们期望的行为——既然它在做磁盘 IO,代码本就应该假设写入可能慢一些。默认行为导致过真实世界的 bug:系统偶尔因并发写入崩溃,不得不用手动重试循环来修复。

解决方法是使用 PRAGMA busy_timeout = 5000 设置忙超时,这样 SQLite 在报 SQLITE_BUSY 之前会尝试最多 5 秒获取锁。这个设置直到最近才被广泛了解,而它本应是个明显的默认值。

需要补充一点:为什么需要支持并发写入器?正常操作下,最好的做法是让所有写入都由单个进程(最好是单线程)完成,并发写入永远不会快。但也有不典型的情况——比如你在命令行上用 sqlite3 工具交互式手动清理数据库,或者有一些不常见的管理任务脚本,但不需要专门写个前端。这些都是完全合法的常见用例。而 SQLite 的默认设置却很可能在这些场景下抛出意外 SQLITE_BUSY 错误,进而导致软件崩溃。

糟糕的默认值 #4:性能

SQLite 的性能调优大有文章可做。配置得当的话,它可以成为真正快速的 RDBMS,甚至能填补通常留给 PostgreSQL 或 MySQL 等大型服务器的角色。但它的默认性能并不好。对此话题感兴趣的读者可以参考 Sylvain Kerkour 的《Optimizing SQLite for Servers》。

最严重的默认问题在于:预写日志(WAL)默认是禁用的。通过 PRAGMA journal_mode = WAL 可以启用它。大多数情况下 WAL 能显著提升写入速度,并且还能通过调整 PRAGMA synchronous = NORMAL 之类的设置来大幅减少磁盘同步次数,同时不冒数据损坏的风险。

解决方案:版本化?

为什么这些默认值还是默认值?最常被引用的理由是向后兼容性。现在改变默认值可能会破坏大量旧软件,并让用户不敢升级 SQLite,担心又像 Python 升级那样一堆东西坏掉。尽量不让依赖者受伤,这是一个值得称赞且罕见的目标。

然而,一个合理的方案是:添加一个“超级编译指示”来一次性更改所有错误的默认值。比如:

PRAGMA Edition = 2026 应该成为至少以下一组编译指示的别名,并把严格模式设为表的默认模式:

  • PRAGMA foreign_keys = ON
  • PRAGMA busy_timeout = 5000
  • PRAGMA journal_mode = WAL
  • PRAGMA synchronous = NORMAL

并且默认表为严格模式。这应该是一个很好的中间立场:避免破坏向后兼容性,同时让数据库引擎能向前发展,不被自己的历史所牵绊。

版本化的想法直接借鉴自 Rust 的版本机制。基于年份的版本比 JavaScript 的 "use strict" 更有优势——随着时间推移,合理的默认值可能变化。也许像 Hctree 的 WAL2 这样的东西会在 2034 年进入主分支,那么 PRAGMA Edition = 2034 有一天就会设置 PRAGMA journal_mode = WAL2

总而言之,SQLite 应该有一个包含更新默认值集的版本系统。不知道是不是遗漏了什么导致这个想法不成立?或者是否还有更多实用建议应该加入这个想象中的“2026 版本”?

来源:https://mort.coffee/home/sqlite-editions/
上一篇Hive collect在数据挖掘中的适用性探讨 下一篇如何让克劳德不再说“承重”?乔拉开发者指南
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Navicat跨平台同步数据后主键自增值不一致原因
数据库 · 2026-07-17

Navicat跨平台同步数据后主键自增值不一致原因

Navicat跨平台同步后主键自增值不一致,源于结构同步默认将源库的auto_increment值硬编码进DDL脚本,忽略目标表当前最大ID。选项IncludeAUTO_INCREMENTvalue默认开启且不保存偏好,易导致冲突。跨平台环境更易暴露此问题。同步前需检查目标表最大ID,同步后调整自增值,同时注意MySQL8 0+的批量插入行为差异。

SQL基础语句实现大表随机抽取5条测试数据
数据库 · 2026-07-17

SQL基础语句实现大表随机抽取5条测试数据

在数据库日常开发中,从大表随机抽取测试数据时应避免使用ORDERBYRAND()导致全表扫描。MySQL可采用采样跳过法,PostgreSQL推荐TABLESAMPLE,SQLServer和SQLite宜基于主键范围生成随机ID。核心原则是利用索引定位,避免全表排序,从而大幅提升性能。

SQL同时分组聚合多个指标的实现方法
数据库 · 2026-07-17

SQL同时分组聚合多个指标的实现方法

在SQL查询中,使用GROUPBY子句可同时计算多个聚合函数,从而避免重复扫描表,提高性能。对于COUNT条件统计,须用CASEWHEN表达式。不同时间窗口的指标应将过滤逻辑下沉到聚合内部,还需注意空值处理,并确保结果正确。

分布式数据库环境下SQL存储过程适配方案
数据库 · 2026-07-17

分布式数据库环境下SQL存储过程适配方案

SQLServer存储过程直接迁移至分布式数据库不可行,需剥离单机语义,删除依赖会话状态、隐式建表及四部分命名等逻辑;环境标识通过参数显式传入;放弃分布式事务,采用本地消息表与异步补偿实现最终一致性;方言逻辑抽离为独立SQL文件按库加载。

SQL标量子查询实现自定义业务编码生成算法
数据库 · 2026-07-17

SQL标量子查询实现自定义业务编码生成算法

标量子查询生成业务编码需保证单值返回,日期前缀用CONVERT严格对齐,流水号补零用RIGHT或FORMAT,并发时加UPDLOCK和HOLDLOCK防重复,且不能在用户定义函数中调用GETDATE或子查询,替代方案为存储过程或传入日期参数。