项目仓库:https://github.com/uya-lang/nosqliteNoSQLite
是一个 Uya-native 的嵌入式文档数据库。它想做的,不是再包一层 JSON API,也不是做一个“看起来像数据库”的玩具项目,而是认真把一个小数据库该有的骨架搭起来:WAL、checkpoint、双 meta 页、checksum、主键 B+Tree、SQL parser/binder/planner/executor、快照读、typed SQL、格式兼容与升级边界。
Please optimize the content of the above text and return the result.
在 nosqlite-v1.5.0 这个里程碑版本封板时,NoSQLite 已经足够“硬”:功能闭环成立,Definition of Done 能跑通,稳定性压力门也有测试和文档证据。可问题也很明显,功能做完不等于产品体验到位。一个数据库,最终还是要回到最朴素的感受上:查得够不够快,写得够不够稳,重启恢复够不够轻。
所以,v1.5.0 之后的性能优化,并不是“锦上添花”,而是一次真正意义上的第二阶段工程。
最开始,NoSQLite 先做的不是优化代码,而是补一把尺子:把 SQLite JSON1 拉进来做同机横向 benchmark。原因很简单,没有参照系,性能讨论很容易变成自我感动;有了参照系,优化才有方向感。
这一步其实很残酷。最初那版数据并不漂亮:warm read、primary lookup、seq scan、durable insert、recovery open,几乎都还在毫秒级;和成熟 C 实现的 SQLite 比,差距甚至是数量级的。可也正因为这样,后面的每一步改进,才都不是“感觉变快了”,而是可以被 benchmark 证明、被提交历史追溯的工程推进。
第一阶段,NoSQLite 先把查询热路径压薄。
此前每次查询,都会重复经过 parser、binder、planner,再走 executor,这对通用 SQL 能力是对的,但对 warm read 来说代价太高。于是后续提交开始围绕“把重复工作拿掉”展开:查询计划缓存、轻量级 collection metadata 绑定、结果集分配池化、行数据按需 materialize,而不是一上来全量加载。
这一步的意义,不只是让数字下降,更重要的是让系统从“整体迟钝”变成“局部可优化”。primary lookup 从接近 20ms 掉到大约 4.5ms,虽然还远谈不上胜利,但至少已经把问题从“大框架太重”收敛到“热路径还不够短”。
第二阶段,优化开始真正咬住 warm lookup 本身。
既然主键点查已经有 B+Tree,就不该每次都重新付出高昂代价。于是后面的优化集中在几件事上:warm read planning、B+Tree reuse、fast accessor、direct row slot 验证,以及 benchmark 口径中的 warmup 预热。看起来都是细节,但数据库性能真正的分水岭,往往就是这些细节。
结果也很直接。NoSQLite 的 primary lookup p50 被压到 1us,seq scan p50 压到 2us 到 3us。这时的对比已经不再是“少输一点”,而是开始在 warm read 口径下追平甚至反超 SQLite JSON1。同机 benchmark 里,primary lookup 从 SQLite 更快 x6627,一路追到 NoSQLite 更快 x3.00;seq scan 也从明显落后,变成 NoSQLite 更快 x2.00。
这不是单一技巧带来的奇迹,而是把系统里每一层多余的成本都剥掉之后的结果。性能优化真正有意思的地方就在这儿:它不像加功能那样“多了一个模块”,它更像是把原来藏在系统内部的阻力,一点一点擦掉。
第三阶段,则是把读路径上的经验延伸到写入、恢复和并发读写边界。
一个数据库不能只在“理想查询”里快。如果 durable insert 还很慢、recovery open 还很重、长读者一存在写者就明显发抖,那这个系统的实际使用感还是不成立。于是后续优化继续深入到 pager 和 WAL 提交链路:复用 pager session,减少反复 open/close;提交时走 batch 和 open files;live commit 跳过恢复场景才需要的旧页 LSN 重读;再配合 CRC32、JSON parser、page 级部分读取等细节调整,把写路径和恢复路径也一起收紧。
到当前 HEAD 的 benchmark 数据,变化已经非常明显:
-
primary_lookup:NoSQLite 1us,SQLite 3us
-
seq_scan_filter:NoSQLite 2us,SQLite 4us
-
durable_insert:NoSQLite 61us,SQLite 58us
-
recovery_open:NoSQLite 91us,SQLite 103us
-
long_query_concurrent_commit:NoSQLite 45us,SQLite 70us
也就是说,在 warm read、recovery open、长查询并发提交这些关键路径上,NoSQLite 已经能在当前原型 benchmark 口径下跑出非常有竞争力的结果;而 durable insert 也已经和 SQLite 进入同一量级,不再是之前那种“看得见的代差”。
更重要的是,这条性能路线没有用“牺牲安全边界”去换数字。
NoSQLite 后续提交里,一边压热路径,一边也补了对应测试:lazy reopen 后继续插入不能丢旧数据,cursor snapshot 仍要能 materialize 出旧 blob,WAL recovery 在特定场景下仍要 replay matching tail。也就是说,这一轮优化并不是“为了 benchmark 重写一条捷径”,而是在原有数据库语义和 durability 约束之内,把系统变快。
这点很关键。因为数据库项目最怕的,不是慢,而是为了快把正确性偷偷让出去。NoSQLite 这段提交历史最值钱的地方,不是几个漂亮数字,而是它体现出来的一种工程取向:先把系统做硬,再把系统做快;快的前提,是不丢底线。
当然,NoSQLite 也没有把自己包装成一个“已经可以全面替代 SQLite”的神话。仓库文档写得很坦诚:当前还是 v0/v1 原型口径,collection 容量仍受单页布局限制,benchmark 数据集也还是缩小样本。这些边界没有被回避,反而被明确写进了 benchmark 报告和 README 里。
也正因为这种坦诚,这条优化路径才更有说服力。它不是靠模糊口径讲故事,而是在明确边界的前提下,拿出一条非常清晰的曲线:从毫秒级,到百微秒级,再到微秒级;从“功能成立”,到“性能开始有质感”;从“一个能跑的小数据库”,到“一个值得继续往下打磨的数据库内核”。
如果你对小型数据库内核、嵌入式存储引擎、Uya 生态或者“如何把一个原型系统一步步磨出性能感觉”这类题目感兴趣,NoSQLite 是一个很值得看的仓库。因为它不只是给出结果,也把过程留下来了。你可以直接沿着 nosqlite-v1.5.0 之后的提交一路看下去,看到一个数据库项目如何从封板、对标、暴露短板,到一点点把热路径抠出来。
想直接看代码、benchmark 和验收脚本,可以访问项目仓库:
https://github.com/uya-lang/nosqlite