Showing Posts From
SQLite
SQLite 存几十 GB 数据的六个优化点
有个错觉——SQLite 只适合小项目。其实很多人低估了它。理论上 SQLite 单库能到 281 TB,实际几十 GB、一亿行都能稳定跑。前提是这几个优化必须做。 先给个使用边界数据量 SQLite 适不适合< 10 GB 完全没问题,甚至比 Postgres 简单10 GB ~ 100 GB 可以,需要认真优化> 100 GB 谨慎,考虑分片 / 换数据库高并发写入 不推荐,SQLite 写是全库锁(WAL 缓解但仍单写)多机共享文件 别玩,NFS 上的 SQLite 特别容易坏适合 SQLite 的典型场景:本地缓存、日志、爬虫存档、IoT 边缘节点、桌面/Electron 应用、单机分析、中小型业务。 1. WAL 模式(必开) PRAGMA journal_mode = WAL;作用:读和写可以并发(默认 journal 会锁全库) 崩溃恢复快 大量写入性能大幅提升一次性开完之后写进库设置,不用每次连接都设。 2. 调 synchronous PRAGMA synchronous = NORMAL; -- 折中,推荐 -- PRAGMA synchronous = OFF; -- 极限性能,接受掉电丢数据FULL(默认)= 每次事务 fsync 磁盘、NORMAL = 检查点时 fsync、OFF = 完全信操作系统。日志/缓存场景 NORMAL 完全够用,性能差好几倍。 3. 批量事务(提升最猛的一条) 单条 insert: for (const row of rows) { db.prepare("INSERT INTO logs VALUES (?, ?)").run(row.a, row.b); }一亿行你等到明年。改成批量: const insert = db.prepare("INSERT INTO logs VALUES (?, ?)"); const many = db.transaction((rows) => { for (const row of rows) insert.run(row.a, row.b); }); many(rows);性能能差 100 ~ 1000 倍。SQLite 每个事务外面套一次 fsync,逐条 insert 就是每行都 fsync。 4. 索引不要滥建 EXPLAIN QUERY PLAN SELECT * FROM logs WHERE user_id = ? AND ts > ?;索引越多,写入越慢(每行 insert 都要维护索引) 只给高频过滤字段建索引 复合索引按 WHERE 里最常见的过滤顺序建:(user_id, ts) 大文本 / JSON 字段别建普通索引,需要就用 FTS55. 分页别用 OFFSET -- 慢,越翻越慢:OFFSET 需要跳过前 N 行 SELECT * FROM logs ORDER BY id LIMIT 20 OFFSET 1000000;-- 快,游标分页:直接从上一页最后一个 id 之后取 SELECT * FROM logs WHERE id > ? ORDER BY id LIMIT 20;学名叫 Keyset Pagination,任何大表分页都该用。 6. 大字段不要塞 blob 图片、视频、大 JSON 直接扔 SQLite 会拖慢一切——包括那些跟大字段无关的查询。 正确做法:文件系统 / 对象存储放实体 SQLite 只存路径 / 元数据或者用 SQLite 官方推荐 的经验值:< 100 KB 存库里更快,> 100 KB 存文件系统更快。 7. 加上几个 PRAGMA PRAGMA cache_size = -100000; -- 100 MB 缓存(负数表示 KB) PRAGMA temp_store = MEMORY; -- 临时表放内存 PRAGMA mmap_size = 268435456; -- 256 MB mmap,减少 syscall PRAGMA busy_timeout = 5000; -- 遇到锁等 5 秒再报错一句话总结 WAL + 批量事务 + Keyset 分页——这三条打下来,几十 GB 的 SQLite 一点都不难。别忘了大字段挪出去、别乱建索引。
Node 内置 SQLite 报 statement has been finalized 的原因
Node 22 开始有了内置的 node:sqlite,同步 API 用起来非常顺。但启动时挂: ExperimentalWarning: SQLite is an experimental feature Failed to start server Error: statement has been finalized at readDeviceStates (src/lib/database.ts:76:38) ... code: 'ERR_INVALID_STATE'这个错的字面意思很明确:你在一个已经 finalize() 过的 prepared statement 上继续调用 all() / get() / iterate()。 三种最常见的成因 1. 手动过早 finalize const stmt = db.prepare("SELECT * FROM devices"); try { return stmt.all(); } finally { stmt.finalize(); }看上去正确。但如果 stmt.all() 返回的是迭代器(iterate())而不是数组(all()),外层还在懒消费,finalize 一走就崩。用 all() 拿到具体数组的写法没这个问题。 2. 用了 using 自动 finalize(Node 22 的新语法) export function readDeviceStates() { using stmt = db.prepare("SELECT * FROM device_states"); return stmt.iterate(); // ← 迭代器,出了作用域 stmt 就被 finalize 了 }using 借用了 TC39 的 Explicit Resource Management 提案,出作用域时会自动调 [Symbol.dispose](),也就是 finalize()。返回迭代器给外部继续用,外部一 next() 就炸。 规则:using + iterate() 不能混,要么用 all() 一次拿全部再返回,要么调用方也 using。 3. 模块级缓存了 stmt // 模块顶部 const readStmt = db.prepare("SELECT * FROM device_states");export function readDeviceStates() { return readStmt.all(); }看着挺合理——避免每次 prepare 的开销。但代码其它地方可能:单元测试用 db.close() 关掉了数据库(stmt 一起 finalize) 有热重载/HMR 重新 import 了模块(旧 stmt 引用还留着) 显式调用了 readStmt.finalize() 想清理,然后忘了之后再走这个函数就 ERR_INVALID_STATE。 推荐写法 每次现 prepare,让 GC 管理: export function readDeviceStates() { const stmt = db.prepare("SELECT * FROM device_states"); return stmt.all(); // 立刻取完,不返回 iterator }现代 SQLite 的 prepare 开销很小,不需要为性能提前优化。 要缓存的话,配套复用而不是复用 + finalize: const cache = new Map<string, ReturnType<typeof db.prepare>>();function s(sql: string) { let stmt = cache.get(sql); if (!stmt) { stmt = db.prepare(sql); cache.set(sql, stmt); } return stmt; }// 用: s("SELECT * FROM device_states").all();不在缓存生命周期外手动 finalize。数据库 close 时统一 cache.clear()。 要用 using 就一次性拿完数据: export function readDeviceStates() { using stmt = db.prepare("SELECT * FROM device_states"); return stmt.all(); // 数组,可以安全跨作用域 }一句话总结 statement has been finalized = 你正在用一个死掉的 stmt。别返回迭代器给作用域外、别在模块级缓存又手动 finalize。改成"每次 prepare + all()"最省心。
