Turso 深度拆解:当 SQLite 的 Rust 重写版决定「顺便说一口 Postgres」——从 VDBE 单核多前端到 BEGIN CONCURRENT,一个自称「数据库界 LLVM」的引擎如何重构嵌入式数据库的终极形态
两年前它叫 Limbo,是「用 Rust 重写 SQLite」的又一次玩票;今天它叫 Turso,GitHub 仓库简介写着一句很狂的话:
A SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.
2026-08-06,Turso 发布了 v0.8.0-pre.3。距离 1.0 还有一段路,但这个项目已经不是当初那个「Rust 版 SQLite」了。它做了一件在数据库圈里非常少见的事:把 SQLite 的 VDBE 字节码虚拟机抽象成通用后端,然后在上面挂了第二个前端——PostgreSQL。同一个进程内引擎,既能吃 SQLite 方言、复用 SQLite 文件格式和 C API,也能开一个 pgwire v3 端口,让 psql 直接连上来。
这篇文章不复述发布公告。我关心的是四件工程上真正硬的东西:
- 「一个 VM、多个 SQL 前端」这个类比 LLVM 的架构,到底是营销话术还是真的成立?
- Postgres 兼容层是怎么焊上去的,它的失败模式在哪里?
BEGIN CONCURRENT+ MVCC 怎么突破 SQLite 单写者的天花板,代价是什么?- Rust 重写真正的红利是不是「内存安全」——我的答案是不是,而是可失败的内存分配和异步 I/O。
以及最后:什么时候可以上生产,什么时候绝对不行。
一、背景:这已经是 Turso 团队第二次动 SQLite 了
要理解 Turso 现在的形态,得先理解他们踩过的坑。
第一次是 fork。 团队在 2023 年 fork 了 SQLite,做出 libSQL,加了远程复制、向量搜索这些现代特性,拿到一万多 star。fork 的好处很直接:可以持续从上游 merge,新特性白拿。
但 fork 有两个致命问题:
- SQLite 的测试套件是专有的。 SQLite 以「测试覆盖率地狱级」闻名,但那套 TH3 测试并不开源。你 fork 了代码,却没 fork 到信心。改一行 B-tree 代码,你不知道自己破坏了什么。
- C 语言的内存安全上限摆在那里。 想大改架构(比如引入 MVCC、异步 I/O),在一份三十年历史的 C 代码库里做,风险是指数级的。
于是有了第二次:完全重写。用 Rust,从零实现,保持 SQLite 兼容。这就是 Limbo,后来改名 Turso Database。
这里有个容易被忽略的判断:重写的动机不是「C 不安全」,而是「我不敢改」。这两件事是不一样的。前者是意识形态,后者是工程现实。当你没有上游那套测试套件、又必须做架构级改动时,重写反而是风险更低的路。
到今天,Turso 的功能清单已经相当长了(以下均来自仓库 README 与 manual,非营销材料):
已稳定的部分:
- SQLite 兼容:SQL 方言、文件格式、C API
BEGIN CONCURRENT:基于 MVCC 的并发写- CDC(变更数据捕获)
- 多语言绑定:Rust / Go / JavaScript / Java / .NET / Python / WebAssembly
- Linux 上的
io_uring异步 I/O - 跨平台:Linux、macOS、Windows、浏览器(WASM)
- 向量支持:精确搜索与向量运算
明确标注实验性的部分:
- Postgres 兼容:SQL 方言 + wire protocol
- 静态加密(encryption at rest)
- 基于 DBSP 的增量计算:增量视图维护、查询订阅
- 基于 tantivy 的全文检索
- 通过
.tshmsidecar 实现的多进程 WAL 协调
看这份清单,你会发现一件事:它早就不在「复刻 SQLite」的赛道上了。SQLite 不会给你 MVCC,不会给你 DBSP 增量视图,更不会给你 Postgres wire protocol。
二、核心概念:把 VDBE 当成数据库世界的 LLVM IR
2.1 SQLite 架构的那个「隐藏资产」
SQLite 的执行模型非常经典:
SQL 文本
→ Parser(生成 AST)
→ Code Generator(生成 VDBE 字节码)
→ VDBE(虚拟机,逐条执行 opcode)
→ B-tree / Pager / OS 接口
绝大多数人把 VDBE 当成一个实现细节。但换个视角看:VDBE 就是数据库领域的中间表示(IR)。
它有 IR 该有的一切特征:
- 指令集是稳定的、与前端语法解耦的(
OpenRead、SeekGE、Column、ResultRow、Next...) - 优化可以在生成 IR 之前(逻辑优化)或之后(peephole)做
- 执行引擎只认 IR,不认 SQL
Turso 的洞见就是这一句:既然 VDBE 是 IR,那前端为什么只能有一个?
2.2 LLVM 类比到底成不成立
LLVM 的模型是:
Clang (C/C++) ─┐
rustc ────┼→ LLVM IR → 优化 Pass → x86 / ARM / RISC-V / WASM
swiftc ────┘
Turso 的模型是:
SQLite 前端 ─┐
Postgres 前端 ┼→ Turso AST → VDBE 字节码 → VDBE 执行引擎 → Pager / MVCC / io_uring
(更多前端) ─┘
这个类比在结构上是成立的,而且比大多数「XX 界的 LLVM」要靠谱得多,因为它满足关键条件:IR 是先于这个想法存在的,而不是为了这个想法硬造的。VDBE 已经被 SQLite 验证了二十多年。
官方还有个很皮的证据:他们做了个 turso-vdbe-doom-example,在 VDBE 上跑 Doom。这当然是玩梗,但它证明的东西是实在的——这个虚拟机的指令集通用到能表达任意计算,而不只是关系代数。
2.3 但类比也有边界
我必须泼一盆冷水。LLVM 的成功建立在一个前提上:C、C++、Rust、Swift 的运行时语义高度重叠(都是指针、整数、栈、调用约定)。
数据库前端不是这样。Postgres 和 SQLite 的差异不在语法层,而在语义层和类型系统层:
| 维度 | SQLite | PostgreSQL |
|---|---|---|
| 类型系统 | 动态类型 + 亲和性(affinity) | 强静态类型,隐式转换规则严格 |
| 并发模型 | 单写者 + WAL | MVCC + 多种隔离级别 |
| NULL 与空串 | 区分,但比较规则宽松 | 严格三值逻辑 |
| 事务隔离 | 实质上是串行化 | RC / RR / Serializable 可选 |
| 扩展体系 | 虚拟表 / loadable extension | 完整的 extension + FDW + 后台 worker |
把 Postgres 的 SQL 压到一个为 SQLite 设计的 VDBE 上,必然会有语义损耗。问题不是「有没有损耗」,而是「损耗时会不会报错」。这直接引出下一节。
三、架构拆解:Postgres 前端是怎么焊上去的
Turso 的 postgres/ 目录结构非常清晰,四个组件:
| 组件 | 职责 |
|---|---|
postgres/parser | 用 pg_query(libpg_query,真正的 PostgreSQL 语法)解析 SQL,然后 translate 成 Turso AST |
postgres/frontend | pg_catalog 模拟、COPY、schema、session 处理 |
postgres/server | PostgreSQL wire protocol v3 服务端,基于 pgwire |
postgres/cli | tursopg,一个类 psql 的 REPL,也能直接托管 server |
3.1 最关键的设计决策:用 libpg_query 而不是自己写 parser
这一步是对的。libpg_query 是从 PostgreSQL 源码里抽出来的真实语法解析器,意味着 Turso 在语法层面对 Postgres 是 100% 兼容的——任何 psql 能解析的语句,Turso 也能解析。
但也正因为这样,产生了整个兼容层最危险的特性。官方文档自己写得很直白:
Because the parser accepts the full PostgreSQL grammar, some clauses parse and run but their semantics are silently dropped, producing wrong results or lost information without any error.
翻译成人话:语法全收,语义选择性执行,丢了不报错。
3.2 「静默丢弃」清单——这是你必须记住的部分
我把 postgres/COMPAT.md 里所有标注为静默忽略或降级的行为整理出来,这是全文最有实用价值的一张表:
| 语句 | 实际行为 | 后果 |
|---|---|---|
DELETE ... USING | USING 子句被静默丢弃 | 删除范围可能远超预期 |
TRUNCATE a, b, c | 降级为对第一张表的 DELETE,CASCADE / RESTART IDENTITY 丢弃 | b、c 表数据还在,序列没重置 |
CREATE TEMP TABLE | TEMP 被静默忽略 | 临时表变成了持久表 |
CREATE TEMP VIEW | 同上 | 同上 |
COMMENT ON | 接受但丢弃,不写入 pg_description | obj_description() 永远返回 NULL |
BEGIN ISOLATION LEVEL ... | 隔离级别与 READ ONLY/WRITE 被接受但忽略 | 你以为设了 Serializable,其实没有 |
SET / SHOW | 转成 PRAGMA,无 GUC 概念 | SHOW search_path 返回空 |
ILIKE / ~* / !~* | 转成 REGEXP,大小写不敏感变体被当作敏感处理 | 查询结果直接错 |
interval / xml / tsvector / tsquery / bit / 几何类型 | 降级为 TEXT | 类型运算全废 |
money | 降级为 REAL | 精度问题 |
CREATE TABLE AS ... WITH NO DATA | 降级为 LIMIT 0 | 被覆盖的 LIMIT 里的错误不上报 |
我的判断:这是兼容层最糟糕的失败模式。 报错是好事——报错意味着你在开发期就知道了。静默降级意味着你在生产环境的某个凌晨三点,看着一张本该只删 10 行却删了 100 万行的表发呆。
如果你真要试 Turso 的 Postgres 前端,第一件事不是跑业务,而是写兼容性探针。
3.3 实战:给 Postgres 前端写一个兼容性探针
思路很简单:对每一条你实际会用到的 SQL 模式,构造一个「如果语义被丢弃,结果必然不同」的断言。
#!/usr/bin/env python3
"""
turso_pg_probe.py — Turso Postgres 前端语义探针
用法: python3 turso_pg_probe.py "postgresql://turso@127.0.0.1:5432/main"
原理: 不测「能不能跑」,测「跑出来的语义对不对」。
静默降级的特征就是: 不抛异常, 但结果和 PG 不一致。
"""
import sys
import psycopg
PROBES = []
def probe(name, critical=False):
def deco(fn):
PROBES.append((name, fn, critical))
return fn
return deco
@probe("DELETE ... USING", critical=True)
def p_delete_using(cur):
cur.execute("DROP TABLE IF EXISTS probe_a; DROP TABLE IF EXISTS probe_b;")
cur.execute("CREATE TABLE probe_a(id INT, v INT);")
cur.execute("CREATE TABLE probe_b(id INT);")
cur.execute("INSERT INTO probe_a VALUES (1,1),(2,2),(3,3);")
cur.execute("INSERT INTO probe_b VALUES (2);")
# PG 语义: 只删 id=2 这一行, 剩 2 行
cur.execute("DELETE FROM probe_a USING probe_b WHERE probe_a.id = probe_b.id;")
cur.execute("SELECT count(*) FROM probe_a;")
left = cur.fetchone()[0]
assert left == 2, f"USING 被丢弃: 期望剩 2 行, 实际剩 {left} 行"
@probe("TRUNCATE 多表", critical=True)
def p_truncate_multi(cur):
cur.execute("DROP TABLE IF EXISTS t1; DROP TABLE IF EXISTS t2;")
cur.execute("CREATE TABLE t1(x INT); CREATE TABLE t2(x INT);")
cur.execute("INSERT INTO t1 VALUES (1); INSERT INTO t2 VALUES (1);")
cur.execute("TRUNCATE t1, t2;")
cur.execute("SELECT (SELECT count(*) FROM t1) + (SELECT count(*) FROM t2);")
total = cur.fetchone()[0]
assert total == 0, f"TRUNCATE 只作用于第一张表: 残留 {total} 行"
@probe("ILIKE 大小写不敏感", critical=True)
def p_ilike(cur):
cur.execute("DROP TABLE IF EXISTS t_ilike;")
cur.execute("CREATE TABLE t_ilike(s TEXT);")
cur.execute("INSERT INTO t_ilike VALUES ('Hello');")
cur.execute("SELECT count(*) FROM t_ilike WHERE s ILIKE 'hello';")
n = cur.fetchone()[0]
assert n == 1, "ILIKE 退化成大小写敏感匹配"
@probe("TEMP TABLE 是否真临时")
def p_temp(cur):
cur.execute("CREATE TEMP TABLE probe_temp(x INT);")
cur.execute("SELECT count(*) FROM pg_class WHERE relname = 'probe_temp';")
# 若 TEMP 被忽略, 它会以普通表身份出现在 catalog 里
n = cur.fetchone()[0]
assert n == 0, "TEMP 被静默忽略, 临时表变成了持久表"
@probe("事务隔离级别是否生效")
def p_isolation(cur):
cur.execute("BEGIN ISOLATION LEVEL SERIALIZABLE;")
cur.execute("SHOW transaction_isolation;")
row = cur.fetchone()
cur.execute("ROLLBACK;")
assert row and "serializable" in str(row[0]).lower(), \
f"隔离级别被忽略, SHOW 返回 {row}"
def main(dsn):
ok = fail = 0
with psycopg.connect(dsn, autocommit=True) as conn:
for name, fn, critical in PROBES:
with conn.cursor() as cur:
try:
fn(cur)
print(f" [PASS] {name}")
ok += 1
except AssertionError as e:
flag = "CRIT" if critical else "WARN"
print(f" [{flag}] {name}: {e}")
fail += 1
except Exception as e:
print(f" [ERR ] {name}: {type(e).__name__}: {e}")
fail += 1
print(f"\n通过 {ok} / 失败 {fail}")
return 1 if fail else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1]))
跑一遍,你就知道自己的 ORM 生成的 SQL 会在哪里翻车。这个成本远低于线上事故的成本。
3.4 类型映射:serial 的处理很聪明
有一处设计我要单独夸一下。Postgres 的 serial 系列类型,Turso 是这样映射的:
-- 你写的
CREATE TABLE users (id serial PRIMARY KEY, name text);
-- Turso 实际展开成
CREATE TABLE users (
id INTEGER NOT NULL DEFAULT nextval('users_id_seq'),
name TEXT,
PRIMARY KEY (id)
) STRICT;
-- 并隐式创建 users_id_seq
注意两点:
- 它真的实现了
CREATE SEQUENCE/nextval/currval/setval,包括START、INCREMENT、MIN/MAXVALUE、CYCLE,以及pg_sequences视图。CHANGELOG 里对应的提交是feat: add PostgreSQL-style sequences and MVCC-safe AUTOINCREMENT——MVCC-safe 这个词是重点,序列在并发事务下的行为是被专门处理过的。 - 所有通过 PG 前端建的表都是
STRICT表。这很关键:Postgres 是强类型的,如果建出 SQLite 那种动态类型表,INSERT INTO t(int_col) VALUES ('abc')会静默成功,语义就彻底崩了。用 STRICT 是唯一正确的选择。
pg_catalog 的模拟也做得比我预期的全:pg_class、pg_namespace、pg_attribute、pg_type(含 array 和 enum)、pg_index、pg_constraint、pg_attrdef、pg_tables、pg_sequences、pg_database、pg_roles、pg_proc、pg_am 都是活的(反映真实 schema),另有一批空表占位。这意味着很多依赖 catalog 内省的工具(DBeaver、部分 ORM 的 migration 检测)能跑起来。
CREATE SCHEMA 的实现也很有意思:schema 就是 ATTACH 的数据库,public 特殊处理。一个嵌入式引擎能用 ATTACH 机制映射出 namespace 概念,这个复用很漂亮。
四、MVCC 与 BEGIN CONCURRENT:突破单写者天花板
4.1 SQLite 的天花板在哪
SQLite 在 WAL 模式下的并发模型是:多读者 + 单写者。任何时刻只能有一个写事务。写多的场景下,SQLITE_BUSY 是你的老朋友。
这不是 bug,是设计。SQLite 的目标场景是「一个应用进程用一个文件」,单写者换来的是极简的实现和地狱级的可靠性。
但 2026 年的嵌入式场景变了:一个 Node.js 服务几十个并发请求、一个 AI Agent 十几个子任务并行写记忆库、边缘节点上跑多租户 —— 单写者就是瓶颈。
4.2 Turso 的三种事务模型
| 模式 | 加锁时机 | 并发能力 | 冲突表现 |
|---|---|---|---|
DEFERRED(默认) | 首次读时开读事务,首次写时升级为写事务 | 多读单写 | 写升级失败 → SQLITE_BUSY |
IMMEDIATE / EXCLUSIVE | BEGIN 时立刻抢写锁 | 多读单写 | BEGIN 就可能 BUSY |
CONCURRENT(需 MVCC) | 从不加锁 | 多读多写 | 提交时冲突检测 → SQLITE_BUSY |
EXCLUSIVE 在 Turso 里是 IMMEDIATE 的别名,这一点和 SQLite 的 WAL 模式行为一致。
4.3 CONCURRENT 事务的完整生命周期
这是文档里写得最扎实的一段,我拆开讲。
开始时(BEGIN CONCURRENT TRANSACTION):
- 分配唯一事务 ID
- 从逻辑时钟记录 begin timestamp
- 创建空的 read set 和 write set
- 不获取任何锁
运行期间(快照隔离):
- 能看到 begin timestamp 之前所有已提交的数据
- 看不到在自己开始之后提交的其他事务的写入
- 同一事务内可重复读
- 多个 concurrent 事务可以同时读写,互不阻塞
- 所有读到的行进 read set,所有写的行进 write set
提交时(三步):
- 独占事务检查:如果此刻有活跃的 exclusive 事务(
BEGIN IMMEDIATE,或者升级成写事务的BEGIN DEFERRED),concurrent 事务无法提交,直接SQLITE_BUSY。 - 写写冲突检测:遍历 write set 里每一行,检查是否满足以下任一条件:
- 该行正被另一个活跃事务修改
- 该行被某个在本事务 begin timestamp 之后提交的事务修改过
- 提交或中止:无冲突则提交,write set 里所有 row version 的 begin timestamp 更新为本事务的 commit timestamp;有冲突则
SQLITE_BUSY,应用层必须回滚重试。
有一个很微妙的设计点:exclusive 事务阻塞 concurrent 事务的提交,但不阻塞它的读写。也就是说 concurrent 事务在独占事务运行期间可以继续干活,只是不能落地。这个设计让 schema 变更这类必须独占的操作能真正独占,同时又不让并发事务白白空转。
官方给的最佳实践很明确:MVCC 模式下,所有写事务都用 BEGIN CONCURRENT;只有真正需要排他写权限时才用 IMMEDIATE / DEFERRED。
4.4 隔离级别的真相:是快照隔离,不是可串行化
文档写得很诚实:CONCURRENT 提供的是 snapshot isolation。
这意味着 write skew(写偏斜)异常依然存在。经典例子:
-- 业务规则: 任何时刻至少要有一位医生在值班
-- 初始: Alice 和 Bob 都在值班
-- 事务 T1 (Alice 请假)
BEGIN CONCURRENT;
SELECT count(*) FROM doctors WHERE on_call = 1; -- 读到 2, OK
UPDATE doctors SET on_call = 0 WHERE name = 'Alice';
COMMIT; -- write set = {Alice 行}
-- 事务 T2 (Bob 请假), 与 T1 并发
BEGIN CONCURRENT;
SELECT count(*) FROM doctors WHERE on_call = 1; -- 也读到 2, OK
UPDATE doctors SET on_call = 0 WHERE name = 'Bob';
COMMIT; -- write set = {Bob 行}
两个事务的 write set 完全不相交(一个改 Alice 行,一个改 Bob 行),写写冲突检测不会触发,两个都提交成功。结果:没有医生值班,业务约束被破坏。
这不是 Turso 的 bug,这是快照隔离的定义性缺陷。 但你必须知道,因为很多人下意识把 MVCC 等同于「安全」。
规避方式有三种:
-- 方案 1: 把约束物化成一行, 强制制造写冲突
UPDATE oncall_counter SET n = n - 1 WHERE ward = 'ICU';
-- 两个事务都会写这一行 -> 必然冲突 -> 一个被拒
-- 方案 2: 该业务走 BEGIN IMMEDIATE, 牺牲并发换正确性
-- 方案 3: 用唯一约束 / CHECK 让数据库替你兜底
4.5 实战:正确的 CONCURRENT 重试封装
SQLITE_BUSY 在 MVCC 下不是异常,是正常控制流。乐观并发的前提就是「冲突了就重试」。但重试有讲究——朴素的立即重试会造成惊群,把冲突率推到更高。
Rust 版:
use rand::Rng;
use std::time::Duration;
use turso::{Connection, Error as TursoError};
/// 判断错误是否为可重试的 MVCC 冲突
fn is_retryable(e: &TursoError) -> bool {
// Turso 把写写冲突映射为 SQLITE_BUSY
let s = e.to_string();
s.contains("SQLITE_BUSY") || s.contains("database is locked")
}
/// 带指数退避 + 抖动的 CONCURRENT 事务执行器
///
/// 注意: 每次重试必须重新执行整个闭包(包括读),
/// 因为快照已经过期, 复用旧读结果会导致逻辑错误。
pub async fn with_concurrent_txn<F, Fut, T>(
conn: &Connection,
max_retries: u32,
mut body: F,
) -> anyhow::Result<T>
where
F: FnMut(&Connection) -> Fut,
Fut: std::future::Future<Output = anyhow::Result<T>>,
{
let mut attempt = 0u32;
loop {
conn.execute("BEGIN CONCURRENT", ()).await?;
match body(conn).await {
Ok(val) => match conn.execute("COMMIT", ()).await {
Ok(_) => return Ok(val),
Err(e) if is_retryable(&e) && attempt < max_retries => {
let _ = conn.execute("ROLLBACK", ()).await;
backoff(attempt).await;
attempt += 1;
continue;
}
Err(e) => {
let _ = conn.execute("ROLLBACK", ()).await;
return Err(e.into());
}
},
Err(e) => {
let _ = conn.execute("ROLLBACK", ()).await;
return Err(e);
}
}
}
}
async fn backoff(attempt: u32) {
// base = 2^attempt * 1ms, 上限 200ms, 叠加 0~50% 抖动
let base = (1u64 << attempt.min(8)).min(200);
let jitter = rand::thread_rng().gen_range(0..=(base / 2).max(1));
tokio::time::sleep(Duration::from_millis(base + jitter)).await;
}
Python 版(更贴近日常):
import random
import time
import turso
class ConflictRetryExhausted(Exception):
pass
def concurrent_txn(conn, fn, max_retries=8):
"""
在 BEGIN CONCURRENT 下执行 fn(cur), 冲突自动重试。
关键约束:
1. fn 必须是幂等的 —— 它会被整体重放
2. fn 内部不要做外部副作用(发邮件/调 API), 那些要挪到提交之后
3. 不要在 fn 里缓存上一轮读到的值
"""
for attempt in range(max_retries + 1):
cur = conn.cursor()
try:
cur.execute("BEGIN CONCURRENT")
result = fn(cur)
cur.execute("COMMIT")
return result
except turso.OperationalError as e:
msg = str(e).lower()
retryable = "busy" in msg or "locked" in msg
try:
cur.execute("ROLLBACK")
except Exception:
pass
if not retryable or attempt == max_retries:
raise
# 指数退避 + 全抖动 (AWS full jitter)
cap = min(0.2, 0.001 * (2 ** attempt))
time.sleep(random.uniform(0, cap))
raise ConflictRetryExhausted(f"{max_retries} 次重试后仍冲突")
# 使用示例: 并发扣库存
def deduct_stock(sku, qty):
def body(cur):
cur.execute("SELECT stock FROM inventory WHERE sku = ?", (sku,))
row = cur.fetchone()
if row is None:
raise ValueError(f"未知 SKU: {sku}")
if row[0] < qty:
raise ValueError("库存不足")
cur.execute(
"UPDATE inventory SET stock = stock - ? WHERE sku = ?", (qty, sku)
)
return row[0] - qty
return concurrent_txn(conn, body)
三条踩坑经验:
- 一个连接只能有一个活跃事务。 文档明确写了:需要并发(包括
BEGIN CONCURRENT)时,必须用不同的连接,而不是在同一连接上并行执行语句。所有在同一连接上 prepare 的语句都属于同一个事务上下文。用连接池,别偷懒。 - 重试必须重放整个事务体,包括读。 快照过期后,你上一轮读到的值就是脏的。见过有人只重放 UPDATE 不重放 SELECT,结果扣出负库存。
- 别在事务体里做外部副作用。 事务会被重放 N 次,发邮件也会发 N 次。
4.6 MVCC 现在的真实状态:别急着上生产
manual 里的 MVCC 限制清单,我原样搬过来,每一条都很重。
- 索引不能创建,带索引的数据库不能用
- 首次访问时把全部数据从磁盘急切加载到内存——大库启动极慢且吃内存
- 只支持
PRAGMA wal_checkpoint(TRUNCATE),而且同时阻塞读者和写者 - 很多特性可能不工作、工作不正确,或者直接 panic
- 查询可能返回错误结果
- 用 MVCC 写过的库,不 checkpoint 就用非 MVCC 模式打开,改动不可见
第一条和第二条基本判了死刑:没有索引 + 全量入内存 = 只能玩玩。
另外 CHANGELOG 里有一条值得注意:core: reject co-enablement of MVCC and multiprocess WAL——MVCC 和多进程 WAL 现在是互斥的,不能同时开。
所以我的判断是:BEGIN CONCURRENT 的架构设计是对的,实现完成度还差得远。 现在应该做的是在测试环境里跑起来验证你的重试逻辑,等索引支持落地再谈迁移。
五、Rust 重写真正的红利:不是内存安全,是「可失败的分配」和异步 I/O
这一节是我认为整个项目里最被低估的部分。
5.1 异步 I/O:让 sqlite3_step 可以「先返回」
SQLite 的 API 是彻底同步的。sqlite3_step() 调用下去,如果页不在缓存里,就得等磁盘。在单线程事件循环(Node.js、Python asyncio、Tokio)里,这就是一次硬阻塞。
Turso 在 Linux 上用 io_uring 做异步 I/O,并且扩展了 sqlite3_step 的语义:数据没准备好时可以立刻返回,让调用方去干别的。
CHANGELOG 里能看到这条主线推进得非常猛:
Begin to remove all blocking IO from corecore: More blocking I/O removalRemove blocking IO from mvcc bootstrap and recovery pathsEliminate blocking I/O in page cache spill pathMake durable storage log completion awaitableStep result yieldadd yielding IO back-end for turso-stress to test async re-entrancycore/io: add runtime-registrable Rust IO backend
最后那条尤其重要:可以在运行时注册自定义 I/O 后端。这意味着你能把 Turso 的 I/O 接到自己的 runtime 上——不管是 Tokio、glommio 还是某个自研的 thread-per-core 框架。
Rust 侧用起来就是原生 async:
use turso::Builder;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let db = Builder::new_local("app.db").build().await?;
let conn = db.connect()?;
conn.execute(
"CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY,
ts INTEGER NOT NULL,
kind TEXT NOT NULL,
body TEXT
)",
(),
).await?;
// 整个 IO 路径都是 await 的, 不会卡住 executor
let mut rows = conn
.query("SELECT id, kind FROM events WHERE ts > ? ORDER BY ts LIMIT 100", (1_700_000_000i64,))
.await?;
while let Some(row) = rows.next().await? {
let id: i64 = row.get(0)?;
let kind: String = row.get(1)?;
println!("{id} {kind}");
}
Ok(())
}
对比一下 rusqlite:那是同步 API,在 async 场景里你要么 spawn_blocking(线程池开销 + 上下文切换),要么直接阻塞 executor(灾难)。Turso 是从内核 I/O 一路 async 上来的,这是重写才能拿到的红利,fork 拿不到。
5.2 可失败的内存分配:这才是 Rust 重写的杀手锏
翻 CHANGELOG,你会看到一整条极其密集的提交线:
core/alloc: add stable push_within_capacity helperAdd allocator API for fallible allocationcore/alloc: Add allocation site annotations and out-of-memory fault injectorcore/skiplist: use TursoAllocator for node allocation and add fallible insertsmvcc: add allocator generic to mv storecore: make schema cloning fallibleschema: make sqlite_schema bootstrap allocation falliblecore/vdbe: make vec allocations falliblecore/translate: propagate fallible Vec allocationsmigrate hash_table.rs / sorter.rs / order_by.rs to use fallible vec allocationsrefactor Extendable trait to use Result for fallible allocationscore/alloc: clone Vec elements through TryClonemvcc: use fallible skiplist allocationsTrack MVCC checkpoint allocation sites
为什么这件事这么重要?
Rust 标准库的 Vec::push 在分配失败时是 abort——直接杀进程。对一个普通 CLI 工具,这没问题。对一个嵌入在你应用进程里的数据库,这是完全不可接受的:
用户 App 内存吃紧 → 数据库内部一次 Vec 扩容失败 → 整个 App 被 abort → 用户数据可能处于半写状态
SQLite 用 C 写,malloc 返回 NULL 就是普通的错误码,一路 propagate 上去变成 SQLITE_NOMEM。这是 C 在这个场景下的天然优势。 Rust 想追上,必须手动把整条分配链路改成 fallible。
Turso 干的就是这件事,而且做得很彻底:
- 自定义
TursoAllocator - 把
Vec/Box<[T]>/ skiplist / hash_table / sorter / order_by 的所有分配点改成返回Result TryClone替代CloneExtendabletrait 返回Result- 加了 OOM 故障注入器,主动在测试里制造分配失败
- 给每个分配点加标注(allocation site annotations),能追踪内存花在哪
这套工作量巨大、毫无宣传价值、外部用户完全感知不到。但它是「玩具」和「能嵌进别人 App 的数据库」之间的分界线。
我的观点:「Rust 重写 = 内存安全」是最肤浅的理解。 真正的价值在于,Rust 的类型系统让「分配可能失败」这件事变成可以在编译期强制、在测试期注入、在运行期追踪的一等公民。C 里你也能写 fallible 分配,但没人能保证你写全了;Rust 里 Result 不处理编译器就报警。
5.3 顺带一提:递归改迭代
还有两条容易被忽略但很关键的提交:
convert expr_walk* to use iteration instead of recursionsqlite/parser: bound expression depth to prevent translator stack overflow
用户可以提交任意嵌套深度的 SQL 表达式。如果 AST 遍历是递归的,一条恶意构造的 SQL 就能爆栈。把递归改成迭代 + 给表达式深度设上限,这是嵌入式数据库必须做的攻击面收敛。
六、可靠性工程:他们怎么补上 SQLite 那套专有测试套件
前面说过,重写的最大障碍是「SQLite 的测试套件不开源」。Turso 的应对方式,是我在开源数据库里见过最认真的一套。
从仓库目录和 CI 配置能看到这些:
| 手段 | 位置 / 证据 | 作用 |
|---|---|---|
| 确定性模拟测试(DST) | Dockerfile.antithesis、antithesis: squash targeting diff into targeted_coverage.json | 用 Antithesis 平台在模拟的硬件/软件故障环境下跑,失败可完全复现 |
| TLA+ 形式化验证 | tlaplus/sqlite-tx/ | 对事务协议做形式化建模,在写代码之前证明协议正确 |
| Simulator | testing/、simulator: support virtual columns | 随机生成 SQL 负载 + 随机注入故障 |
| Stress / Whopper | turso_stress、Add recovery-heavy profile to Whopper | 压力测试,专门有「恢复密集型」profile |
| Shuttle 并发测试 | testing/stress: reset shuttle step counter on iteration progress | 系统性探索线程交错 |
| Aristo WAL 验证 | .aristo/、Add Aristo WAL verification | 专门验证 WAL 的正确性 |
| 模糊测试 | fuzz/、lock cargo-fuzz to 0.13.0 | parser 与执行路径的 fuzzing |
| OOM 故障注入 | out-of-memory fault injector | 主动制造分配失败 |
| 一致性对拍 | tools/dbhash、testing/conformance/javascript | 与 SQLite 做结果对拍 |
| 性能回归 CI | CodSpeed,ci: shard CodSpeed benchmarks by toolchain | 每个 PR 都跑基准,防性能回退 |
这套组合拳的思路很清晰:SQLite 用「三十年 + 海量人工测试用例」堆信心,Turso 用「形式化验证 + 可复现的随机」堆信心。
后者理论上更强——TLA+ 能覆盖人想不到的交错,DST 能让「三个月才出现一次的诡异 bug」变成可以按需重放的确定性场景。前者胜在时间检验。
顺便提一个有意思的观察:CHANGELOG 的提交者列表里有一个叫 roboturso 的账号,提交内容包括:
core/translate: use abort journal analysis for upsert DO UPDATE armscore/mvcc: reset index shadow finger when index keys are created mid-scancore: classify virtual tables by parsing schema SQLsqlite/parser: bound expression depth to prevent translator stack overflow
而且仓库里有 .claude/skills、.codex、AGENTS.md、CLAUDE.md,CONTRIBUTING.md 里专门有 Add section on AI coding 和 Add some testing guidance for agents,甚至有 fossier: vouch for @roboturso 这样的信任背书提交。
这是我第一次在一个严肃的数据库项目里,看到 AI agent 作为常规 contributor 出现在 changelog 里,而且提交的是 MVCC 索引游标这种硬核补丁。 你可以说这很激进,但换个角度:一个有 DST + TLA+ + fuzzing + OOM 注入 + 性能回归 CI 的项目,恰恰是最有资格让 AI 提交代码的项目——因为它的验证网足够密。测试基础设施的强度,决定了你能接受多快的代码产出速度。
七、上手实战
7.1 装、跑、连
# 安装 CLI
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/tursodatabase/turso/releases/latest/download/turso_cli-installer.sh | sh
# 或者 brew
brew install turso
# 交互式 shell (默认内存库)
tursodb
# 打开持久化文件
tursodb ./app.db
$ tursodb
Turso
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database
turso> CREATE TABLE users (id INT, username TEXT);
turso> INSERT INTO users VALUES (1, 'alice'), (2, 'bob');
turso> SELECT * FROM users;
┌────┬──────────┐
│ id │ username │
├────┼──────────┤
│ 1 │ alice │
├────┼──────────┤
│ 2 │ bob │
└────┴──────────┘
几个值得记的 CLI 开关:
tursodb --mcp # 起 MCP server 而不是交互 shell
tursodb --experimental-encryption ./a.db # 静态加密(实验)
tursodb --experimental-views ./a.db # 视图(实验)
tursodb --experimental-multiprocess-wal ./a.db # 多进程 WAL(实验)
tursodb -m list ./a.db # 输出格式对齐 sqlite3, 方便脚本对拍
tursodb --readonly ./a.db # 只读打开
-m list 这个开关在做 SQLite 对拍时特别有用,输出格式和 sqlite3 默认一致,可以直接 diff。
7.2 各语言接入
# Rust
cargo add turso
# JavaScript / TypeScript (含 WASM)
npm i @tursodatabase/database
# Python
uv pip install pyturso
# Go
go get turso.tech/database/tursogo
// JavaScript —— 注意 connect 是 async, 但 prepare/all 是同步的
import { connect } from '@tursodatabase/database';
const db = await connect('sqlite.db');
// batch() 返回每条语句各自的结果
const results = await db.batch([
"CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)",
"INSERT OR REPLACE INTO kv VALUES ('a', '1')",
"SELECT * FROM kv",
]);
const users = db.prepare('SELECT * FROM users WHERE id > ?').all(10);
console.log(users);
# Python —— DB-API 2.0 风格, 从 sqlite3 迁移几乎零成本
import turso
con = turso.connect("sqlite.db")
cur = con.cursor()
cur.execute("SELECT * FROM users")
print(cur.fetchone())
# 两个 sqlite3 没有的实用能力(CHANGELOG: release the GIL and expose
# interrupt() / set_query_timeout())
con.set_query_timeout(5000) # 毫秒级查询超时
# 另一线程可以调 con.interrupt() 打断慢查询
Python 绑定「release the GIL」这一条对 CPU 密集的分析查询很关键——查询跑在 Rust 侧时不持有 GIL,其他 Python 线程能继续跑。sqlite3 标准库在这方面就没这么干净。
// Go —— 标准 database/sql 接口
import (
"database/sql"
_ "turso.tech/database/tursogo"
)
conn, _ := sql.Open("turso", "sqlite.db")
defer conn.Close()
stmt, _ := conn.Prepare("select id, username from users")
defer stmt.Close()
rows, _ := stmt.Query()
for rows.Next() {
var id int
var username string
rows.Scan(&id, &username)
fmt.Printf("User: %d %s\n", id, username)
}
7.3 内置 MCP server:数据库自己长出了 AI 接口
这是 Turso 一个很有前瞻性的设计——CLI 内置 Model Context Protocol server:
tursodb ./analytics.db --mcp
配到 MCP 客户端里:
{
"mcpServers": {
"turso": {
"command": "/path/to/.turso/tursodb",
"args": ["/path/to/your/database.db", "--mcp"]
}
}
}
Claude Code 一行搞定:
claude mcp add my-project-db -- tursodb ./data/app.db --mcp
暴露九个工具:open_database、current_database、list_tables、describe_table、execute_query(只读 SELECT)、insert_data、update_data、delete_data、schema_change。
也可以直接走 JSON-RPC 裸调,方便写集成测试:
cat << 'EOF' | tursodb --mcp
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"client","version":"1.0"}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"schema_change","arguments":{"query":"CREATE TABLE users (id INTEGER, name TEXT)"}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_tables","arguments":{}}}
EOF
注意这里的权限设计:execute_query 明确限定只读 SELECT,写操作拆成独立的 insert_data / update_data / delete_data / schema_change。 这个拆分不是为了好看,是为了让 MCP 客户端能做工具级授权——你可以只放开读工具,禁掉所有写工具。相比「给 Agent 一个万能 execute_sql」,这是负责任得多的设计。
一句话总结这个功能的意义:数据库自带 MCP,意味着「AI 能查我的数据」这件事不再需要中间层。 对本地 Agent 场景(个人知识库、日志分析、代码库索引),这省掉的是一整个服务。
7.4 迁移策略:影子双写 + dbhash 对拍
如果你真想从 SQLite 迁到 Turso,别一刀切。仓库里有个 tools/dbhash,配合 -m list 输出模式,可以搭一套影子校验:
#!/usr/bin/env bash
# shadow_verify.sh —— SQLite / Turso 双跑对拍
# 用法: ./shadow_verify.sh ./queries.sql ./data.db
set -euo pipefail
QUERIES="$1"
DB="$2"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
# 1) 各复制一份, 避免互相干扰
cp "$DB" "$WORK/sqlite.db"
cp "$DB" "$WORK/turso.db"
# 2) 同一批语句分别在两个引擎上执行
sqlite3 "$WORK/sqlite.db" < "$QUERIES" > "$WORK/out.sqlite" 2> "$WORK/err.sqlite" || true
tursodb -q -m list "$WORK/turso.db" < "$QUERIES" > "$WORK/out.turso" 2> "$WORK/err.turso" || true
# 3) 对比查询输出
if diff -u "$WORK/out.sqlite" "$WORK/out.turso" > "$WORK/diff.out"; then
echo "[OK] 查询结果一致"
else
echo "[DIFF] 查询结果不一致:"
head -50 "$WORK/diff.out"
fi
# 4) 对比落盘后的逻辑内容
H1=$(dbhash "$WORK/sqlite.db")
H2=$(dbhash "$WORK/turso.db")
if [ "$H1" = "$H2" ]; then
echo "[OK] 数据库内容哈希一致: $H1"
else
echo "[DIFF] 内容哈希不一致: sqlite=$H1 turso=$H2"
fi
有一个坑必须提前知道:Turso 不保证查询结果的顺序与 SQLite 一致(manual 的 Limitations 里明确写了,对应 issue #2964)。所以对拍脚本里,所有 SELECT 都必须带完整确定的 ORDER BY,否则你会被大量假阳性 diff 淹没。
顺便说,这个限制本身也是个警告:如果你的业务代码依赖了「没有 ORDER BY 但结果碰巧有序」这件事,那你在任何数据库上都是在赌博。 Turso 只是把这个赌局提前引爆了。
八、性能:什么负载受益,什么不受益
Turso 的定位是 in-process 数据库,读写延迟目标是亚微秒级(没有网络往返)。但「重写成 Rust」本身不等于更快。从 CHANGELOG 里能看到几条真实的优化:
已经落地的:
perf(vdbe): reuse btree cursor on OpenRead instead of rebuilding—— OpenRead 时复用 B-tree cursor 而不是重建。这对循环内反复打开同一张表的执行计划收益明显。perf(vdbe): only build the comparator and clone the aggregate arg when needed—— 聚合参数惰性 clone。Eliminate quadratic performance for prepare of queries with a lot of parameters—— 参数多的语句 prepare 从 O(n²) 降到线性。批量 INSERT 用几百个占位符的场景直接受益。avoid full range scan for index seek operation—— 索引 seek 不再退化成全范围扫描。Linear better than quadratic—— 同类优化。Page cache soft limit+Eliminate blocking I/O in page cache spill path—— 页缓存软上限,溢出路径不阻塞。core/mvcc: serialize buffer directly, avoid 2x buffer heap size—— MVCC 序列化不再开双份缓冲。mem usage in MVCC: pack timestamp begin/end in RowVersion—— 时间戳打包进 RowVersion,缩小版本链内存。core/mvcc: Shrink version chains and remove empty slots at checkpoint—— checkpoint 时收缩版本链。introduce ImmutableRecordRef to reduce cloning—— 减少记录 clone。core/optimizer: improve starting table selection in greedy join—— 贪心 join 的起始表选择改进。
判断受益 / 不受益的经验规则:
| 场景 | 判断 | 原因 |
|---|---|---|
| async runtime 里的高并发小查询 | ✅ 明显受益 | 不再 spawn_blocking,io_uring 全程非阻塞 |
| 参数极多的批量 INSERT | ✅ 明显受益 | prepare 从 O(n²) 变线性 |
| 多写者场景(且能接受无索引) | ✅ 受益 | BEGIN CONCURRENT 突破单写者 |
| 需要同时被 psql 工具链访问的嵌入式库 | ✅ 独一份 | 没有第二个 in-process 引擎有 pgwire |
| 单线程顺序读、小数据集 | ⚠️ 基本持平 | SQLite 本来就在这个场景做到极致了 |
| 大库 + 复杂 join + 重索引 | ❌ 暂不建议 | 优化器成熟度差 SQLite 二十年 |
| 需要 vacuum / savepoint / 稳定 trigger | ❌ 不支持 | manual 明确列为限制 |
| MVCC + 大数据集 | ❌ 危险 | 全量入内存,启动慢且吃爆内存 |
测性能的正确姿势:别信任何人给的 benchmark(包括这篇文章),用你自己的真实 query 集跑对拍。Turso 自己在 CI 上接了 CodSpeed 做每 PR 的性能回归追踪,还按 toolchain 和 benchmark 分片 —— 这说明他们自己也知道性能是要持续盯的,不是发个数字就完事。
九、风险清单:我认为你现在必须知道的几件事
9.1 文档和实现存在明显时间差
这一点我要特别标出来,因为它会直接坑到人。
docs/manual.md 的 Limitations 章节说:
- No multi-process access
- No triggers
- No views
- No savepoints
- No vacuum
但 README 的 Features 里写着:
- Multi-process WAL coordination via the
.tshmsidecar(实验)
CLI 也有 --experimental-views 开关。而 CHANGELOG 里有 core/storage: add savepoint rollback consistency intent、core/pager: reset pager state on savepoint rollback、core/mvcc: rollback savepoint on abandoned statement —— savepoint 显然在做了。还有 core/btree: invalidate shape caches on peer writes; drop trigger workaround —— trigger 也有动静。
结论:manual 的限制清单落后于实现。 遇到冲突时,以 README.md + COMPAT.md + CHANGELOG 为准,manual 当参考。更稳妥的做法是:你关心的每一个特性,都自己写个五行的最小复现验证一遍,别信文档。
9.2 实验性特性的密度非常高
统计一下带 experimental 标记的东西:Postgres 兼容、静态加密、DBSP 增量计算、tantivy 全文检索、多进程 WAL、索引(manual 里写 Indexes are currently experimental in Turso and not enabled by default)、views、page codecs、index method。
索引默认不启用这一条最要命。一个默认没索引的数据库,能跑的负载类型是极其有限的。
9.3 版本号还是 0.x,但 README 说「已在多家组织的生产环境运行」
原文:It runs in production today at multiple organizations. 同时 CHANGELOG 里有一条 Drop the beta warning。
这两件事同时为真是可能的——在受控场景下(数据量可控、SQL 模式固定、有完整回滚方案、不开 MVCC 不开 PG 前端)。但它绝不等于「你可以拿它替换生产 SQLite」。
我的建议按风险分三档:
| 档位 | 场景 | 建议 |
|---|---|---|
| 🟢 可以上 | 本地工具、CLI、开发环境、可重建的缓存库、AI Agent 的临时记忆库 | 直接用,出问题重建即可 |
| 🟡 可以试 | 需要 async I/O 的服务、需要 pgwire 的嵌入式场景 | 影子双写 + dbhash 对拍,保留 SQLite 回滚路径 |
| 🔴 不要碰 | 用户核心数据、金融记录、无备份的单一数据源、开 MVCC 的大库 | 等 1.0,等索引稳定 |
9.4 Postgres 前端的定位要摆正
不要把它理解成「Turso 可以替代 PostgreSQL」。它是「你的嵌入式数据库可以被 Postgres 生态的工具访问」。
这个区别很重要。真正的用例是:
- 本地开发时用 psql / DBeaver / pgAdmin 直接查嵌入式库,不用装第二套工具
- 已有的 Postgres 驱动和部分 ORM 可以复用,减少胶水代码
- 边缘节点跑嵌入式引擎,中心用 Postgres,SQL 方言可以尽量统一
- 测试环境用嵌入式 Turso 顶替 Postgres 容器,起停快得多
而不是:把生产 Postgres 集群换成 Turso。缺的东西太多了:没有 EXPLAIN(一个都没有)、没有 information_schema 视图、没有 LISTEN/NOTIFY、没有 advisory lock、没有大对象、没有 range/multirange 类型、没有 BRIN/GiST 索引、pg_roles 只有一个硬编码的 turso 角色。
十、总结:这个项目真正的价值在哪
回到开头那个问题:「数据库界的 LLVM」是不是营销话术?
我的结论是:架构上成立,成熟度上还早,但方向是对的。
三个理由:
第一,VDBE 作为 IR 的复用是真实的技术资产,不是包装。 SQLite 二十多年前就把 SQL 编译成字节码这件事做对了,只是没人想过前端可以换。Turso 把这个隐藏资产显式化了。一旦这条路走通,加第三个、第四个前端(MySQL 方言?某种时序 DSL?)的边际成本会大幅下降——这正是 LLVM 模型的核心价值:新增前端的成本远低于新建一个数据库。
第二,Rust 重写的真正红利被大多数人理解错了。 不是内存安全(SQLite 用 C 写了三十年也没爆过多少内存漏洞),而是:
- 全链路异步 I/O —— fork 一份 C 代码是做不到的,这是架构级改动
- 可失败的内存分配 —— 让「OOM 不 abort 宿主进程」成为编译期强制、测试期可注入、运行期可追踪的性质
- 可运行时注册的 I/O 后端 —— 把引擎接到任意 runtime 上
这三条,每一条都是「重写」才能拿到,「fork」拿不到的。这也回答了「为什么要重写」这个最初的问题。
第三,可靠性工程的投入配得上它的野心。 TLA+ 形式化验证 + Antithesis DST + simulator + shuttle 并发测试 + fuzzing + OOM 故障注入 + CodSpeed 性能回归 —— 这套组合在开源数据库里属于第一梯队。它在明确地对标 SQLite 那套不开源的 TH3。
但坦率说,现在还不是用它替换 SQLite 的时候。 索引默认不开、MVCC 全量入内存、文档落后于实现、一堆实验性特性、Postgres 前端的静默语义丢弃——每一条都够你在生产环境上一次强度不小的课。
那什么时候该关注它?现在。
- 如果你在 Rust / Node.js / Python 里做 async 服务,且被 SQLite 的同步 API 恶心过 —— 现在就可以试
- 如果你在做本地 AI Agent,需要一个自带 MCP、能跑在 WASM 里、能加密的嵌入式库 —— 现在就可以试
- 如果你被 SQLite 的单写者卡住过 —— 关注
BEGIN CONCURRENT,等索引支持落地 - 如果你在做边缘/多租户,希望嵌入式库也能被 psql 工具链访问 —— 关注 Postgres 前端,但先跑一遍兼容性探针
最后留一个更大的问题。
SQLite 是世界上部署量最大的软件之一,靠的是「极致可靠 + 极致简单 + 三十年不变」。Turso 走的是相反的路:极致激进 + 特性爆炸 + 用形式化验证和故障注入去兜可靠性的底。
这两条路,长期看谁赢?
我倾向于认为它们不冲突。SQLite 会继续统治那些「装进去就再也不用管」的场景——手机、浏览器、飞机、路由器。而 Turso 争夺的是一块新战场:需要嵌入式的低延迟,但又要求 MVCC、异步、加密、向量、增量视图、多方言的现代应用。这块地在 2020 年之前几乎不存在,是 edge computing 和本地 AI Agent 硬生生撑出来的。
从这个角度看,Turso 最聪明的地方,可能不是用 Rust 重写了 SQLite,而是它意识到「嵌入式数据库」这个品类的需求变了,然后按新需求重新设计了一遍,只是顺便保持了 SQLite 兼容。
顺便保持兼容——这四个字,才是它最大的护城河。
参考与验证入口(本文所有技术细节均可在以下位置核对):
- 主仓库:
github.com/tursodatabase/turso(MIT 协议) - SQLite 兼容矩阵:仓库根目录
COMPAT.md - Postgres 兼容矩阵:
postgres/COMPAT.md - 事务与 MVCC 语义:
docs/manual.md的 Transactions 章节 - 性能说明:
PERF.md - 形式化规格:
tlaplus/sqlite-tx/ - 最新版本:
v0.8.0-pre.3(2026-08-06 发布)