比文件系统快 35%

1.总结

SQLite 读取和写入小 blob(例如缩略图)比相同的 blob 快 35%¹ 可以使用以下命令读取或写入磁盘上的单个文件 fread() 或 fwrite()。

此外,单个SQLite数据库保存 10 KB 的 blob 使用的磁盘空间比 将 Blob 存储在单个文件中。

性能差异出现(我们相信)是因为当 从SQLite数据库工作,open()和close()系统调用 仅调用一次,而 open() 和 close() 为每个 blob 调用一次 使用存储在单个文件中的 Blob 时。看来 调用 open() 和 close() 的开销大于开销 使用数据库。尺寸减小源于以下事实: 单个文件被填充到文件系统的下一个倍数 块大小,而 blob 更紧密地打包到 SQLite 中 数据库。

本文中的测量是在 2017-06-05 这一周进行的 在 3.19.2 和 3.20.0 之间使用SQLite版本。你可能会期待 SQLite的未来版本将表现得更好。

1.1.注意事项

¹上述 35% 的数字是近似值。实际时间有所不同 取决于硬件、操作系统和 实验的细节,以及由于随机性能波动 在实际硬件上。有关更多详细信息,请参阅下面的文本。 自己尝试实验。报告重大偏差 SQLite 论坛

35%的数字基于在每台机器上运行的测试 作者很容易手头。 本文的一些审稿人报告说,SQLite具有更高的 延迟比其系统上的直接 I/O 延迟。我们还不明白 区别。我们还看到有迹象表明SQLite没有 在运行实验时使用时执行和直接 I/O 冷文件系统缓存。

所以让你的收获是这样的:读/写延迟 SQLite 在单个文件的读/写延迟方面具有竞争力 磁盘。通常SQLite更快。有时SQLite几乎是 一样快。无论哪种方式,本文都反驳了常见的 假设关系数据库必须比直接数据库慢 文件系统 I/O。

Jim Gray和其他人研究了BLOB的读取性能 与微软SQL Server的文件I / O相比,发现读取BLOB 出 对于小于 250KiB 和 1MiB 之间的 BLOB 大小,数据库速度更快。 ()。 在该研究中,数据库仍然存储内容的文件名,即使 如果内容保存在单独的文件中。所以查阅数据库 对于每个 BLOB,即使它只是提取文件名。在此 文章,BLOB 的键是文件名,所以没有初步数据库 需要访问。因为数据库在以下情况下根本不使用。 从本文中的各个文件中读取内容,阈值 直接文件 I/O 变得比 Gray 的要小 纸。

本网站上的内部与外部 BLOB文章是一个 早期调查(大约 2011 年),使用与 Jim Gray paper — 将 blob 文件名作为条目存储在 数据库 — 但用于 SQLite 而不是 SQL Server。

2.如何进行这些测量

使用kvtest.c程序测量 I/O 性能 从 SQLite 源代码树。 要编译此测试程序,请先收集 kvtest.c 源文件 放入包含SQLite 合并源的目录中 文件 “sqlite3.c” 和 “sqlite3.h”。然后在 unix 上,运行类似 以下内容:

gcc -Os -I. -DSQLITE_DIRECT_OVERFLOW_READ \
  kvtest.c sqlite3.c -o kvtest -ldl -lpthread

或者在装有 MSVC 的 Windows 上:

cl -I. -DSQLITE_DIRECT_OVERFLOW_READ kvtest.c sqlite3.c

针对安卓编译的说明 如下所示。

使用生成的“kvtest”程序 生成具有 100,000 个随机不可压缩的测试数据库 斑点,每个斑点都有一个随机的 大小介于 8,000 到 12,000 字节之间 使用如下命令:

./kvtest init test1.db --count 100k --size 10k --variance 2k

如果需要,可以通过运行以下命令来验证新数据库:

./kvtest stat test1.db

接下来,将所有 blob 复制到目录中的各个文件中 使用如下命令:

./kvtest export test1.db test1.dir

