SQLite3读取数据库与优化的一点探索
如题。
背景
想用python开发一个离线版查词工具。其中,离线词典文件的选择是一个麻烦事。常见的离线词典格式有.mdx
, .eudic
等,但这些格式解析起来有些麻烦,有些甚至需要专有库。因此一直未能选择到合适的离线词库。
前几天逛Github时看到一个存储库:
https://github.com/skywind3000/ECDICT
这个存储库的作者收集了四百多万个英语单词和词组,构建了一个非常全面的离线词库,非常符合我的心意。看release页面,作者提供的格式除了.mdx
和 .eudic
以外,还提供了SQLite格式(.db
)的数据库文件,这意味着我们可以使用python标准库sqlite3
进行读取。
SQLite是一个轻量化的SQL数据库管理工具,支持SQL语法进行数据库操作,并提供许多种编程语言的接口,目前已成为使用最广泛的数据库,应用场景包括轻量级网站服务器、嵌入式设备、移动应用程序等的数据存储。
我们从作者的GitHub release中下载SQLite格式的词库文件并解压缩,得到stardict.db
文件,这个就是SQLite能够直接操作的数据库文件了。整个文件大小为 811.9M ,非常大(后面会提到如何减小文件大小)。
直接在SQLite中查询这个数据库(以查询单词“python”为例。需要首先在命令行中通过sqlite3
指令启动到SQLite3工具的交互式界面,然后运行下面的指令):
1 | .open stardict.db --打开数据库文件 |
输出结果如下。
1 | sqlite> .open stardict.db |
这个词库收录了单词的音标、中英文释义、各种场景下的词频统计以及单词的变形,仅此一例就可以看出这个离线词库做得很精心。
下面是在python程序中的一些探索。
python对SQLite数据库的操作
python标准库中提供了sqlite3
这个库,可以原生支持对SQLite数据库的操作。使用前需要import sqlite3
。在python中查词的方法(仍然以查询单词“python”为例):
1 | import sqlite3 |
输出结果如下。注意到,在python中,sqlite3查询数据库以后返回的是一个python list,这大大方便了后续的处理。
1 | >>> import sqlite3 |
整个离线查词工具的实现细节此处省略,源代码见GitHub存储库 ,目前仅支持在命令行环境下使用。
减小SQLite3文件的大小
我们注意到,作者提供的数据库原始文件,其大小达到了 811.9M之多,然而当我们用下面的指令导出数据到.csv
以后却发现整个CSV表格才306.0M。
1 | # 从sqlite3导出数据库到CSV文件 |
SQLite格式比CSV表格格式大了这么多,为什么呢?能不能压缩数据库文件,让他节省点空间呢?
很遗憾不能。python的sqlite3
库似乎并不支持直接读取压缩后的数据库文件;尽管有一些方法可以绕过上述限制,但总归很麻烦并且很占内存。
不过确实有方法可以降低SQLite数据库文件的体积的。一种方法是使用VACUUM
命令清理未使用的空间;另一种方法是通过PRAGMA page_size
指令修改数据库分页大小,从而提高空间利用率。
1. VACUUM
指令
当我们从Sqlite删除数据后,未使用的磁盘空间被添加到一个内在的”空闲列表”中用于存储下次插入的数据。这会导致删除数据后文件大小没有变化。然而,”空闲列表”所占用的空间对于我们一般的操作来说是没有帮助的,这意味着我们可以清理掉这部分空间。VACUUM
指令就是用来清理它们的。
1 | .open stardict.db --打开数据库文件 |
很简单,对不对?这一个指令把所有能干的事情都干完了。
不过当我们再次查看stardict.db
文件大小时,发现文件大小变化并不显著,仅仅从811.9M降低到了 762.0M 。
能不能进一步压缩空间呢?有 文章 提到可以通过设置分页大小以减少文件大小。
2. 修改分页大小
页面(PAGE)是SQLite的最小存储单元,它是表扩张和收缩的基本单位,表中的记录都存储在PAGE中。PAGE_SIZE
用来指定PAGE的大小,这个数值必须是2的指数幂,并且在512和65536之间。SQLite的不同版本有不同的默认值(v3.12之前是1024 Byte即1KB,v3.12之后是4096 Byte即4KB),一般来说使用默认值即可,但也可也修改PAGE_SIZE
以满足我们自己的需求。
根据 另一篇文章 的描述,SQLite在PAGE中的存储表记录时(在SQLite中也称为payload),会首先使用当前PAGE中剩余的存储空间,当剩余空间不够用时,会产生一个overflow page(溢出页),然后继续在溢出页中存储payload剩余的内容,空间仍然不够用时,会继续产生溢出页,以此种方式直到将payload表达完整(如下图)。不论是1KB的PAGE_SIZE
还是4KB的PAGE_SIZE
,都实在是太小了,如果一条记录占用空间超过4KB,则需要不止一次的寻址,并且为了实现高效寻址,SQLite数据库也必然会使用更多的空间存储元数据,使得数据库文件占用空间增大。
查询和修改PAGE_SIZE
的指令很简单:
1 | PRAGMA page_size; --查询PAGE_SIZE |
但是请注意,通过上述指令完成PAGE_SIZE
修改以后,并不会马上生效,需要手动更新整个数据库。
因此完整指令如下:
1 | .open stardict.db --打开数据库文件 |
或者在修改完PAGE_SIZE
以后,直接将数据表另存为一份副本,从而实现更新(参考这篇 文章 ):
1 | .open stardict.db --打开数据库文件 |
完成上述步骤以后,我们成功将 stardict.db
文件的大小减小到了 298.6M ,甚至小于导出的CSV文件(306.0M)。说明PAGE_SIZE
对SQLite数据库文件的大小确实有很大的影响。
不过最后还得补充一点,经过上述方法处理以后,虽然数据库文件大小显著减小,但查询速度也受到了一些影响而变得很慢,这和数据库索引以及数据存储方式有关。重新建立一下数据库索引,会好很多。