马年春节,挑战让 AI 重写一个现代 SQLite(时机/架构/性能)

20 阅读9分钟

The SQLVibe Repo, Need your ✨ github.com/cyw0ng95/sq…

特别感谢 MonkeyCode AI 提供免费的计算环境,开发过程采用模型:Claude + GLM + MiniMax

第一部分:时机——为什么现在是重写的时间窗口

硬件架构的范式转移

SQLite 诞生于 2000 年,时代的硬件特征是:单核 CPU、256MB 内存、机械硬盘。这些物理约束塑造了 SQLite 的核心架构——B-Tree 页面管理、LRU 缓存淘汰、磁盘 I/O 优化。

二十五年后的今天,硬件已经面目全非:

硬件特性2000 年2026 年架构含义
CPU 核心1 核8-16 核 (消费级)向量化、多线程并行成为可能
SIMD 指令512 位 (AVX-512)一次处理 8 个 64 位整数
内存容量256MB16-32GB (消费级)空间换时间、列式存储可行
存储介质磁盘为主内存优先抛弃磁盘优化遗产

最关键的变化是存储介质:越来越多的数据库场景是"纯内存"——嵌入式设备、端侧 AI、隐私敏感应用。SQLite 的 :memory: 模式只是机械地把文件映射到内存,而不是真正为内存访问模式设计的数据库。

这意味着:为我们从架构层面为内存优化,可以完全抛弃 B-Tree 页面管理的磁盘 I/O 思维,获得数量级的性能提升。

AI 辅助编程的成熟

重写 SQLite 是一个约 20000 行代码的工程:

  • SQL 解析器:约 2000 行
  • 存储引擎:约 5000 行
  • 执行器(200+ 指令):约 8000 行
  • 查询优化器:约 3000 行
  • 工具链:约 2000 行

这个量级对人类工程师是巨大挑战,对 AI 来说是"大型项目"而非"不可能项目"。

AI 的核心价值在于"放大"人类能力:

  • 负责架构设计、边界验证、语义判断——这些需要领域知识和系统思维
  • AI 负责代码生成、模式补全、测试扩展——这些需要记忆力和执行力

这种分工让我在七天内完成了原本需要数周的工作。

但 AI 也有明确的边界:

  • 它不擅长"理解需求背后的意图"——当我告诉它"实现 MVCC"时,它给出一个教科书式的实现,却忽略了事务回滚时的版本链清理
  • 它不擅长"系统级的权衡"——面对一致性 vs 性能的抉择,它给出的是"平庸的平衡"而非"明确的立场"
  • 它不擅长"破坏性测试"——它生成的测试用例是"快乐路径",而真正的测试需要"如何让系统崩溃"的思维

我们的机会是:用更现代的架构(纯内存 + 向量化 + Go 语言),做得更快。


第二部分:架构设计——AI 能教给我们什么?

整体架构图