此时,您可以测量使用的磁盘空间量 test1.db 数据库和 test1.dir 目录使用的空间 及其所有内容。在标准的 Ubuntu Linux 桌面上, 数据库文件的大小为 1,024,512,000 字节,test1.dir 目录将使用 1,228,800,000 字节的空间(根据“du -k”), 比数据库多约20%。

上面创建的“test1.dir”目录将所有 blob 放入一个 文件夹。据推测,某些操作系统将执行 当单个目录包含 100,000 个对象时,情况不佳。为了测试这一点, KVtest 程序还可以将 blob 存储在没有 每个文件夹超过 100 个文件和/或子目录。另一种选择 可以使用 --tree 创建 blob 的磁盘表示形式 “export”命令的命令行选项,如下所示:

./kvtest export test1.db test1.tree --tree

test1.dir 目录将包含 100,000 个文件 名称如“000000”、“000001”、“000002”等,但 test1.tree 目录将在子目录中包含相同的文件,例如 “00/00/00”、“00/00/01”等。测试1.目录和测试1.测试 但是,目录占用的空间量大致相同 test1.test 由于额外的目录条目,测试略大。

接下来的所有实验都对任一实验进行相同的操作 “test1.dir”或“test1.tree”。性能差异很小 无论哪种情况,无论操作系统如何,都可以测量。

测量从数据库和从 使用以下命令的单个文件:

./kvtest run test1.db --count 100k --blob-api
./kvtest run test1.dir --count 100k --blob-api
./kvtest run test1.tree --count 100k --blob-api

根据您的硬件和操作系统,您应该看到读取 从 test1.db 数据库文件比从 test1.dir 或 test1.tree 文件夹中的单个文件。结果可能会有所不同 由于缓存,从一次运行到下一次运行显着,因此建议 多次运行测试并采用平均值或最坏情况或最佳 案例,具体取决于您的要求。

数据库读取测试上的 --blob-api 选项导致 kvtest 使用 SQLite 的sqlite3_blob_read() 功能来加载 blob,而不是运行纯 SQL 语句。这有助于SQLite运行 在读取测试中速度更快。您可以省略该选项以比较 运行 SQL 语句的 SQLite 的性能。 在这种情况下,SQLite的性能仍然优于直接读取。 不如使用sqlite3_blob_read() 时那么多。 对于从单个磁盘读取的测试,将忽略 --blob-api 选项 文件。

通过添加 --update 选项来测量写入性能。这导致 斑点被另一个随机斑点覆盖 大小完全相同。

./kvtest run test1.db --count 100k --update
./kvtest run test1.dir --count 100k --update
./kvtest run test1.tree --count 100k --update

上面的写入测试并不完全公平,因为SQLite正在执行电源安全事务,而直接到磁盘的写入则不是。 要将测试置于更平等的基础上,请添加 --nosync 选项到 SQLite 写入以禁用调用 fsync() 或 FlushFileBuffers() 强制内容到磁盘,或使用 --fsync 选项 用于直接到磁盘测试以强制它们调用 fsync() 或 更新磁盘文件时的 FlushFileBuffers()。

默认情况下,kvtest 运行数据库 I/O 测量值,全部在 单个事务。使用 --multitrans 选项运行每个 blob 在单独的事务中读取或写入。--multitrans 选项使 SQLite 速度慢得多,并且与直接磁盘 I/O 相比没有竞争力。这 选项再次证明,要从 SQLite,你应该在 单个事务。

还有许多其他测试选项,可以通过运行 命令:

./kvtest help

2.1.读取性能测量值

下图显示了使用kvtest.c收集的五种不同数据 系统:

  • Win7:大约2009年的戴尔Inspiron笔记本电脑,奔腾双核 在2.30GHz,4GiB RAM,Windows7。
  • Win10:2016年联想YOGA 910,英特尔i7-7500,频率为2.70GHz, 16GiB 内存,视窗 10。
  • Mac:2015 MacBook Pro,3.1GHz英特尔酷睿i7,16GiB RAM, 苹果操作系统 10.12.5
  • Ubuntu:由英特尔i7-4770K构建的台式机,3.50GHz,32GiB RAM, 优麒麟 16.04.2 LTS
  • Android: Galaxy S3, ARMv7, 2GiB RAM

