同样是嵌入式存储引擎,SQLite、LevelDB和RocksDB的默认性能可能相差数倍,这种差距首先来自存储结构,而不是单纯的代码质量。很多性能对比只给出最终吞吐,却不说明同步策略、压缩算法或数据规模,导致结论难以复现。要把三者放在同一基准下比较,必须把写入路径、读放大和持久化开销拆开来看。

先说明测试条件:单机 SATA SSD、ext4 文件系统、8 核 16 线程 CPU、32GB 内存,数据量为 10 万条键值对,值大小固定为 128 字节。SQLite 先建表再在事务中批量插入,LevelDB 和 RocksDB 使用 WriteBatch 批量写入,并关闭同步写入,以观察不同引擎的写入吞吐上限。下面的脚本构造了三个写入函数,统一记录耗时,适合作为快速对比的起点。
import sqlite3
import time
import plyvel
import rocksdb
DATA_SIZE = 100_000
def timeit(name, fn):
start = time.perf_counter()
fn()
print(f"{name}: {time.perf_counter() - start:.3f}s")
def write_sqlite():
db = sqlite3.connect('test_sqlite.db')
cur = db.cursor()
cur.execute('CREATE TABLE IF NOT EXISTS kv(k TEXT PRIMARY KEY, v BLOB)')
cur.execute('BEGIN')
for i in range(DATA_SIZE):
cur.execute('INSERT OR REPLACE INTO kv VALUES(?, ?)', (f'key-{i}', b'x' * 128))
db.commit()
db.close()
def write_leveldb():
db = plyvel.DB('test_leveldb', create_if_missing=True)
wb = db.write_batch()
for i in range(DATA_SIZE):
wb.put(f'key-{i}'.encode(), b'x' * 128)
if i % 1000 == 999:
db.write(wb, sync=False)
wb = db.write_batch()
db.write(wb, sync=False)
db.close()
def write_rocksdb():
opts = rocksdb.Options()
opts.create_if_missing = True
db = rocksdb.DB('test_rocksdb', opts)
wb = rocksdb.WriteBatch()
for i in range(DATA_SIZE):
wb.put(f'key-{i}'.encode(), b'x' * 128)
if i % 1000 == 999:
db.write(wb, sync=False)
wb = rocksdb.WriteBatch()
db.write(wb, sync=False)
db.close()
if __name__ == '__main__':
timeit('SQLite写入', write_sqlite)
timeit('LevelDB写入', write_leveldb)
timeit('RocksDB写入', write_rocksdb)
在上述环境下,SQLite 的批量写入大约需要 2.8 到 3.2 秒,LevelDB 约 0.8 到 1.0 秒,RocksDB 约 0.6 到 0.8 秒。这个结果不代表 SQLite 不如后两者,而是因为它默认维护了更完整的 SQL 层和事务日志。
存储模型差异决定性能天花板
SQLite 使用 B 树作为主要存储结构,数据页按主键顺序组织,叶子节点保存记录。B 树的优势是单点查询路径短,通常只需要几次磁盘页访问就能定位到记录。缺点也很明显:随机插入会引发页分裂和大量随机写,尤其是当主键本身没有顺序时,写入放大会明显增加。
LevelDB 采用 LSM 树,写入先进入内存中的 MemTable,达到阈值后落盘为 SSTable。由于写入过程只是顺序追加日志,LevelDB 的写吞吐通常远高于 B 树。但 LSM 的读需要先查 MemTable,再逐层查 SSTable,读放大比 B 树高。LevelDB 设计精简,适合顺序写和较小规模的数据集。
RocksDB 继承了 LevelDB 的 LSM 架构,但把压缩策略、合并策略、布隆过滤器、列族管理都做成了可配置项。它可以通过更激进的分层压缩减少空间放大,也能通过不同 compaction 策略在写放大和读放大之间做权衡。因此 RocksDB 在默认配置下的写性能通常略好于 LevelDB,而在经过调优后差距还会更大。
写入与读取场景对比
从写入测试看,LSM 家族在批量写场景优势明显。SQLite 每次插入都涉及 B 树页的定位和可能的页分裂,即使使用事务将多个插入包在一起,仍不如顺序写日志高效。LevelDB 和 RocksDB 的 WriteBatch 可以把多个 Put 合并成一次日志追加和一次内存写入,所以 10 万条数据能在 1 秒内完成。
随机读场景则反过来。B 树的单点查询通常只需 2 到 4 次磁盘页访问,而 LSM 树需要逐层探测。LevelDB 默认的布隆过滤器配置较简单,如果读的键不存在,可能要扫描多层 SSTable;RocksDB 可以通过为每一层启用布隆过滤器来减少无效读。经验上,SQLite 在内存足够缓存索引页时,随机点读延迟可低至微秒级,LevelDB 和 RocksDB 在未命中的情况下通常更高。
范围扫描同样是 SQLite 的优势。B 树叶节点之间有链表,顺序遍历很高效。LSM 树的范围查询则需要在 MemTable 和多个 SSTable 之间做归并,虽然在 RocksDB 中可以通过合适的 compaction 和 bloom filter 优化,但底层复杂度仍然高于 B 树。这是很多事务型业务坚持用 SQLite 的原因之一。
同步、压缩和缓存如何改变结论
性能测试中最容易被忽略的是同步刷盘策略。SQLite 的 PRAGMA synchronous=FULL 会等待数据写入磁盘后才返回,导致写入变慢;改成 NORMAL 可以显著提升吞吐,但极端断电情况下可能丢失最近提交。LevelDB 和 RocksDB 的 WriteOptions.sync 同理,是否打开同步写会带来数量级差异。只比较默认配置,经常夸大了 LSM 库的优势。
压缩策略对 LSM 引擎影响很大。RocksDB 默认使用 Level Compaction,也可以切换为 Universal Compaction 或 FIFO Compaction。前者适合写入密集且可以接受一定空间放大的场景,后者更适合 TTL 数据或日志类数据。SQLite 没有 LSM 式压缩,但可以通过 VACUUM 回收页空间,运行时的空间放大通常更可预测。
缓存配置也会改变读性能。SQLite 的页缓存通过 PRAGMA cache_size 调整,LevelDB 的块缓存需要在打开数据库时指定。RocksDB 的 BlockBasedTableConfig 可以设置块缓存大小、布隆过滤器位数和索引类型。给 RocksDB 分配足够块缓存后,热点数据随机读性能可以接近内存哈希表,而默认配置下则明显偏低。
选型建议与混合方案
如果业务使用 SQL 查询、需要事务、外键或复杂过滤条件,SQLite 仍然是最合适的默认选择。单个数据库文件、无服务端依赖和完整 ACID 能力,让它在桌面端、移动端和小型服务中非常省心。不要在已经有 SQL 需求的项目里强行换用 KV 库,除非数据访问可以被简单地抽象为 get/put。
LevelDB 适合数据访问模式简单、写入为主、规模不大的场景。比如日志缓存、队列索引、本地元数据存储。它没有依赖,很适合嵌入到轻量级程序中。但如果数据量会持续增长,或者未来需要在线调整压缩和布隆过滤器,RocksDB 是更稳妥的选择。
RocksDB 适合高写入吞吐、大规模 KV 数据、对读延迟可调优的基础组件。不少分布式数据库和存储引擎用 RocksDB 作为单节点存储层,就是看中它在 SSD 上的写放大控制和丰富的参数。不过这也意味着需要投入更多精力做调优,否则默认配置未必比 LevelDB 好多少。混合方案也很常见:用 SQLite 保存业务表,用 RocksDB 保存大对象、日志或时序数据,两者各司其职。