┌─────────────────────────────────────────────────────────────────┐
│                         sqlvibe                                  │
├─────────────────────────────────────────────────────────────────┤
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐  │
│  │   SQL CLI    │  │  HTTP API    │  │   Go SDK (pkg)       │  │
│  └──────────────┘  └──────────────┘  └──────────────────────┘  │
├─────────────────────────────────────────────────────────────────┤
│                     Query Processing (QP)                       │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐  ┌──────────┐  │
│  │ Tokenizer  │→ │  Parser    │→ │ Optimizer  │→ │ Compiler │  │
│  └────────────┘  └────────────┘  └────────────┘  └──────────┘  │
├─────────────────────────────────────────────────────────────────┤
│                     Execution Engine (QE/VM)                    │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              Vectorized Execution Engine                 │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐  │   │
│  │  │  Plan Cache │  │Result Cache │  │ Predicate Push  │  │   │
│  │  └─────────────┘  └─────────────┘  └─────────────────┘  │   │
│  │  ┌─────────────────────────────────────────────────────┐│   │
│  │  │              Register-based VM (200+ ops)           ││   │
│  │  └─────────────────────────────────────────────────────┘│   │
│  └──────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────┤
│                     Storage Engine (DS)                         │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              Hybrid Store (Row/Columnar Adaptive)        │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐    │   │
│  │  │  B-Tree     │  │ ColumnStore│  │  RoaringBitmap │    │   │
│  │  │  (Row)      │  │ (Columnar) │  │   Index        │    │   │
│  │  └─────────────┘  └─────────────┘  └─────────────────┘    │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐    │   │
│  │  │   WAL Log   │  │ Compression │  │   Backup       │    │   │
│  │  │   (MVCC)    │  │ (LZ4/ZSTD)  │  │   (Incremental)│    │   │
│  │  └─────────────┘  └─────────────┘  └─────────────────┘    │   │
│  └──────────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────────┤
│                     Transaction Manager (TM)                    │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐  │
│  │   WAL Mode  │  │   MVCC      │  │   Lock Manager         │  │
│  │             │  │  Snapshot   │  │  (Deadlock Detection)  │  │
│  └─────────────┘  └─────────────┘  └─────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

存储引擎:混合行/列式架构

这是 sqlvibe 与 SQLite 最大的架构差异。

SQLite 的存储架构:

┌─────────────────────────────────────────┐
│            SQLite Database File         │
├─────────────────────────────────────────┤
│  ┌─────────────────────────────────┐   │
│  │         B-Tree Leaf Page        │   │
│  │  ┌─────┬─────┬─────┬─────┬─────┐│   │
│  │  │Row 1│Row 2│Row 3│Row 4│Row 5││   │
│  │  │id=1 │id=2 │id=3 │id=4 │id=5 ││   │
│  │  │name │name │name │name │name ││   │
│  │  │age  │age  │age  │age  │age  ││   │
│  │  └─────┴─────┴─────┴─────┴─────┘│   │
│  └─────────────────────────────────┘   │
│                                         │
│  * 每页 4KB-64KB                        │
│  * 行数据连续存储                       │
│  * 适合点查询和写入                     │
└─────────────────────────────────────────┘

sqlvibe 的混合存储架构:

┌─────────────────────────────────────────────────────────────┐
│                   Hybrid Store                              │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Row Store (B-Tree) - 热数据 / 写入                 │   │
│  │  ┌─────┬─────┬─────┬─────┬─────┐                    │   │
│  │  │Row 1Row 2Row 3Row 4Row 5│  ← 适合点查询      │   │
│  │  └─────┴─────┴─────┴─────┴─────┘                    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Column Store - 分析查询 / 聚合                     │   │
│  │                                                       │   │
│  │   id:  [1] [2] [3] [4] [5] [6] [7] [8] ...          │   │
│  │        └───┴───┴───┴───┴───┴───┴───┴───┘            │   │
│  │                                                      │   │
│  │   name: ["A"] ["B"] ["C"] ["D"] ["E"] ["F"] ...     │   │
│  │         └─────┴─────┴─────┴─────┴─────┴─────┘        │   │
│  │                                                      │   │
│  │   age:  [20] [25] [30] [35] [40] [45] ...           │   │
│  │         └───┴───┴───┴───┴───┴───┴───┘               │   │
│  │                                                      │   │
│  │  * 列数据连续存储,SIMD 友好                         │   │
│  │  * 一次读取整列,无需行转换                          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  RoaringBitmap - 索引过滤                           │   │
│  │                                                       │   │
│  │  age > 30:  [0,0,0,1,1,1,0,1,0,0,1,1,...]           │   │
│  │             ↓ RoaringBitmap 压缩                    │   │
│  │             [3,4,5,7,10,11,...]                     │   │
│  │                                                      │   │
│  │  * O(1) 复杂度的位图交集/并集                       │   │
│  │  * 配合列式存储实现谓词下推                          │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

自适应切换策略:

Write/Point Query → Row Store (B-Tree)
                   ↓ 访问模式分析
                   ↓ 冷数据自动迁移
                   ↓
Analysis/Aggregate → Column Store (Vectorized)

执行引擎:向量化与 VM 的混合模式

传统火山模型(Volcano Model):

┌─────────────────────────────────────────┐
│           Volcano Execution             │
├─────────────────────────────────────────┤
│                                         │
│   SELECT SUM(age) FROM users            │
│              │                          │
│              ▼                          │
│   ┌─────────────────────┐               │
│   │   Table Scan       │ ← 逐行处理     │
│   └─────────────────────┘               │
│              │                          │
│              ▼                          │
│   ┌─────────────────────┐               │
│   │   Filter (age > 30) │ ← 逐行判断    │
│   └─────────────────────┘               │
│              │                          │
│              ▼                          │
│   ┌─────────────────────┐               │
│   │   Aggregate (SUM)   │ ← 逐行累加    │
│   └─────────────────────┘               │
│              │                          │
│              ▼                          │
│            Result                       │
│                                         │
│  * 函数调用开销大                      │
│  * 分支预测失败多                      │
│  * CPU 缓存命中率低                    │
└─────────────────────────────────────────┘

sqlvibe 向量化执行:

┌─────────────────────────────────────────────────────────────┐
│              Vectorized Execution                           │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   SELECT SUM(age) FROM users WHERE age > 30                │
│                           │                                  │
│                           ▼                                  │
│   ┌─────────────────────────────────────────────────────┐  │
│   │           Predicate Evaluation (Vectorized)         │  │
│   │                                                      │  │
│   │   age:  [20, 25, 30, 35, 40, 45, 50, 55, ...]     │  │
│   │   mask: [0,  0,  0,  1,  1,  1,  1,  1,  ...]     │  │
│   │              ↓ SIMD 批量比较                         │  │
│   │              ↓ 生成过滤位图                          │  │
│   └─────────────────────────────────────────────────────┘  │
│                           │                                  │
│                           ▼                                  │
│   ┌─────────────────────────────────────────────────────┐  │
│   │           Columnar Projection                       │  │
│   │                                                      │  │
│   │   filtered_age: [35, 40, 45, 50, 55, ...]           │  │
│   │                  └───┴───┴───┴───┴───┴───┘          │  │
│   │   * 连续内存,CPU 预取器高效工作                    │  │
│   └─────────────────────────────────────────────────────┘  │
│                           │                                  │
│                           ▼                                  │
│   ┌─────────────────────────────────────────────────────┐  │
│   │           Vectorized Aggregate                       │  │
│   │                                                      │  │
│   │   ┌─────────────────────────────────────────────┐  │  │
│   │   │   for i := 0; i < len(col); i += 4 {        │  │  │
│   │   │       sum += col[i] + col[i+1]              │  │  │
│   │   │            + col[i+2] + col[i+3]            │  │  │
│   │   │   }  // 手动循环展开,SIMD 友好              │  │  │
│   │   └─────────────────────────────────────────────┘  │  │
│   └─────────────────────────────────────────────────────┘  │
│                           │                                  │
│                           ▼                                  │
│                        Result: 225                           │
│                                                              │
│  * 一次处理 1024 行                                        │
│  * 缓存命中率高                                            │
│  * 分支预测失败少                                          │
└─────────────────────────────────────────────────────────────┘

多级缓存体系

sqlvibe 实现了三级缓存,这是性能提升的关键:

┌─────────────────────────────────────────────────────────────┐
│                  Query Processing Pipeline                  │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Level 1: Result Cache (FNV-1a 哈希键)              │   │
│  │                                                      │   │
│  │   Query: "SELECT * FROM users WHERE id = 1"         │   │
│  │           ↓ FNV-1a 哈希                              │   │
│  │   Key:   0x8a3f2b1c...                              │   │
│  │           ↓ 缓存命中?                                │   │
│  │   Result: 直接返回 (< 2 µs)                          │   │
│  │                                                      │   │
│  │   * 相同 SQL 文本直接返回缓存                        │   │
│  │   * LRU 淘汰策略                                     │   │
│  │   * 命中率 > 70% 时性能提升 95x                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                         │                                   │
│                         │ miss                              │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Level 2: Plan Cache (解析后执行计划复用)           │   │
│  │                                                      │   │
│  │   SQL 文本 → Tokenize → Parse → Optimize → Plan    │   │
│  │                    │                                │   │
│  │                    │ 缓存命中                        │   │
│  │                    ▼                                │   │
│  │           跳过 Tokenize/Parse,直接 Optimize         │   │
│  │                                                      │   │
│  │   * 复用 AST 和执行计划                             │   │
│  │   * 减少重复的解析开销                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                         │                                   │
│                         │ miss                              │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Level 3: Predicate Pushdown (Go 层预过滤)          │   │
│  │                                                      │   │
│  │   WHERE age > 30 AND city = 'Beijing'              │   │
│  │           ↓ 在 Go 层评估                            │   │
│  │   只返回满足条件的行到 VM                            │   │
│  │                                                      │   │
│  │   * 减少 VM 的执行负担                              │   │
│  │   * 配合 RoaringBitmap 实现 O(1) 过滤              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

内存管理:Arena 分配器

Go 的 GC 一直是性能敏感型应用的痛点。我们采用了 Arena 分配器模式:

┌─────────────────────────────────────────────────────────────┐
│              Traditional Go Memory Allocation               │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   Query 1:  func Exec() {                                   │
│                row := new(Row)    // 每次分配 heap          │
│                rows = append(rows, row)                     │
│             }                                                │
│             // GC 需要扫描所有对象                          │
│                                                              │
│   Query 2:  func Exec() {                                   │
│                row := new(Row)    // 又一次 heap 分配       │
│             }                                                │
│             // GC 压力累积                                   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│              Arena Allocation (sqlvibe)                      │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌─────────────────────────────────────────────────────┐   │
│   │              Arena Allocator                        │   │
│   │                                                      │   │
│   │   ┌─────────────────────────────────────────────┐   │   │
│   │   │  Pre-allocated: 64MB memory block          │   │   │
│   │   │                                              │   │   │
│   │   │  [████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]   │   │   │
│   │   │              ↑ used                         │   │   │
│   │   │                                              │   │   │
│   │   │  Query 1: ptr = arena.Alloc(64)             │   │   │
│   │   │  Query 2: ptr = arena.Alloc(128)            │   │   │
│   │   │  Query 3: ptr = arena.Alloc(32)             │   │   │
│   │   │                                              │   │   │
│   │   │  Query End: arena.Reset()  // 一键释放      │   │   │
│   │   │                   ↓                          │   │   │
│   │   │         GC 压力降为 0                        │   │   │
│   │   └─────────────────────────────────────────────┘   │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                              │
│   * 一次分配大块内存                                        │
│   * 查询期间从 arena 切分使用                               │
│   * 查询结束后整体释放                                      │
│   * GC 扫描时间降为 O(1)                                    │
└─────────────────────────────────────────────────────────────┘

第三部分:性能对比——数据说话

测试环境

  • CPU:AMD EPYC 7763 (64 cores @ 2.45GHz)
  • 内存:256GB
  • Go 版本:1.21+
  • 数据集:1,000 行,内存数据库 (:memory:)
  • 基准方法:两端迭代所有结果行进行端到端对比

查询性能对比

操作sqlvibeSQLite Go加速比
SELECT 全表扫描22 µs590 µs27x
SELECT WHERE (过滤)101 µs120 µs1.2x
ORDER BY72 µs328 µs4.5x
COUNT(*)4.4 µs28 µs6.4x
SUM (聚合)3.9 µs100 µs26x
GROUP BY50 µs564 µs11x
JOIN (两表)182 µs275 µs1.5x

DML 性能对比