所有计算机都使用 SSD,除了 Win7 具有 硬盘。测试数据库是 100K 个 blob,大小统一 分布在 8K 和 12K 之间,总共约 1 GB 的内容。数据库页面大小 为 4KiB。-DSQLITE_DIRECT_OVERFLOW_READ 编译时选项是 用于所有这些测试。 测试运行了多次。 第一次运行用于预热缓存,其计时被丢弃。

下图显示了直接从 文件系统与从 SQLite 读取相同 blob 所需的时间 数据库。 实际时序因系统而异 (Ubuntu 桌面很多 例如,比Galaxy S3手机更快)。 此图表显示了 从文件中读取 Blob 所需的时间除以所需的时间 从数据库中。图表中最左侧的列是规范化的 从数据库中读取以供参考的时间。

在此图表中,一个 SQL 语句(“从 kv 中选择 v WHERE k=?1”) 准备一次。然后,对于每个 Blob,绑定 Blob 键值 到 ?1 参数,并计算语句以提取 斑点内容。

图表显示,在Windows10上,可以从SQLite读取内容。 数据库的速度比直接从磁盘读取的速度快大约 5 倍。 在Android上,SQLite只比从磁盘读取快35%左右。


图表 1:相对于直接文件系统读取的 SQLite 读取延迟。
100K 个 blob,每个平均 10KB,使用 SQL 的随机顺序

通过绕过SQL层可以稍微提高性能 并使用sqlite3_blob_read() 接口直接读取 Blob 内容,如下图所示:


图表 2:相对于直接文件系统读取的 SQLite 读取延迟。
100K 个 blob,平均大小 10KB,使用 sqlite3_blob_read() 进行随机排序

通过使用 SQLite 的内存映射 I/O功能,可以进一步提高性能。在下一个图表中, 整个 1GB 数据库文件是内存映射的,并且会读取 blob (按随机顺序)使用sqlite3_blob_read() 接口。 通过这些优化,SQLite的速度是Android的两倍。 或MacOS-X,比Windows快10倍以上。


图 3:相对于直接文件系统读取的 SQLite 读取延迟。
100K blob,平均大小 10KB,使用内存映射数据库中的 sqlite3_blob_read() 进行随机排序

第三个图表显示,从 SQLite 中读取 blob 内容可以是 读取 Mac 磁盘上单个文件的速度是其两倍,并且 Android,而且Windows的速度快了十倍。

2.2.写入性能测量

写入速度较慢。 在所有系统上,使用直接 I/O 和 SQLite,写入性能为 比读取慢 5 到 15 倍。

通过替换(覆盖)进行写入性能测量 具有不同 Blob 的整个 Blob。这些中的所有斑点 实验是随机的和不可压缩的。因为写太多了 比读取慢,数据库中的 100,000 个 blob 中只有 10,000 个 被替换。要替换的 blob 是随机选择的,并且 没有特定的顺序。

直接到磁盘写入是使用 fopen()/fwrite()/fclose() 完成的。 默认情况下,在下面显示的所有结果中,OS 文件系统缓冲区为 从未使用 fsync() 刷新到持久存储或 FlushFileBuffers().换句话说,没有试图使 直接到磁盘写入事务性或电源安全。 我们发现在每个文件上调用 fsync() 或 FlushFileBuffers() 书面原因直接到磁盘存储 比写入 SQLite 慢大约 10 倍或更多。

下一个图表将WAL 模式下的 SQLite 数据库更新与磁盘上单独文件的原始直接到磁盘覆盖进行比较。 PRAGMA 同步设置为正常。 所有数据库写入都在单个事务中。 数据库写入的计时器在事务处理后停止 提交,但在运行检查点之前。 请注意,SQLite 写入与直接到磁盘写入不同, 是事务性和电源安全的,尽管因为同步 设置为正常而不是完整,事务不持久。


图 4:相对于直接文件系统写入的 SQLite 写入延迟。
10K blob,平均大小 10KB,随机顺序,具有同步正常功能的 WAL 模式,

