惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 最新话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
PCI Perspectives
PCI Perspectives
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
H
Heimdal Security Blog
S
Security @ Cisco Blogs
N
News | PayPal Newsroom
J
Java Code Geeks
罗磊的独立博客
Security Archives - TechRepublic
Security Archives - TechRepublic
N
News and Events Feed by Topic
V
V2EX
WordPress大学
WordPress大学
Google Online Security Blog
Google Online Security Blog
N
News and Events Feed by Topic
www.infosecurity-magazine.com
www.infosecurity-magazine.com
月光博客
月光博客
AI
AI
小众软件
小众软件
The GitHub Blog
The GitHub Blog
MongoDB | Blog
MongoDB | Blog
A
Arctic Wolf
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
美团技术团队
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tailwind CSS Blog
S
Schneier on Security
博客园 - 三生石上(FineUI控件)
F
Full Disclosure
B
Blog RSS Feed
Forbes - Security
Forbes - Security
S
SegmentFault 最新的问题
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
人人都是产品经理
人人都是产品经理
云风的 BLOG
云风的 BLOG
Jina AI
Jina AI
Cisco Talos Blog
Cisco Talos Blog
U
Unit 42
Project Zero
Project Zero
H
Hacker News: Front Page
Y
Y Combinator Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
The Cloudflare Blog
大猫的无限游戏
大猫的无限游戏
S
Secure Thoughts
The Hacker News
The Hacker News
Microsoft Azure Blog
Microsoft Azure Blog

est の 输入 输出和出入

gitweets改版,复刻微信「朋友圈」 MiMoCode 干完活儿发通知 写作能力和 locate cost [AI] curl -NT. 导致100% CPU原因 或许「数羊」真是个有效的入睡法 唯物主义「天命」论 我的 Vibe Coding 最佳实践——ADR文档 MacOS 快速插入当前时间 locate cost 基于 git 的零拷贝静态web服务器 AI和柜台费 Sutton 论 discovery Elon Musk 五步工作法 Playlet:DLNA听歌神器 免安装app播放NAS里的歌 不修改nginx接收websocket AI 硬伤 理解LLM的范式——它就是个差分机? 路径依赖 AI 流式接口的pattern FSRS核心字段 SVG 时钟 AI的自我觉醒是必然 唐宋之变 Louis Alexander 谈英语学习 Predict Next Word Instruct 的意义 Sentry 替代品 Bugsink 安装配置 浏览器通过WebGPU上做AI推理 Indent Is All You Need 语文学习和考试 失落的学习兴趣 如何跟孩子讲学习的意义 物权 Bonsai 在 M2 安装 The Porsche Diffusion 首页和404更新 无用之用——或许LLM真的还不是AGI 发明后训练的人真是天才 看好 Taalas 码奸 claw会代替员工? 如果拿 token 抵工资会发生什么? tmux enables AIs to operate servers safely chat.est.im launches at 3.14 2026 股灾? AI 弑父 Configurable Intelligence 巴别塔,Vibe Coding和Lisp之咒 Citrini的2028智力危机 中文不是唯一的意译语言 观星有感 体细胞阶级论——当繁衍权被剥夺
grep vs sqlite 谁更适合微信聊天记录?
est · 2026-07-09 · via est の 输入 输出和出入

一个爆火的讨论

云风 @cloudwu 2026-06-29
微信的开发人员根本就不懂该怎么储存数据。这种聊天软件,文本和媒体文件分开存,文本根本就不应该保存在什么数据库(sqlite)里, 一个对话一个文本文件追加就可以了。需要搜索的时候 grep 一下性能完全符合需求。一个对话能有多少文本?一秒一个字 24 小时不间断,一年也就 30M 个字。

网上的争论都是猜测,我呢,决定让 opus 跑一局。

首先让AI去搜微信聊录表结构

微信(Android)聊天记录存储在加密 SQLite 数据库 EnMicroMsg.db 中,使用 SQLCipher(AES-256-CBC,PBKDF2 256000 轮派生密钥)。核心 message 表:

