最近在折腾SQLite的底层能力时,用Deep Seek把APSW和SQLite的关系捋了一遍。说实话,AI总结概念的能力确实强,把两者之间的层级关系、设计哲学讲得头头是道,但真正到了写代码、调接口、跑数据的时候,光靠那些概念总结远远不够——APSW和SQLite之间那些说不清的细节,全得靠实操去踩。所以这篇就把我自己折腾下来的理解、实测过的代码、踩过的坑一起写出来,给那些想从Python标准库sqlite3跳出来、真正掌控SQLite底层能力的人一条道走。
1. 先搞懂SQLite是谁,APSW又是谁
1.1 SQLite的存在感比你想的高得多
SQLite是嵌入式关系型数据库里最被低估的一个。它没有独立的服务器进程,不依赖网络端口,整个数据库就是磁盘上的一个普通文件,但偏偏把ACID事务、复杂SQL查询、索引、触发器、视图这些东西全做齐了。你在手机上随便打开一个App,里面可能就有几十个SQLite文件在悄悄干活;浏览器、路由器、智能电视、车载系统里它也是常客。Python自带的sqlite3模块能直接操作它,很多人的第一个数据库上手就是在它上面完成的。
但“能用”和“能深入用”是两回事。SQLite本身是以C语言源码发布的,所有功能都是通过C API暴露出来的。你在Python里写的sqlite3.connect(),说白了只是Python官方对这套C API做了一层包装。这层包装用起来很方便,但为了方便也砍掉了很多原始能力。当你需要的功能越过这层包装的能力边界时,你就得想别的办法了。APSW就是在这个背景下出现的另一个答案。
1.2 APSW到底做了什么
APSW这个名字全称是Another Python SQLite Wrapper,作者是Roger Binns。它的定位从名字就看得出来——也是SQLite C API的Python包装,但它和Python标准库sqlite3绑定的包装走的路线完全不一样。
APSW的核心理念可以概括成一句:和SQLite C API保持一比一的映射关系。标准库sqlite3会为了Python的易用性重新设计抽象层,比如把Connection、Cursor、Row这些Python对象包装出来,让你感觉像在用一套“Python风格的数据库API”。而APSW不干这种事,它尽可能原样地暴露SQLite C API的行为,包括底层的事务语义、连接标志位、虚拟表机制、扩展加载方式等等。换句话说,SQLite C函数能做什么,APSW就能做什么;SQLite C函数怎么叫,APSW就怎么包装。
这样一来,APSW和sqlite3就不再是“同一种东西的两个实现”,而是面向不同用户群体的两条路。sqlite3适合百分之九十的常规项目——读写数据、增删改查、事务处理。APSW适合那剩下的百分之十——需要碰虚拟表、自定义VFS、控制底层标志位、或者被sqlite3的抽象层挡住去路的人。用Deep Seek的话说,sqlite3是“为Python设计的外壳”,APSW是“为SQLite设计的大门”。这个总结挺准的。
2. APSW和Python内置sqlite3,别再说“差不多”
2.1 架构层面的差异是根本性的
很多人在网上搜“APSW和sqlite3区别”,看到的回答往往停留在“APSW更底层一点”“APSW更接近C API”这种模糊表述。真要较真起来,有几个具体的维度可以对比。
从架构上看,Python内置sqlite3模块从Python 3.x开始使用的是SQLite的C库,但这个C库怎么暴露给Python,官方做了大量“翻译”工作。比如sqlite3.connect()内部会帮你管理连接状态,Cursor对象会有自己的fetchall()风格方法,Row对象支持按列名访问,还有默认的隐式事务管理逻辑。这些设计都是为了让写应用的人更舒服。
APSW则更耿直。它把SQLite的C API近乎原样映射成Python方法,命名都尽量保持一致。你打开APSW的官方文档,看到的章节结构几乎就是SQLite C API文档的结构:Connection、Cursor、Virtual Table、VFS、Blob、Authorizer。这种设计带来的直接效果是:如果你会用C语言调用SQLite接口,你不需要额外学一套Python抽象,直接就能看懂APSW的写法。
对比下来我把几个关键差异列了个表,这么做比较直观:
| 对比维度 | Python内置sqlite3 | APSW |
|---|---|---|
| 目标定位 | 面向应用开发者的易用封装 | 面向底层控制的一比一封装 |
| 事务管理 | 模块默认隐式开启事务 | 完全暴露SQLite原生事务语义 |
| 异常体系 | 笼统的Error类型 | 细分到约束冲突等具体错误码 |
| 自定义函数 | 支持,但注册方式相对简单 | 支持,可直接映射C/C扩展方法 |
| 虚拟表/VFS | 基本不开放 | 完全开放 |
| 版本支持 | 内置SQLite版本随Python版本绑定 | 可加载系统任意版本SQLite |
| 第三方扩展 | 加载方式受限 | 可加载扩展模块 |
| API跟随程度 | 跟随Python DB-API标准 | 跟随SQLite C API标准 |
这个表格一列出来,APSW和sqlite3的定位差异就很清楚了。sqlite3的标准是“我把自己做成Python模块”,APSW的标准是“我把C库原样给你用”。
2.2 事务语义的差别在工作中最明显
普通人写SQLite代码用得最多的就是事务。但恰恰是在事务控制上,APSW和sqlite3的行为截然不同,而且这个差别如果没人点破,很容易写出藏着隐患的代码。
Python标准库sqlite3在默认配置下会执行一个“隐式事务管理”的策略:当你执行INSERT、UPDATE、DELETE这类改变数据的语句时,模块会在背后自动发起BEGIN事务。如果你不显式调用commit(),事务就一直悬着;一旦遇到异常,模块还会自动回滚。这个设计保证了“忘记提交”时数据不会一半写入,但同时也让你失去了对照SQLite原生行为的能力。
APSW不搞这种包装。它默认采用SQLite的autocommit模式,也就是SQLite C库自身的行为:每条语句独立执行,执行完就提交。如果你像写sqlite3那样写APSW,执行完INSERT之后不去手动BEGIN、COMMIT,那每一条INSERT都是独立落盘的。单独看每条语句没有错,但如果一批数据在插入一半时发生异常,已经插入的部分不会回滚。
这就带来一个重要的实操提醒:用APSW做批量写入必须显式控制事务。我自己的习惯是手动执行BEGIN,写完一批数据后执行COMMIT,中间一旦捕获到异常就执行ROLLBACK。这个和Python标准库的“隐形保底”完全不同,很多人第一次从sqlite3迁到APSW时都会在这里踩坑。建议大家在代码里用with语句包住事务范围,或者写一个简单的上下文管理器,让事务边界变得肉眼可见。
2.3 异常体系也让排查问题的效率拉开差距
日常写sqlite3代码遇到约束冲突,比如插入一条违反唯一索引的数据,Python标准库会抛一个sqlite3.IntegrityError。这对大多数场景够用了,但它不会告诉你具体是哪个约束出了问题、错误码是多少。
APSW把SQLite的错误码体系完整搬了过来,抛出的异常类型更丰富,包括apsw.ConstraintError、apsw.ConstraintUnique、apsw.ConstraintNotNull、apsw.ConstraintPrimaryKey等等。每个异常还能带着SQLite原生的扩展错误码。排查问题时你能直接定位到是唯一约束、主键约束还是非空约束出了问题,省去了自己逐条检查SQL的环节。
除了错误码,APSW还暴露了SQLite里的extended_result_codes选项。开了这个选项后,API返回的整数错误码附带扩展信息。这个能力在做深度排查和数据校验时相当有用,但在标准库sqlite3里很难接触得到。
2.4 什么时候必须换用APSW
说实话,大部分项目用sqlite3就够了,没必要为了“底层”而底层。但有几个硬场景,不用APSW还真搞不定。
第一个是做虚拟表。SQLite允许开发者实现自定义的虚拟表接口,让一张表的数据来源背后不是存储在数据库文件本身,而是可以对接外部数据源、内存计算逻辑等。Python标准库sqlite3基本屏蔽了这个机制,而APSW直接开放了apsw.VTab接口,可以按SQLite规范注册虚拟表模块。
第二个是做自定义VFS。SQLite支持把磁盘读写、日志写入等底层操作替换成自己的实现,你可以把数据库文件放在加密容器里、远程存储上,或者做内存文件系统。sqlite3没有暴露这个通道,APSW的vfs参数可以直接传自定义VFS对象。
第三个是需要精确控制SQLite版本和编译选项的场景。APSW是独立发行的包,内部会链接系统里的SQLite库,你可以自己编译对应版本的SQLite,然后让APSW加载它。而sqlite3的SQLite版本绑定在Python发行版里,想升级必须等Python版本更新。
第四个是加载需要SQLITE_EXTFUNCTION等特殊标志的第三方扩展。标准库在加载扩展时会做一次标志检查,有些功能因此打不开。APSW不会拦着你。
3. 上手实操:APSW的核心用法
3.1 安装和第一段连接代码
APSW的安装比想象中简单,正常情况下直接用pip install apsw就能装上。如果你所在平台没有预编译wheel包,它会尝试使用自带的SQLite源码进行编译,这种情况下你需要确认机器上有C编译工具链和Python开发头文件。在Rocky Linux之类的服务器系统上,提前装好gcc、python3-devel基本就不会出问题。
安装完成后先验证一下版本:
import apsw print(apsw.sqlite_lib_version())这一步会输出APSW加载的SQLite库版本号。如果输出和你预期的不一致,后面很多行为会受影响——比如某些SQLite新特性在这个版本里根本不存在。
打开数据库的方式如下:
import apsw # 打开磁盘数据库,不存在会自动创建 conn = apsw.Connection("demo.db") # 打开内存数据库 conn_m = apsw.Connection(":memory:") cursor = conn.cursor() cursor.execute("SELECT sqlite_version()") print(cursor.fetchone())APSW的连接构造函数的第一个参数是数据库路径,第二个参数是flags。默认情况下APSW会采用SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI等一组标志,比sqlite3更接近SQLite原生。如果你想用只读方式打开数据库,可以这样写:
conn_ro = apsw.Connection("file:demo.db?mode=ro", flags=apsw.SQLITE_OPEN_READONLY | apsw.SQLITE_OPEN_URI)这里用到了URI文件名格式,?mode=ro表示只读打开。这种细颗粒度的标志控制在sqlite3里是做不到的,在APSW里就是一行参数的事。
3.2 建表、插入、查询的完整示例
下面给一个麻雀虽小五脏俱全的例子,覆盖建表、插入、查询、更新这几个基础操作。假设我要做一个小型的“书籍库存”表:
import apsw conn = apsw.Connection("books.db") cursor = conn.cursor() # 创建表 cursor.execute(""" CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT NOT NULL, price REAL NOT NULL, stock INTEGER NOT NULL DEFAULT 0 ) """) # 插入单条数据 cursor.execute( "INSERT INTO books (title, author, price, stock) VALUES (?, ?, ?, ?)", ("深入理解SQLite", "张三", 89.0, 100) ) # 批量插入 book_list = [ ("APSW实战", "李四", 79.5, 50), ("数据库原理", "王五", 99.0, 30), ("嵌入式SQLite", "赵六", 69.9, 200), ] cursor.executemany( "INSERT INTO books (title, author, price, stock) VALUES (?, ?, ?, ?)", book_list ) # 查询 cursor.execute("SELECT * FROM books WHERE price > 70") for row in cursor: print(row) # 更新 cursor.execute("UPDATE books SET stock = stock - 10 WHERE title = ?", ("APSW实战",)) # 提交 conn.commit()有个小细节要注意:cursor本身是可以迭代的,每次迭代会拿到一行数据,行的类型是apsw.Row,既支持按下标访问row[0],也支持按列名访问row["title"],两者都很快。如果一次性要全部结果,也可以用cursor.fetchall()。
我看到很多做数据转换的人从Windows那边的MySQL迁到SQLite时,经常纠结SQL语法不兼容的问题。APSW走的是SQLite原生SQL解析器,行为就认准SQLite的语法标准即可,不存在“Python模块自己再翻译一遍”的中间层,这反倒让SQL表现更稳定。
3.3 事务控制实操,这部分必须仔细看
前面提过APSW默认是自动提交模式,所以写批量修改数据的代码时,一定要手动开事务。我自己习惯把事务包在一个上下文管理器里:
import apsw class Transaction: def __init__(self, conn): self.conn = conn def __enter__(self): self.conn.execute("BEGIN") return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: self.conn.execute("COMMIT") else: self.conn.execute("ROLLBACK") return False # 使用方式 conn = apsw.Connection("books.db") with Transaction(conn): cursor = conn.cursor() cursor.execute("UPDATE books SET stock = stock - 10 WHERE title = ?", ("APSW实战",)) cursor.execute("UPDATE books SET stock = stock + 5 WHERE title = ?", ("数据库原理",))这个写法比裸写BEGIN/COMMIT要安全,因为一旦代码块里抛异常,__exit__会自动执行ROLLBACK,不会留着半个事务。这段代码里有一个理解要点:APSW执行conn.execute("BEGIN")和直接执行SQL本质上没有区别,但不要同时开多个并发事务还指望SQLite自动帮你隔离开,SQLite的锁粒度在这里不会因为用了APSW就变弱。
另外,APSW支持SAVEPOINT,这个是处理复杂嵌套事务的利器。比如你在某个长事务里先插入一批数据,然后发现有一部分还需要写入另一个表,中间某一处错了又不能整个回滚,就可以用SAVEPOINT打一个存档点,出问题只回滚到存档点:
conn.execute("BEGIN") conn.execute("SAVEPOINT sp1") try: cursor.execute("INSERT INTO books (title, author, price, stock) VALUES (?, ?, ?, ?)", ("临时书", "佚名", 10.0, 10)) # 模拟一个错误 raise ValueError("手动触发异常") except ValueError: conn.execute("ROLLBACK TO sp1") conn.execute("RELEASE sp1") conn.execute("COMMIT")注意RELEASE sp1在这里的作用是释放保存点,让事务继续往前走。这个技巧在数据清洗、ETL里非常实用,不用为了一条坏数据把一批好数据全回滚掉。
3.4 扩展能力:自定义函数、URI连接、BLOB处理
APSW真正比sqlite3强的地方之一,是自定义函数的注册方式。SQLite本身就支持注册外部标量函数,你在SQL里可以像用内置函数一样调用它。在APSW里,注册一个函数就是纯Python函数,不需要额外用C扩展,这让你可以在SQL查询里直接跑Python逻辑:
import apsw import hashlib def sha256_hex(text): return hashlib.sha256(text.encode("utf-8")).hexdigest() conn = apsw.Connection("demo.db") conn.create_function("sha256_hex", 1, sha256_hex) cursor = conn.cursor() cursor.execute("SELECT sha256_hex('hello')") print(cursor.fetchone()[0])create_function的第二个参数是参数个数,这里1表示接收一个参数。函数注册后可以直接出现在SQL表达式里。这个能力的应用场景非常多,比如在数据库端做字段脱敏、哈希映射、自定义排序键计算等,省去把数据全部拉到Python内存里再处理的环节。
APSW对BLOB的支持也非常直接。SQLite的BLOB类型在APSW中就是Python的bytes,插入和查询都零转换成本:
cursor.execute("CREATE TABLE IF NOT EXISTS blobs (id INTEGER PRIMARY KEY, data BLOB)") blob_data = b"\x00\x01\x02\x03\xff" cursor.execute("INSERT INTO blobs (data) VALUES (?)", (blob_data,)) cursor.execute("SELECT data FROM blobs WHERE id = 1") result = cursor.fetchone()[0] print(type(result), result) # <class 'bytes'> b'\x00\x01\x02\x03\xff'这里值得多说一句:SQLite的BLOB在C API层面是void*加长度,Python标准库sqlite3在读取时会做复制转换,而APSW会尽量保持数据通道的薄度。虽然最终到Python手里还是bytes,但在大数据块读取时,这个环节少了一层额外的抽象开销,性能上会有一点优势。
URI连接方式前面提过一嘴,这里展开一下。APSW的默认flags里已经包含SQLITE_OPEN_URI,所以你可以直接在路径里写file:开头的URI,并携带参数:
conn = apsw.Connection("file:demo.db?mode=rw&cache=shared")常用的模式有:
mode=ro只读打开mode=rw读写打开,但不自动创建mode=rwc读写打开,不存在则创建cache=shared开启共享缓存
这个能力在做多连接共享同一个内存数据库时极其好用。SQLite本身支持:memory:数据库,但如果每个连接各自开一个:memory:,它们之间是相互隔离的。用file:memdb1?mode=memory&cache=shared这样的URI,可以让多个连接共享同一个内存数据库。这个技巧在写测试、模拟多会话并发场景时很有价值。
4. 结合几个亲测场景,聊聊APSW在真实项目里的表现
4.1 十万条数据的查询到底要多久
网上有个热搜词是“十万条数据,sqlite查询需要多久”,这说明很多人对SQLite的性能没有底。正好我之前用APSW做过一个十万行级别的测试。
测试环境是一台普通的笔记本Linux系统,数据库文件在本地NVMe SSD上。表结构是常见的日志表:id、ts、level、message。十万行数据在没有索引的情况下,执行SELECT COUNT(*) FROM logs这类全表扫描,耗时大约在几十毫秒到几百毫秒之间,具体受IO和页面大小影响。如果加上WHERE level = 'ERROR'这种条件过滤,并且这个字段没有索引,SQLite就得把十万行全扫一遍,耗时依然在几百毫秒量级。但如果你在level字段上建了索引,二次查询的耗时会降到几毫秒到十几毫秒,差距完全是数量级的。
这个测试说明一个问题:十万条数据在SQLite面前根本不算什么,问题是你的查询是否利用了索引。业务上“SQLite查询需要多久”的答案不在于SQLite本身,而在于表结构设计和查询写法。与其纠结SQLite慢,不如先跑一下EXPLAIN QUERY PLAN看看有没有走你预期中的索引:
EXPLAIN QUERY PLAN SELECT * FROM logs WHERE level = 'ERROR';APSW里也可以直接执行这条SQL,返回的文本中出现了SEARCH字样说明走了索引,出现了SCAN说明是全表扫描。这个排查思路在所有SQLite Python库上都通用。
4.2 SQLite修改字段类型的正确姿势
另一个热门问题是“sqlite修改字段的类型”。SQLite对ALTER TABLE的支持相当有限,能直接做的只有RENAME COLUMN和ADD COLUMN,不能直接修改一个已有列的类型。这个话题在网上被反复问起,是因为很多人一开始觉得SQLite像MySQL一样可以随意ALTER TABLE MODIFY COLUMN。
SQLite在修改列类型上的思路是:新建一张结构正确的表,把旧数据搬过去,删除旧表,再把新表重命名成旧表名。这个流程用APSW完全可以自己写脚本跑:
import apsw conn = apsw.Connection("demo.db") cursor = conn.cursor() # 假设把 books.price 从 REAL 改成 INTEGER cursor.execute("BEGIN") try: # 1. 创建新表,结构正确 cursor.execute(""" CREATE TABLE books_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT NOT NULL, price INTEGER NOT NULL, stock INTEGER NOT NULL DEFAULT 0 ) """) # 2. 数据搬迁 cursor.execute("INSERT INTO books_new (id, title, author, price, stock) SELECT id, title, author, price, stock FROM books") # 3. 删旧表 cursor.execute("DROP TABLE books") # 4. 重命名新表 cursor.execute("ALTER TABLE books_new RENAME TO books") conn.execute("COMMIT") except Exception: conn.execute("ROLLBACK") raise这里重点在于整个操作必须包在同一个事务里,任何一个环节失败都回滚到最初状态。如果不小心中途掉了连接,旧表还在,数据不会丢。这也是我建议用APSW控制事务的原因——对于这种需要原子性的多步骤结构变更,一个可靠的事务边界比什么都重要。
SQLite还有个“类型亲和性”的概念,就是即使你在建表时把一个字段声明为某种类型,SQLite也可能把类型转换成它的亲和类型。比如INT、BIGINT、SMALLINT都会被转成INTEGER亲和。所以当你发现“改了类型”之后实际存储行为好像没变,别惊讶,这是SQLite的既定规则。理解这个机制,你就明白为什么网上很多教程建议“与其改类型,不如从一开始设计好表结构”。
4.3 SQLite生态里的周边工具和跨语言场景
做SQLite开发,桌面端的管理工具也要备一个。db4s是DB Browser for SQLite的缩写,开源的跨平台SQLite可视化工具,在Windows、Linux、macOS上都有发行版,可以直接查看表结构、执行SQL、导入导出CSV等。我在排查数据文件问题时习惯先用db4s打开看一眼,快速确认表结构状态,然后再回APSW代码里查问题。这种“可视化工具+代码层调试”的组合效率很高。
SQLite本身和语言绑定无关,所以Python这边用APSW、C#那边用Microsoft.Data.Sqlite或System.Data.SQLite、Node.js里用better-sqlite3,操作的数据库文件格式是一致的。有人会问“Rocky Linux上C#能不能连SQLite”,答案当然是可以,微软官方的Microsoft.Data.Sqlite在.NET下是跨平台的,在Rocky Linux上用VSCode写C#项目,装好.NET SDK和对应的NuGet包,读写SQLite没有平台障碍。
如果你正好在Windows上有MySQL的数据想迁到SQLite,SQLite官方提供了一套迁移工具和技术文章,但核心思路其实就是把MySQL的表结构翻译成SQLite的建表语句,再用INSERT INTO ... SELECT的方式把数据导过去。这种迁移在APSW里同样可以实现为Python脚本,读取MySQL的结果,逐行写入SQLite。如果数据量有几百万行,记得用事务和批量插入,别一条一条提交。
5. 常见问题与排查技巧实录
5.1 这几类报错我基本都遇过
APSW使用中碰到报错一点也不稀奇,我把自己踩过的几个典型问题整理成了一张速查表:
| 典型报错 | 出现场景 | 解决办法 |
|---|---|---|
apsw.CantOpenError | 打不开数据库文件 | 检查路径、权限、是否被其他进程独占 |
apsw.ConstraintError | 违反唯一/主键/非空约束 | 根据子类型定位具体的约束来源 |
apsw.TransactionError | 事务操作顺序错误 | 确认没有在事务未结束时又开新事务 |
OperationalError: no such module: ... | 加载扩展失败 | 确认扩展编译选项与SQLite版本匹配 |
OverflowError | 数值超出SQLite整数范围 | SQLite的INTEGER是64位,检查数据范围 |
ImportError: DLL load failed | Windows上安装失败 | 安装对应Python版本的预编译wheel包 |
CantOpenError是新手最常遇到的。SQLite的锁机制决定了它是“单写多读”模型,当一个连接持有写事务没提交,另一个连接试图写入时会收到SQLITE_BUSY,也就是APSW里的BusyError。这个报错不代表数据文件坏了,而是并发访问没协调好。解决办法包括设置busy_timeout,或者通过conn.execute("PRAGMA busy_timeout = 5000")给等待重试留出时间窗口。记住,高并发写入场景其实不太适合直接用SQLite,真要强上,就得把写入请求串行化,或者用WAL模式缓解读写的互相阻塞。
5.2 排查思路和工具推荐
遇到APSW或SQLite层面的诡异问题,我的排查路径通常是这样:
先确认SQLite版本。apsw.sqlite_lib_version()输出的是APSW实际加载的版本,和Python绑定的内置版本无关。如果某些SQL语法或功能不生效,十有八九是版本太老。SQLite 3.35.0开始才支持ALTER TABLE DROP COLUMN,3.25.0开始支持RENAME COLUMN,你的版本决定你能用哪些特性。
然后用PRAGMA检查和验证:
PRAGMA integrity_check:体检整个数据库文件,返回ok说明结构正常PRAGMA journal_mode:查看日志模式,WAL模式更适合多读少写PRAGMA foreign_keys:确认外键约束是否开启,SQLite默认是不开启的,需要每个连接自己打开PRAGMA cache_size:调整页面缓存大小,批量读时影响明显
最后再配合db4s这类可视化工具,直观查看表结构和执行计划。EXPLAIN QUERY PLAN的输出虽然文本不长,但对判断索引是否生效非常关键,很多时候查询性能问题的答案不在SQLite,而在你的SQL写法上。
5.3 一个基于APSW的高效写入模板
这里分享一个我常用的大批量写入模板。假设要从一个CSV文件往SQLite导入几十万行数据,最怕的就是逐条INSERT提交导致速度慢到无法忍受。优化核心是两个:事务批量提交、预编译语句复用。
import apsw import csv def bulk_import(csv_path, db_path): conn = apsw.Connection(db_path) cursor = conn.cursor() cursor.execute("PRAGMA journal_mode = WAL") cursor.execute("PRAGMA synchronous = NORMAL") conn.execute("BEGIN") try: with open(csv_path, "r", encoding="utf-8") as f: reader = csv.reader(f) header = next(reader) # 预编译插入语句 placeholders = ",".join(["?"] * len(header)) sql = f"INSERT INTO target_table VALUES ({placeholders})" for row in reader: cursor.execute(sql, row) conn.execute("COMMIT") except Exception: conn.execute("ROLLBACK") raise这里默认表结构和CSV字段完全一致。PRAGMA synchronous = NORMAL配合WAL模式,可以在不牺牲太多安全性的前提下明显提高写入吞吐。我自己实测过,在普通SSD上十万行写入的时间,逐条自动提交往往要几十秒,改成事务批量提交后能快到1秒上下。
5.4 别忘了APSW也是一款软件,要看它的版本更新
APSW本身迭代速度不算快,但一直在维护。新版APSW经常会跟进SQLite C API的更新,增加对新版本SQLite特性的支持。如果你的代码要依赖某个特定的SQLite新功能,比如JSON函数增强、STRICT表、RETURNING子句等,记得检查APSW版本是否足够新。官方文档里的changelog写得详细,切换版本前扫一眼很有必要。
我自己现在处理一个新的数据小项目时,已经完全切到APSW了。倒不是因为它比sqlite3“高级”,而是它把SQLite的实际情况老老实实摊在我面前,不替我做决定。数据库是慢是快、事务是开是关、扩展能不能加载,我都看得一清二楚,出了问题也更好定位。对于想把SQLite用明白的Python开发者来说,APSW是一个值得花时间研究的工具。最后再分享一个细节:如果你也在做从sqlite3到APSW的迁移,第一条要注意的就是老代码里那些依赖“隐式事务”的逻辑——老老实实找到每一处数据修改,给它们加上显式的事务边界,这一步做好了,后面会省去很多莫名其妙的坑。