不包括检查点时间

写入实验的安卓性能数据被省略 因为Galaxy S3的性能测试是如此随机。二 连续运行完全相同的实验会给出截然不同的结果 次。而且,公平地说,SQLite在android上的性能略有不同。 比直接写入磁盘慢。

下图显示了 SQLite 与直接到磁盘的性能 当事务被禁用时(PRAGMA journal_mode=OFF) 并且PRAGMA 同步设置为关。这些设置将 SQLite 放在 与直接到磁盘写入具有同等地位,也就是说,它们使 由于系统崩溃和电源故障,数据容易损坏。


图 5:相对于直接文件系统写入的 SQLite 写入延迟。
10K 个 blob,平均大小 10KB,随机顺序,禁用日记功能,
同步关闭。

在所有写入测试中,禁用防病毒软件非常重要 在运行直接到磁盘性能测试之前。我们发现 防病毒软件将直接到磁盘的速度减慢了一个数量级 而它对SQLite写入的影响很小。这可能是由于 事实上,直接到磁盘更改了数千个单独的文件,这些文件都需要 由防病毒检查,而SQLite写入仅更改单个 数据库文件。

2.3.变化

DSQLITE_DIRECT_OVERFLOW_READ编译时选项导致 SQLite 在从溢出页面读取内容时绕过其页面缓存。这 帮助数据库读取 10K blob 运行得更快一些,但不是那么多 更快。SQLite仍然比直接文件系统读取具有速度优势 没有SQLITE_DIRECT_OVERFLOW_READ编译时选项。

其他编译时选项,例如使用 -O3 而不是 -Os 或 using-DSQLITE_THREADSAFE=0和/或其他一些推荐的编译时选项可能有助于 SQLite 运行得更快 相对于直接文件系统读取。

测试数据中 Blob 的大小会影响性能。 对于较大的 blob,文件系统通常会更快,因为 open() 和 close() 的开销在更多字节的 I/O 上摊销, 而数据库在速度和空间上都会更有效率 随着平均 blob 大小的减小。

3.一般发现

  1. SQLite 与存储在 中的 blob 具有竞争力,并且通常比存储在 磁盘上的单独文件,用于读取和写入。

  2. SQLite比在Windows上直接写入磁盘要快得多 当防病毒保护处于打开状态时。自杀毒软件 默认情况下,在Windows中处于打开状态,这意味着SQLite 通常比在 Windows 上直接写入磁盘快得多。

  3. 对于所有人来说,阅读比写作快一个数量级 系统以及 SQLite 和直接到磁盘 I/O。

  4. I/O 性能因操作系统和硬件而异。 在得出结论之前进行自己的测量。

  5. 其他一些 SQL 数据库引擎建议开发人员将 blob 存储在单独的 文件,然后将文件名存储在数据库中。在这种情况下,其中 打开之前,必须先查阅数据库以查找文件名 并读取文件,只需将整个 blob 存储在数据库中 使用 SQLite 提供更快的读写性能。 有关详细信息,请参阅内部 BLOB 与外部 BLOB一文。

4.附加说明

4.1.在安卓设备上编译和测试

kvtest 程序在 Android 上编译和运行如下。 首先安装 Android SDK 和 NDK。然后准备一个脚本 名为“android-gcc”,大致如下所示:

#!/bin/sh
#
NDK=/home/drh/Android/Sdk/ndk-bundle
SYSROOT=$NDK/platforms/android-16/arch-arm
ABIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin
GCC=$ABIN/arm-linux-androideabi-gcc
$GCC --sysroot=$SYSROOT -fPIC -pie $*

使该脚本可执行并将其放在您的$PATH上。然后 编译 KVTEST 程序如下:

android-gcc -Os -I. kvtest.c sqlite3.c -o kvtest-android

接下来,将生成的 kvtest-android 可执行文件移动到 Android 装置:

adb push kvtest-android /data/local/tmp

最后使用“adb shell”在Android设备上获取shell提示, cd 到 /data/local/tmp 目录,然后开始运行测试 与任何其他 UNIX 主机一样。