操作sqlvibeSQLite Go加速比
INSERT 单行9.6 µs50 µs5x
INSERT 100 行 (批量)311 µs741 µs2.4x

特殊场景性能

操作sqlvibeSQLite Go加速比
结果缓存命中2 µs190 µs95x
TOP-N LIMIT 10 (10K 行)571 µs1.23 ms2.2x
COUNT(*) via PK index84 µs30 µsSQLite 胜
全表扫描 + 过滤 (10K)1.87 ms1.52 msSQLite 胜

性能数据解读

sqlvibe 胜出的场景:

  • 27 倍的 SELECT 加速:向量化执行 + 列式存储 + SIMD 友好循环展开的组合优势。当扫描全表时,连续的列数据使 CPU 预取器充分发挥作用。
  • 95 倍的缓存加速:应用层结果缓存对系统层缓存的"降维打击"。查询完全命中缓存时,甚至不需要触达存储层。
  • 11 倍的 GROUP BY 加速:列式存储避免了行数据转换,向量化聚合一次处理 1024 行。

SQLite 胜出的场景:

  • 2.8 倍的 PK COUNT(*):SQLite 的 B-Tree 索引实现经过二十年的打磨极其高效,我们的实现还有优化空间。
  • 1.2 倍的全表扫描 + 过滤:对于超大数据集(10K+ 行),SQLite 的"低每行开销"反而成为优势。

结论:sqlvibe 是"内存优先"的数据库,优势在中小规模数据(KB-GB 级别)的分析型工作负载。SQLite 是"磁盘优先"的数据库,优势在随机 I/O 场景。


第四部分:挑战——重写一个"活着的传奇"

挑战一:SQL 兼容性的深渊

SQLite 最大的陷阱是:它看起来简单,实际上深不可测。

第一轮测试:31 / 84 失败。

问题包括:

  • IN 子查询WHERE id IN (SELECT id FROM t2) 解析错误
  • GROUP_CONCAT:SEPARATOR 默认值处理错误
  • 除以零:返回 NULL 还是报错?(SQLite 选择返回 NULL)
  • 字符串比较:二分法还是 ICU 规则?(SQLite 选择 BINARY)

我们花了三天时间,一个一个修复测试用例。 这让我深刻体会到:SQLite 的兼容性是二十多年无数用户反馈和边界用例的打磨形成的"事实标准",不是"实现一个 SQL 标准"那么简单。

挑战二:事务一致性的复杂性

实现 MVCC 时,AI 给出的版本链清理方案存在一个致命 bug:事务回滚时没有正确释放旧版本。

这个错误在单线程测试中完全无法发现——每个事务都成功提交了。只有当 20 个 goroutine 并发执行读写事务时,才触发了一个极其隐蔽的数据竞争。而这样的用例生成,对 AI 而言,只是一些 tokens.

挑战三:性能优化的"最后一公里"

初版性能数据让人心凉了一半:SELECT 只快了 5 倍,GROUP BY 基本持平,且当前仍在较为初级的阶段。

问题出在"数据转换开销":虽然设计了列式存储,但执行层读取数据时,需要先将列数据"解压"成行格式,然后再"压缩"成向量化格式。这一来一回,开销巨大。

解决方案是"统一内存布局":让列数据在存储层和执行层之间零拷贝流动。

优化后的结果:SELECT 快了 27 倍,GROUP BY 快了 11 倍。


结语:站在巨人的肩膀上

第一,"重写 The Old Guys"是一个真实的需求。 AI 将告诉你在现代的语境下应如何开发。

第二,AI 辅助编程正在改变标准程序的开发方式。 一个人 + 一个 AI,可以在七天内完成一个"可用"的数据库原型。这种开发效率的提升是革命性的。

第三,性能优化需要回归硬件本质。 当摩尔定律放缓,我们不能再依赖 CPU 主频提升,而是需要精细地适配现代处理器的微架构特性。这需要更深入的 AI 编程能力——也正是人类正在为其训练和磨合的技能。