CREATE TABLE message (
    msgId      INTEGER PRIMARY KEY,  -- 本地自增 ID
    msgSvrId   INTEGER,              -- 服务器消息 ID
    type       INTEGER,              -- 1=文字, 3=图片, 34=语音, 43=视频
    isSend     INTEGER,              -- 0=接收, 1=发送
    createTime INTEGER,              -- Unix 时间戳
    talker     TEXT,                 -- wxid 或群 chatroom ID
    content    TEXT,                 -- 消息正文
    imgPath    TEXT                  -- 附件路径
);

测试设计

  • 数据量:50 万条模拟消息(模拟中度用户 ~2 年)
  • 搜索关键词微信支付服务器数据库会议周末
  • 环境:macOS Apple Silicon, Python 3.14, ripgrep 15.1, DuckDB 1.5.4, Polars 1.42
  • 每项测试 3 轮取最小值

参赛选手

分类 方案 思路
传统文本 grep (BSD) 最朴素的逐字节匹配
SIMD文本 ripgrep AVX2/NEON 并行 + 多线程
零拷贝 mmap 直接搜索 OS page cache + Python bytes.find
压缩文本 zstd 流式解压搜索 省空间,边解压边搜
索引 倒排索引 (2-gram) 搜索引擎思路,内存索引
索引 Bloom Filter 分块 概率型预过滤
RDBMS SQLite LIKE 微信的实际方案(去掉加密)
RDBMS SQLite mmap 模式 mmap I/O 加速
RDBMS FTS SQLite FTS5 (trigram) 全文搜索引擎
列式DB DuckDB contains() OLAP 列式扫描
列式DB DuckDB FTS DuckDB 的全文搜索扩展
列式文件 Parquet(zstd) + DuckDB 列式文件直接查询
DataFrame Polars lazy scan Rust实现的极速 DataFrame
DataFrame Polars in-memory 全量载入内存
并行文本 ripgrep 多文件并行 分块文件 + rg 多线程

测试结果

关键词搜索延迟(ms, 3轮最小值)

# 方案 微信支付 服务器 数据库 会议 周末 平均
1 SQLite FTS5 (trigram) 0.65 0.46 0.37 ❌² ❌² 0.31¹
2 Polars lazy scan 3.51 2.56 2.43 2.49 2.47 2.69
3 倒排索引 (2-gram) 2.71 3.26 2.78 3.24 2.37 2.87
4 Polars in-memory 2.67 3.61 2.98 3.21 3.21 3.13
5 DuckDB contains() 3.72 3.23 3.18 3.91 4.41 3.69
6 Parquet + DuckDB 4.63 4.37 4.55 4.88 4.84 4.65
7 ripgrep 多文件并行 10.65 10.26 9.48 10.32 9.05 9.95
8 DuckDB FTS (BM25) 12.53 11.77 12.85 11.82 12.78 12.35³
9 ripgrep (SIMD, 单文件) 13.24 13.77 13.40 14.14 13.10 13.53
10 mmap 直接搜索 17.00 22.62 22.86 31.36 30.52 24.87
11 Bloom Filter + 扫描 25.34 25.25 25.06 26.68 27.01 25.87
12 SQLite mmap LIKE 37.45 37.45 37.92 37.83 37.81 37.69
13 SQLite LIKE 43.13 41.40 41.50 41.51 41.46 41.80
14 grep (BSD) 139.10 136.86 141.42 122.54 124.16 132.82
15 zstd 流式解压搜索 161.32 164.22 164.56 170.23 170.73 166.21

¹ FTS5 只对 ≥3字符 的关键词有效,取3个有效关键词平均
² trigram tokenizer 无法匹配 2 字符的中文词
³ DuckDB FTS 默认 tokenizer 不支持中文,返回 0 结果(延迟仍可参考)

