← 返回信息流
AI 资讯Hacker News·3 天前

SQLite就够了

原标题:SQLite Is All You Need

速览

SQLite是一款轻量级嵌入式数据库引擎。这句流行句式暗示在某些场景下,SQLite能够替代更复杂的数据库系统。它强调简单性和可靠性,尤其在边缘计算或移动应用中具有优势。

AI 深度解读

背景

2025年4月,Hacker News上一篇题为“SQLite Is All You Need”的博文引发了广泛讨论。作者受到Evan Hahn关于“在SQLite中使用STRICT表”一文的启发,开始认真思考:如果把SQLite当作一个真正存储生产数据的、带类型的数据库来用,它到底能走多远?为了验证这一想法,作者团队基于SQLite构建了一个完整的社交网络应用Chirp,并进行了极限压力测试。测试结果出人意料:一个343MB的单文件、一个Node进程,在一台笔记本电脑上就能支撑每秒315万次请求。这一发现挑战了“小型项目用SQLite,大型项目上Postgres”的默认认知,也促使开发者重新审视数据库选型中的惯性与实际需求。

核心内容

测试项目:Chirp社交网络

选择社交网络作为压力测试的原因在于其核心查询不可避免:首页时间线需要将你发布的帖子与我关注的人进行关联,按时间排序,并统计点赞数。这种查询无法通过缓存绕过第一次请求,且随着社交图谱的增长会越来越慢。

作者构建的Chirp社交网络具备以下特征:

  • 单文件存储:整个应用的数据存放在一个名为chirp.db的文件中,大小343MB,承载了5万用户和100万条帖子。
  • 所有表均为STRICT模式:严格确保列类型,避免隐式类型转换。
  • 后端架构:单个Node进程(Node 22),使用better-sqlite3驱动(SQLite 3.49.2),无数据库服务进程、无连接字符串、无端口5432、无容器。
  • 生产配置仅需5个pragma
    PRAGMA journal_mode = WAL;
    PRAGMA synchronous = NORMAL;
    PRAGMA mmap_size = 268435456;  -- 256MB
    PRAGMA page_size = 4096;
    PRAGMA cache_size = -20000;    -- 20MB
    
    整个配置无需任何资源预配步骤。

关键性能数据

测试硬件:Apple M1笔记本电脑,8核CPU,16GB RAM。所有测试结果均为四次运行的中位数,运行间波动约10%。

端到端HTTP负载测试(真实Node服务器、真实Socket、真实JSON序列化,50并发连接,使用autocannon): | 端点 | 请求/秒 | 每日请求量 | |------|---------|------------| | 点读(单行查询) | 51,190 | 44亿 | | 最重查询(时间线,关联250万关注与100万帖子,并统计点赞) | 3,654 | 3.15亿 |

注意:最重查询是在并发写入的情况下运行的,单Node进程,单核。如果你的产品每天超过3.15亿次时间线加载,你不在99.99%的范围内,你早就知道自己是谁。其余的人都是在为一个可以放进一个文件的工作负载争论连接池。

原始SQL查询性能(每次运行在新进程中,使用数据库的干净副本,运行三次取中位数): | 操作 | 吞吐量 | |------|--------| | 点读 | 87,000+ 次/秒 | | 时间线查询 | 17,581 次/秒 | | 写入(真实提交、持久化、事务性) | 23,000 次/秒 |

每个写入都是真实、已提交、持久的事务,不是批处理、缓冲区或队列。23,000次提交事务/秒,在笔记本上,外键约束打开。

WAL模式:决定性因素

过去对SQLite的常见批评是“它锁表”。一个写入者占据数据库,其他人必须等待。这一批评针对的是回滚日志模式(rollback journal),该模式自2010年以来对大多数应用来说已经是错误的默认选项。

Write-Ahead Logging (WAL) 改变了问题的格局:写入者向日志追加记录,而不是就地修改页面,因此读取者可以在写入进行时继续读取最后一次提交的快照。读取者不会阻塞写入者,写入者也不会阻塞读取者。

作者进行了对比测试:7个读取线程执行时间线查询,先单独运行,然后与一个每秒写入1000次的写入者并行,再与一个全速写入的写入者并行。分别在WAL和回滚日志模式下运行:

| 模式 | 场景 | 读取吞吐量(次/秒) | p99延迟 | |------|------|-------------------|---------| | journal_mode=WAL | 无写入者 | 17,581 | - | | journal_mode=WAL | 写入者1000次/秒 | 15,530 | 4.40ms | | journal_mode=WAL | 写入者全速 | 14,300 | - | | journal_mode=DELETE | 无写入者 | 17,500 | - | | journal_mode=DELETE | 写入者1000次/秒 | 2,792 | 133ms | | journal_mode=DELETE | 写入者全速 | 2,500 | 800ms(个别请求) |

在WAL模式下,所有场景中零SQLITE_BUSY错误,不是“几乎没有”,而是零。

局限性:数字在哪里停止

作者坦诚地列出了SQLite的短板,以免读者只看到好的一面。

1. 并发写入下读取性能下降 当没有写入者时,7个读取线程达到17,581次/秒。加入一个仅1000次/秒的写入者后,读取降至2,792次/秒,下降6.3倍。即使写入增加到14,000次/秒,读取吞吐量也不会进一步恶化。原因在于缓存失效:读取者之前的速度来自256MB的内存映射窗口和热页面缓存。每次提交会使映射页面失效,导致读取者回退到真实I/O和重新验证。关闭mmap后,纯读取性能从17,069次

查看原文 →dbpro.app