关注「索引目录」公众号,获取更多干货。
有一个显而易见的瓶颈——我们搜索行的方式。
事实证明,只需重新思考查找操作的设计方式,就能大幅提升性能WHERE。无需重写业务逻辑,也无需更换数据库,只需改变搜索键的形式即可。
是啊,它的速度提升得如此之快,简直令人难以置信。
真正的痛苦
许多应用程序都有类似这样的查询:
SELECT * FROM table
WHERE a = ? AND b = ? AND c = ? AND d = ?;
你一开始只有一个条件,然后又添加了两个条件,突然间,每次查找都需要比较多个字符串或文本字段。
小规模使用时没问题。但一旦读取流量增加或并行运行查询,性能就会急剧下降,因为 SQLite 必须:
-
在关键比较中权衡多个值 -
比较可变长度文本 -
深入探索 B 树的更深层节点。 -
存储更大的键 → 每页键数更少 → 更多 B 树层级和比较次数。
所以我尝试了一种不同的方法:将多个标识字段打包到一个哈希列中,并对该哈希列进行索引。
这并非什么高深莫测的科学——但却是真正意义上的变革。
诀窍在于:将标识字段哈希到一个整数中
我没有按(cluster, name)字符串搜索,而是构建了一个类似 URL 的字符串并对其进行哈希处理:
export function hashUrlToKey(url: string): string {
const hash = crypto.createHash('sha256').update(url).digest();
return hash.readBigInt64BE(0).toString(); // 8 bytes → BIGINT
}
现在,不再是:
WHERE cluster=? AND name=?
我们确实这样做:
WHERE url_hash = ?
在该列上创建索引/主键(最好如此WITHOUT ROWID),然后——SQLite 每一步只比较一个 8 字节的整数,而不是多个文本值。
基准测试:对约 52k 行的表进行 50,000 次查找
我们运行了两次完全相同的基准测试——一次使用哈希 BIGINT 键查找,一次使用复合文本查找。其他条件均未改变。
哈希查找 — url_hash(BIGINT,不含 ROWID)
╔════════════════════════════════════════════════════════════════════╗
║ SVG Icon URL Lookup Benchmark ║
╚════════════════════════════════════════════════════════════════════╝
📋 Configuration:
Icon rows: 51,898
URL entries: 51,898
Total queries: 50,000 (fixed)
Worker configs: [1, 2]
CPU pinning: Workers on CPU [0, 1]
Hash algorithm: SHA256 (first 8 bytes as INTEGER)
Table structure: WITHOUT ROWID (clustered by url_hash)
⚡ Running benchmarks...
2p × 1w (2 workers, 25000 queries each) ... ✓ 0.670s
2p × 2w (4 workers, 12500 queries each) ... ✓ 0.824s
══════════════════════════════════════════════════════════════════════════════════════════════════════════════
📈 RESULTS - SVG ICON URL LOOKUP PERFORMANCE
══════════════════════════════════════════════════════════════════════════════════════════════════════════════
┌──────┬────────────┬──────────┬──────────┬──────────────┬──────────────┬─────────────┬──────────────┐
│ Rank │ Config │ Proc×Wrk │ Queries │ Duration (s) │ QPS │ Avg Query │ vs Slowest │
├──────┼────────────┼──────────┼──────────┼──────────────┼──────────────┼─────────────┼──────────────┤
│ 1 │ 2p-1w │ 2×1 │ 50,000 │ 0.670 │ 74,680 │ 13.39µs │ 1.23x │
│ 2 │ 2p-2w │ 2×2 │ 50,000 │ 0.824 │ 60,680 │ 16.48µs │ 1.00x │
└──────┴────────────┴──────────┴──────────┴──────────────┴──────────────┴─────────────┴──────────────┘
📌 Summary:
🎯 Workload: 50,000 URL lookups
⚡ Best config: 2p-1w → 0.670s
🚀 Peak QPS: 74,680.4 queries/second
⏱️ Avg lookup: 13.39µs per URL
✅ Performance validated
复合字符串查找 — cluster + name(文本 + 文本)
╔════════════════════════════════════════════════════════════════════╗
║ SVG Icon URL Lookup Benchmark ║
╚════════════════════════════════════════════════════════════════════╝
📋 Configuration:
Icon rows: 51,898
URL entries: 51,898
Total queries: 50,000 (fixed)
Worker configs: [1, 2]
CPU pinning: Workers on CPU [0, 1]
Lookup strategy: Cluster + name equality lookup
Table structure: WITHOUT ROWID (cluster + name columns)
⚡ Running benchmarks...
2p × 1w (2 workers, 25000 queries each) ... ✓ 1.012s
2p × 2w (4 workers, 12500 queries each) ... ✓ 1.112s
══════════════════════════════════════════════════════════════════════════════════════════════════════════════
📈 RESULTS - SVG ICON URL LOOKUP PERFORMANCE
══════════════════════════════════════════════════════════════════════════════════════════════════════════════
┌──────┬────────────┬──────────┬──────────┬──────────────┬──────────────┬─────────────┬──────────────┐
│ Rank │ Config │ Proc×Wrk │ Queries │ Duration (s) │ QPS │ Avg Query │ vs Slowest │
├──────┼────────────┼──────────┼──────────┼──────────────┼──────────────┼─────────────┼──────────────┤
│ 1 │ 2p-1w │ 2×1 │ 50,000 │ 1.012 │ 49,391 │ 20.25µs │ 1.10x │
│ 2 │ 2p-2w │ 2×2 │ 50,000 │ 1.112 │ 44,975 │ 22.23µs │ 1.00x │
└──────┴────────────┴──────────┴──────────┴──────────────┴──────────────┴─────────────┴──────────────┘
📌 Summary:
🎯 Workload: 50,000 URL lookups
⚡ Best config: 2p-1w → 1.012s
🚀 Peak QPS: 49,391.36 queries/second
⏱️ Avg lookup: 20.25µs per URL
TL;DR 比较
同一台机器,
同一张表,
相同的查询次数,
相同的工作线程,
相同的 WAL 模式
唯一区别:
url_hash = 一个 8 字节整数;
cluster+name = 两个文本比较
结果:
实际应用中,BIGINT 查找速度提升了约 1.5 倍。
为什么哈希算法有帮助
SQLite索引是B树。搜索成本是O(n^2) ,但每一步O(log N)比较的内容至关重要。
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这不是魔法——只是降低你已经使用的结构中每个步骤的成本。
如果你的应用已经有一些类似于身份字段的条件(例如 user_id + device_id + type、cluster + name + version 等),就不要再单独处理它们了。把它们合并到一个哈希表中并建立索引。
并行负载也很重要。
当运行 2 个进程 × 多个工作进程猛烈攻击数据库时(就像昨天的帖子中那样),这种改进就更加明显了。
使用多列文本查找时,工作进程会更频繁地争用 CPU 资源;而使用哈希整数查找时,它们则能轻松应对。
这就是两者的区别:
-
CPU正忙于执行有用的二叉树遍历 -
CPU 浪费周期比较字符串。
真正有价值的收获(而非空谈理论)
这不仅仅关乎图标或网址路径。
如果您有 3-4 个始终同时出现的 WHERE 等式条件,请将它们哈希到一个合成键中,并对该列建立索引。
它:
-
简化每次查找 -
缩小索引页 -
提高缓存密度 -
显著提高读取并发速度
而且你不需要重新设计整个数据库。
你只需要添加一列和一个索引。
最坏的情况是,一切照旧。
最好的情况是,你可以像我刚才那样获得免费的性能提升。
尝试以下情况:
✔ 查找基于相等性,而非范围
✔ WHERE 子句中多个字段始终同时出现
✔ 您正在进行大量读取操作或并发操作
✔ 您的键是长字符串或 UUID
结语
人们总是喜欢争论 SQLite 和 Postgres 哪个更好。但实际上,在 SQLite 中,只需添加一个存储哈希值的列,
就能获得50% 以上的性能提升。
关注「索引目录」公众号,获取更多干货。

