当前位置: 首页
数据库
SQLite或推出Rust风格版本

SQLite或推出Rust风格版本

热心网友 时间:2026-07-16
转载

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/

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
生产环境MySQL为何不建议使用过多触发器

生产环境MySQL为何不建议使用过多触发器

生产环境触发器过多易引发长事务、死锁和主从延迟,且问题隐蔽,慢SQL日志无法定位触发器内部语句。高并发下触发器逐行同步执行,串行化写入链路,性能急剧下降。替代方案如应用层封装、Canal异步落库等更可靠。

时间:2026-07-18 20:11
Redis集群SSL/TLS加密传输配置方法

Redis集群SSL/TLS加密传输配置方法

为Redis集群启用TLS,所有节点必须统一开启,版本、证书、配置缺一不可。Redis6及以上版本需编译支持TLS,且OpenSSL版本不低于1 1 1,所有节点共用同一CA证书文件。配置需设置tls-cert-file、tls-key-file等五项参数,并禁用明文端口。客户端连接时需指定CA证书,证书更新需逐节点滚动重启。

时间:2026-07-18 20:11
MySQL两阶段提交协调机制详解

MySQL两阶段提交协调机制详解

MySQL的提交通过两阶段提交实现:准备阶段将重做日志标记为准备并刷盘,提交阶段二进制日志写入成功后才通知InnoDB引擎更新日志状态。崩溃恢复时比对事务标识,两者一致才提交,否则回滚。配置参数不当会破坏一致性。

时间:2026-07-18 20:10
MySQL中DELETE比TRUNCATE更易产生锁等待的原因

MySQL中DELETE比TRUNCATE更易产生锁等待的原因

MySQL中DELETE属DML操作,逐行加行级锁并写undo日志,事务提交前持续持有锁,易引发锁等待;TRUNCATE属DDL操作,仅瞬时持表级锁并隐式提交,锁等待近乎为零,但存在不可回滚、自增重置、触发器失效及外键拒绝等语义差异。

时间:2026-07-18 20:10
Spring Boot导出Excel时SQL排序注入点修复方法

Spring Boot导出Excel时SQL排序注入点修复方法

在SpringBoot导出Excel中,SQL排序注入因ORDERBY无法参数化而高发,唯一可靠防护是白名单硬编码校验字段名与排序方向,别名也需独立白名单,拒绝用户输入直接拼接,确保所有导出入口均经过统一校验。

时间:2026-07-18 20:10
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