视觉化排名

 1.                                                            █   0.31ms SQLite FTS5
 2.                                                            █   2.69ms Polars lazy
 3.                                                            █   2.87ms 倒排索引
 4.                                                            █   3.13ms Polars in-mem
 5.                                                            █   3.69ms DuckDB contains
 6.                                                           ██   4.65ms Parquet+DuckDB
 7.                                                         ████   9.95ms rg多文件并行
 8.                                                       ██████  12.35ms DuckDB FTS
 9.                                                       ██████  13.53ms ripgrep (SIMD
10.                                                 ████████████  24.87ms mmap 直接搜索
11.                                                 ████████████  25.87ms Bloom+扫描
12.                                           ██████████████████  37.69ms SQLite mmap LIKE
13.                                         ████████████████████  41.80ms SQLite LIKE
14. ████████████████████████████████████████████████████████████ 132.82ms grep (BSD
15. ████████████████████████████████████████████████████████████ 166.21ms zstd流式解压

复合条件查询(指定用户 + 时间范围 + 关键词"会议")

方案 延迟 (ms) 倍率(vs grep)
SQLite indexed 2.80 76x
DuckDB 4.57 47x
Polars in-memory 7.02 30x
ripgrep pipe 20.47 10x
grep pipe 213.90 1x

存储大小

格式 大小 vs TSV 说明
Parquet (zstd) 8.3 MB 0.18x 列式 + 字典编码 + 压缩
zstd 压缩 TSV 12.7 MB 0.27x 纯压缩
DuckDB + FTS 26.0 MB 0.55x 含全文索引
TSV 纯文本 47.0 MB 1.00x 基线
SQLite 69.2 MB 1.47x B-tree 开销
SQLite + FTS5 116.8 MB 2.49x trigram 索引翻倍

各方案深度分析

Tier 1: 亚毫秒级(< 1ms)

SQLite FTS5 (trigram)
- 原理:对 content 字段的每个 3 字符子串建倒排索引
- 优点:查询极快(0.3-0.6ms),无需额外依赖
- 缺点:索引体积翻倍(+68MB);trigram 无法匹配 ≤2 字符的关键词
- 适用:搜索词通常 ≥3 字符的场景

Tier 2: 低毫秒级(2-5ms)

Polars (lazy/in-memory)
- 原理:Rust 实现的 DataFrame 引擎,列式内存布局 + SIMD 字符串匹配
- 优点:Parquet 文件仅 8.3MB(最小!),查询 2-3ms,复合查询也快(7ms)
- 缺点:需要加载到内存;Python 库依赖
- 杀手锏:8MB 的 Parquet 文件 + 3ms 搜索延迟,这是存储效率和速度的最佳平衡点

倒排索引 (2-gram, 内存)
- 原理:搜索引擎最经典的思路,对所有 2-gram 建 posting list
- 优点:构建仅 0.78s,查询 2.9ms,支持任意长度关键词
- 缺点:纯内存(需要序列化/加载),索引构建需要全量遍历
- 适用:append-only 数据可以增量更新索引

DuckDB contains()
- 原理:列式存储,content 列连续存放,CPU cache 友好 + SIMD 扫描
- 优点:无需专门索引即可 3.7ms;复合查询也仅 4.6ms;文件仅 26MB
- 缺点:需要 DuckDB 运行时
- 杀手锏:不建任何索引,纯靠列式布局就比 SQLite LIKE 快 11 倍

Parquet 文件 + DuckDB 零拷贝查询
- 原理:Parquet 本身就是列式格式,DuckDB 可以直接查询不需导入
- 优点:文件仅 8.3MB,不需要数据库进程,查询 4.6ms
- 缺点:每次查询需要启动 DuckDB 连接
- 杀手锏:一个 8MB 的文件就是完整的"数据库",任何语言都能读

Tier 3: 10ms 级

ripgrep 多文件并行
- 原理:把消息分块成多个文件,ripgrep 的 work-stealing 线程池并行搜索
- 优点:比单文件 ripgrep 快 ~35%(10ms vs 13.5ms)
- 缺点:文件管理复杂
- 适用:数据天然按时间分文件存储的场景

ripgrep (SIMD, 单文件)
- 原理:AVX2/NEON 每周期处理 16-32 字节,多线程(对单文件仍用单线程)
- 优点:零配置,即装即用
- 缺点:对单文件只能单线程

Tier 4: 失败/不推荐的"邪路"

Bloom Filter 分块预过滤
- 问题:中文常用 2-gram 只有 ~690 种,每个块都包含所有 n-gram,过滤率为 0
- 结论:对高频 n-gram 的数据集完全无效,白费构建时间

zstd 流式解压搜索
- 问题:Python 解压+搜索 166ms,比不压缩的 grep 还慢
- 结论:CPU 密集的解压抵消了 I/O 节省。如果数据在 SSD 上,不如直接读原文
- 可能有用的场景:数据在网络存储/HDD 上,I/O 是瓶颈时

DuckDB FTS (BM25)
- 问题:默认 tokenizer(类似 ICU word boundary)不支持中文
- 结论:需要自定义 tokenizer 或等 DuckDB 支持 trigram/CJK

mmap 直接搜索
- 表现:24.87ms,比 ripgrep 慢 2 倍
- 原因:Python 的 mmap.find() 是朴素搜索,没有 SIMD 优化
- 如果用 C/Rust 实现的 mmap + SIMD,预期接近 ripgrep 水平

推荐方案排名

如果从零设计微信聊天存储

优先级 方案 搜索延迟 存储 理由
🥇 Parquet + Polars/DuckDB 3-5ms 8 MB 存储最小、搜索极快、格式通用、append 友好
🥈 DuckDB 数据库 3.7ms 26 MB 单文件数据库、列式、SQL 查询、FTS 索引可选
🥉 SQLite + FTS5 0.3ms 117 MB 最快搜索(有索引)、但索引太大
4 ripgrep + 分块 TSV 10ms 47 MB 最简单、无依赖、人类可读

如果要"一行命令搜聊天记录"

# 方案 A#: ripgrep 搜纯文本 (~10ms)
rg "微信支付" messages.tsv

## 方案 B: DuckDB 直接查 Parquet (~5ms)
duckdb -c "SELECT * FROM read_parquet('messages.parquet') WHERE contains(content, '微信支付')"

## 方案 C: DuckDB 复合查询 (~5ms)  
duckdb -c "SELECT * FROM read_parquet('messages.parquet')
           WHERE talker='wxid_xxx' AND createTime > 1672531200
           AND contains(content, '会议')"

最终结论

"用 grep 代替 SQLite" — 部分正确,但格局太小

  1. ripgrep 比 SQLite LIKE 快 3 倍(13ms vs 42ms)— "grep 派"的论据成立
  2. 但 DuckDB/Polars 比 ripgrep 又快 4 倍(3ms vs 13ms)— 列式才是正道
  3. Parquet 比纯文本还小 5.6 倍(8MB vs 47MB)— 省空间还更快
  4. 复合查询(按人+时间+关键词)SQLite 仍然最强(2.8ms)

真正的启示

聊天记录是 append-only 数据 → 最适合列式存储 (Parquet/DuckDB)

  • 不需要 RDBMS 的事务/锁/B-tree 开销
  • 列式布局让 content 列连续存放,CPU cache 极度友好
  • zstd 字典压缩对重复模式多的聊天文本压缩率惊人(82%)
  • 追加写入只需要 append 新的 row group

微信搜索慢的根因:不是 SQLite vs grep 的问题,而是 SQLCipher 加密的 PBKDF2 256000 轮 + 行式存储对全文扫描不友好。如果用 Parquet + 硬件 AES,可以同时做到加密和极速搜索。

脑洞:终极方案

messages.parquet (8MB, zstd压缩, 硬件AES加密)
  → DuckDB/Polars 直接查询 (3-5ms)
  → 可选: 内存倒排索引 for < 1ms 搜索
  → 追加: 新消息 append 到 staging 文件, 定期 merge
  → 导出: parquet 是开放格式, 任何语言/工具可读

复现

pip install duckdb polars zstandard cryptography
python3 benchmark.py 500000     # 基础测试 (SQLite/grep)  
python3 benchmark_v2.py 500000  # 扩展测试 (DuckDB/Polars/mmap/...)

源码放在 https://github.com/est/snippets/tree/master/grep_vs_sqlite

模拟聊天记录有500,000行 一个 50MB 的 .tsv 太大了。自己让AI根据文章和最上面表结构生成一份吧。

后记

被Polars的性能震惊了。说实话如果换我肯定不会选sqlite。聊天记录又不会改。ACID的 OLTP 明显浪费