SQLite有一个非常实用的特性:打开数据库时把文件名写成:memory:,整个数据库就完全运行在内存里,建表、插入、查询都不落盘,速度比磁盘模式快出一个量级。这个特性常被用来做单元测试、临时计算和数据加工中转站。但内存库有个天然短板,连接一关数据就没了。于是问题来了:磁盘上已经有一个现成的数据库文件,怎么把它整个搬进内存加速处理?内存里算完的结果,又怎么安全地写回磁盘文件长期保存?这篇文章就把两边的互导方案一次讲清楚。

先搞清楚:内存库和磁盘库到底差在哪
SQLite判断一个数据库是不是内存库,依据只有一个:打开时传给它的文件名。传:memory:就是内存库,传C:\db\data.db这样的路径就是磁盘库。内存库的页面缓存、数据、索引全部驻留在进程的堆内存中,读写不经过文件系统,也没有磁盘寻道的开销,因此小数据量场景下性能优势非常明显。
有一个容易被忽视的细节::memory:打开的库是连接私有的。同一个进程里用两个连接分别打开:memory:,得到的是两个互不相干的数据库。想让多个连接共享同一份内存库,需要开启共享缓存模式,也就是在URI里加上cache=shared,写成file::memory:?cache=shared的形式,并保证所有参与共享的连接都这么写。这一点在做多线程处理时尤其重要,否则各线程会各自建库,数据对不上。
另外,内存库默认不受PRAGMA temp_store影响,临时表本来就在内存里。真正需要留意的是内存占用:一个100MB的磁盘库整体载入内存,加上页面结构的额外开销,实际占用只多不少。搬库之前先掂量一下机器的可用内存,避免搬进去之后进程被系统直接挤崩。
ATTACH加SQL语句:最直观的互导方式
SQLite允许单个连接同时挂载多个数据库,靠的就是ATTACH DATABASE语句。把磁盘文件附加到内存连接上之后,就可以直接用INSERT INTO ... SELECT在两个库之间搬数据,这是最容易被理解和上手的方式,命令行工具里同样适用。
-- 打开一个内存库
.open :memory:
-- 把磁盘上的库附加进来,取个别名叫 diskdb
ATTACH DATABASE 'C:\db\source.db' AS diskdb;
-- 逐表搬运:先建结构,再导数据
CREATE TABLE main.users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
INSERT INTO main.users SELECT id, name FROM diskdb.users;
-- 用完解除附加
DETACH DATABASE diskdb;
如果目标表结构可以照抄源表,还可以用CREATE TABLE ... AS SELECT一步到位。但要注意这种方式只复制数据和基本列类型,主键约束、索引、触发器都不会带过来,搬完之后得手工补建。所以正式项目里更稳妥的做法是先执行SELECT sql FROM sqlite_master WHERE type='table'把源库的建表语句查出来,在目标库里跑一遍,再用INSERT SELECT灌数据。
反向操作也就是内存导磁盘,逻辑完全对称:在内存连接上附加一个磁盘文件路径,把内存表写进去即可。如果附加的磁盘文件不存在,SQLite会自动创建,这正好可以当导出功能用。ATTACH方式的好处是纯SQL、任何语言的绑定都能用、还能挑着搬某几张表;缺点是库对象多的时候要写一堆语句,而且搬数据期间会同时占用内存和磁盘IO。
backup接口:官方推荐的整库搬迁方案
从SQLite 3.6.11开始,官方提供了一套专门的备份接口:sqlite3_backup_init、sqlite3_backup_step和sqlite3_backup_finish。它会按页面完整复制源库,表结构、索引、触发器、视图一样不落,还支持分步执行,特别适合在源库被其他连接使用的情况下在线拷贝。
#include <stdio.h>
#include <sqlite3.h>
/* 把 from 库整库拷贝到 to 库,nPage 为每步页数,-1 表示一次拷完 */
int copy_db(sqlite3 *to, sqlite3 *from, int nPage)
{
sqlite3_backup *bak;
int rc;
bak = sqlite3_backup_init(to, "main", from, "main");
if (bak == 0) {
fprintf(stderr, "backup_init 失败: %s\n", sqlite3_errmsg(to));
return sqlite3_errcode(to);
}
do {
rc = sqlite3_backup_step(bak, nPage);
} while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED);
sqlite3_backup_finish(bak);
return rc;
}
int main(void)
{
sqlite3 *mem, *disk;
int rc;
/* 内存导出到磁盘:C 字符串里反斜杠要写成两个 */
sqlite3_open(":memory:", &mem);
sqlite3_exec(mem, "CREATE TABLE t(a INTEGER); INSERT INTO t VALUES (1),(2);",
0, 0, 0);
sqlite3_open("C:\\db\\export.db", &disk);
rc = copy_db(disk, mem, -1);
printf("备份结果码: %d\n", rc);
sqlite3_close(disk);
sqlite3_close(mem);
return 0;
}
把参数反过来传,也就是copy_db(mem, disk, -1),就变成了磁盘导入内存。backup接口的另一个亮点是分步拷贝:每一步只复制指定数量的页面,步与步之间会释放锁,源库上的其他业务不至于被长时间卡住。对超大的磁盘库,用小步长循环调用是更友好的做法。只是要注意,如果每步之间源库持续被写入,剩余部分会自动重拷,极端情况下可能永远拷不完,这种场景建议改用WAL模式或者挑业务低峰执行。
Python标准库的sqlite3模块把这个能力封装成了Connection.backup方法,几行代码就能完成双向互导,日常脚本里用起来最省事。
import sqlite3
# 磁盘导入内存:把 disk 的内容整个拷给 mem
disk = sqlite3.connect(r"C:\db\source.db")
mem = sqlite3.connect(":memory:")
disk.backup(mem)
disk.close()
# 在内存里做批量加工
mem.execute("UPDATE users SET name = name || '_v2'")
mem.commit()
# 内存导出磁盘:把 mem 的内容整个拷给 out
out = sqlite3.connect(r"C:\db\result.db")
mem.backup(out)
out.close()
mem.close()
serialize与deserialize:二进制层面的整库镜像
SQLite 3.23新增了一对更底层的接口:sqlite3_serialize能把一个数据库序列化成内存里的一块连续字节,sqlite3_deserialize则把这块字节直接当作数据库内容挂到某个连接上。这对接口适合整库快照、跨进程传递数据、把磁盘文件一次性映射成内存库这类场景。
sqlite3 *db;
sqlite3_int64 sz = 0;
unsigned char *data;
/* 打开磁盘库并序列化成内存镜像 */
sqlite3_open("C:\\db\\source.db", &db);
data = sqlite3_serialize(db, "main", &sz, 0);
sqlite3_close(db);
/* 用镜像直接构造一个内存库 */
sqlite3_open(":memory:", &db);
sqlite3_deserialize(db, "main", data, sz, sz,
SQLITE_DESERIALIZE_FREEONCLOSE);
/* 此后 db 就是一个内容与源文件一致的内存库 */
参数里的SQLITE_DESERIALIZE_FREEONCLOSE表示连接关闭时自动释放那块镜像内存,省得自己管理;如果镜像数据只读,再加一个SQLITE_DESERIALIZE_READONLY可以避免误写。需要注意deserialize对页面尺寸有要求,目标连接的page_size要与镜像保持一致,通常的做法是在deserialize之前先执行PRAGMA page_size设成源库的值,或者干脆在一个全新连接上操作,让引擎自适应。
serialize拿到的是数据库某一时刻的完整快照,如果源库正在被写入,序列化结果可能处于中间状态,必要的话先加读锁或者配合事务保证一致性。另外这块内存默认由sqlite3_malloc分配,超大的库要确认进程能拿出一大块连续内存,否则会分配失败返回空指针,调用后务必判空。
常见坑与选型建议
第一个坑是Windows路径转义。C语言字符串里反斜杠是转义符,路径必须写成C:\\db\\data.db这种双反斜杠形式;Python里建议用原始字符串r"C:\db\data.db";写在ATTACH语句里的路径,同样受宿主语言字符串规则约束,这三层别搞混。SQLite在Windows上其实也接受正斜杠路径,但在Windows环境写代码,统一用反斜杠并正确转义更不容易出错。
第二个坑是事务提交。ATTACH方式搬数据时,有人搬完不执行COMMIT就断开连接,结果磁盘文件里什么都没有。稳妥的做法是显式写BEGIN和COMMIT。backup接口则没有这个问题,sqlite3_backup_finish返回成功就代表已经落盘。
第三个坑是内存库的生命周期。磁盘导入内存后,要确认后续所有操作都走同一个连接,或者按前面说的开启共享缓存,否则会出现数据神秘消失的假象。导出时如果目标磁盘文件已有旧数据,backup会整库覆盖,旧数据不会保留,需要增量合并的场景请改用ATTACH加INSERT SELECT按表处理。
最后给一个简单的选型结论:整库搬迁、追求完整和稳妥,首选backup接口;只想搬几张表,或者搬运过程中要做数据清洗转换,用ATTACH加SQL最灵活;需要把库当成一块字节到处传递、或者要做快照恢复,serialize与deserialize最合适。三种方式并不互斥,实际项目里经常组合使用,比如程序启动时用deserialize快速装载镜像,退出前用backup落盘保存,两头的性能和安全就都照顾到了。
SQLite内存数据库数据库备份ATTACH DATABASE修改时间:2026-09-28 12:42:22