小巧、快速、可靠。
三选二。
比文件系统快 35%

1. 摘要

SQLite 读取和写入小型 Blob(例如,缩略图图像)的速度比使用 fread() 或 fwrite() 从磁盘上的单个文件读取或写入相同的 Blob 快约 35%¹

此外,包含 10 千字节 Blob 的单个 SQLite 数据库比将 Blob 存储在单个文件中使用的磁盘空间少约 20%。

性能差异的产生(我们认为)是因为当从 SQLite 数据库中读取时,open() 和 close() 系统调用仅调用一次,而当使用存储在单个文件中的 Blob 时,则对每个 Blob 调用一次 open() 和 close()。调用 open() 和 close() 的开销似乎大于使用数据库的开销。大小减小的原因在于,单个文件会填充到文件系统块大小的下一个倍数,而 Blob 会更紧凑地打包到 SQLite 数据库中。

本文中的测量是在 2017 年 6 月 5 日当周进行的,使用的是 SQLite 3.19.2 和 3.20.0 之间的版本。您可能会期望 SQLite 的未来版本表现得更好。

1.1. 注意事项

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

35% 的数据是基于在作者手边所有机器上运行测试得出的。本文的一些审阅者报告说,在他们的系统上,SQLite 的延迟高于直接 I/O。我们尚不了解差异。我们还发现一些迹象表明,当使用冷文件系统缓存运行实验时,SQLite 的性能不如直接 I/O。

因此,您的结论应该是:SQLite 的读/写延迟与磁盘上单个文件的读/写延迟具有竞争力。通常 SQLite 速度更快。有时 SQLite 几乎一样快。无论哪种方式,本文都反驳了关系数据库必须比直接文件系统 I/O 慢的普遍假设。

一项 2022 年的研究GitHub 上的替代链接)发现,与 Linux 上的 Btrfs 和 Ext4 相比,SQLite 在现实世界的负载下大约快两倍。

Jim Gray 和其他人研究了 BLOB 与文件 I/O 的读取性能,发现对于小于 250KiB 到 1MiB 的 BLOB 大小,从数据库中读取 BLOB 速度更快。(论文)。在那项研究中,即使内容存储在单独的文件中,数据库仍然存储内容的文件名。因此,即使只是为了提取文件名,每个 BLOB 都会查询数据库。在本文中,BLOB 的键是文件名,因此不需要初步的数据库访问。因为在本文中从单个文件中读取内容时根本不使用数据库,所以直接文件 I/O 变得更快的阈值小于 Gray 论文中的阈值。

本网站上的 内部 Blob 与外部 Blob 文章是早期(大约 2011 年)的一项调查,它使用了与 Jim Gray 论文相同的方法——将 Blob 文件名存储为数据库中的条目——但适用于 SQLite 而不是 SQL Server。

2. 这些测量是如何进行的

I/O 性能使用 SQLite 源代码树中的 kvtest.c 程序进行测量。要编译此测试程序,首先将 kvtest.c 源文件收集到包含 SQLite 合并源文件“sqlite3.c”和“sqlite3.h”的目录中。然后在 Unix 上,运行以下命令

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

或在 Windows 上使用 MSVC

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

在 Android 上编译的说明如下所示

使用生成的“kvtest”程序生成一个包含 100,000 个随机不可压缩 Blob 的测试数据库,每个 Blob 的大小在 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 个文件和/或子目录。Blob 的替代磁盘表示可以使用“export”命令的 --tree 命令行选项创建,如下所示

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

test1.dir 目录将包含 100,000 个名为“000000”、“000001”、“000002”等的文件,但 test1.tree 目录将包含子目录(如“00/00/00”、“00/00/01”等)中的相同文件。test1.dir 和 test1.test 目录占用大约相同数量的空间,尽管由于额外的目录条目,test1.test 略微大一些。

所有后续实验都对“test1.dir”或“test1.tree”进行相同的操作。无论操作系统如何,在这两种情况下测量的性能差异都很小。

使用以下命令测量从数据库和单个文件中读取 Blob 的性能

./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 文件夹中的单个文件读取的速度快约 35%。由于缓存,结果可能会在每次运行之间发生很大变化,因此建议多次运行测试并根据您的要求取平均值或最坏情况或最佳情况。

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

通过添加 --update 选项来测量写入性能。这会导致 Blob 被完全相同的另一个随机 Blob 就地覆盖。

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

上述写入测试并不完全公平,因为 SQLite 正在执行 电源安全事务,而直接写入磁盘则没有。为了使测试更公平,请向 SQLite 写入添加 --nosync 选项以禁用调用 fsync() 或 FlushFileBuffers() 以强制将内容写入磁盘,或者对直接写入磁盘测试使用 --fsync 选项以强制它们在更新磁盘文件时调用 fsync() 或 FlushFileBuffers()。

默认情况下,kvtest 在单个事务中运行所有数据库 I/O 测量。使用 --multitrans 选项在单独的事务中运行每个 Blob 读取或写入。--multitrans 选项使 SQLite 变得非常慢,并且无法与直接磁盘 I/O 竞争。此选项再次证明,要获得 SQLite 的最佳性能,您应该在单个事务中尽可能多地对数据库进行交互。

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

./kvtest help

2.1. 读取性能测量

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

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

下图显示了直接从文件系统读取 Blob 的平均时间与从 SQLite 数据库读取相同 Blob 所需的时间。实际时间在不同的系统之间差异很大(例如,Ubuntu 台式机的速度远快于 Galaxy S3 手机)。此图显示了从文件读取 Blob 所需的时间与从数据库读取 Blob 所需的时间之比。图表中最左边的列是用于参考的数据库读取的标准化时间。

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

图表显示,在 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 数据库文件都被内存映射,并且使用 sqlite3_blob_read() 接口(以随机顺序)读取 Blob。通过这些优化,SQLite 的速度是 Android 或 MacOS-X 的两倍,是 Windows 的 10 倍以上。


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

第三张图表显示,从 SQLite 中读取 Blob 内容的速度可以是 Mac 和 Android 上从磁盘上的单个文件读取速度的两倍,对于 Windows 而言,速度惊人地提高了 10 倍。

2.2. 写入性能测量

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

写入性能测量是通过用不同的 Blob 替换(覆盖)整个 Blob 来进行的。这些实验中的所有 Blob 都是随机的且不可压缩的。由于写入速度比读取慢得多,因此仅替换数据库中 100,000 个 Blob 中的 10,000 个。要替换的 Blob 是随机选择的,并且没有特定的顺序。

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

下一张图表比较了SQLite数据库在WAL模式下的更新与磁盘上单独文件的原始直接覆盖。 PRAGMA synchronous设置设置为NORMAL。所有数据库写入都在单个事务中进行。数据库写入的计时器在事务提交后停止,但在运行检查点之前。请注意,与直接写入磁盘的操作不同,SQLite写入是事务性的,并且电源安全的,尽管由于同步设置是NORMAL而不是FULL,因此事务不是持久的。


图表4:SQLite写入延迟相对于直接文件系统写入。
10K个Blob,平均大小10KB,随机顺序,
使用同步NORMAL的WAL模式,
不包括检查点时间

由于Galaxy S3上的性能测试非常随机,因此省略了写入实验的Android性能数据。连续两次运行完全相同的实验会给出截然不同的时间。而且,公平地说,SQLite在Android上的性能略慢于直接写入磁盘。

下一张图表显示了在禁用事务(PRAGMA journal_mode=OFF)且PRAGMA synchronous设置为OFF的情况下,SQLite与直接写入磁盘的性能比较。这些设置使SQLite与直接写入磁盘的操作处于平等地位,也就是说,它们使数据容易受到系统崩溃和电源故障导致的损坏。


图表5:SQLite写入延迟相对于直接文件系统写入。
10K个Blob,平均大小10KB,随机顺序,
禁用日志记录,同步OFF。

在所有写入测试中,在运行直接写入磁盘的性能测试之前,务必禁用防病毒软件。我们发现防病毒软件会使直接写入磁盘的速度降低一个数量级,而对SQLite写入的影响却很小。这可能是由于直接写入磁盘会更改数千个单独的文件,所有这些文件都需要由防病毒软件进行检查,而SQLite写入仅更改单个数据库文件。

2.3. 变体

编译时选项-DSQLITE_DIRECT_OVERFLOW_READ会导致SQLite在从溢出页读取内容时绕过其页面缓存。这有助于数据库读取10K Blob的速度略有提高,但提高幅度并不大。即使没有SQLITE_DIRECT_OVERFLOW_READ编译时选项,SQLite仍然比直接文件系统读取速度快。

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

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

3. 总体发现

  1. SQLite与存储在磁盘上单独文件中的Blob具有竞争力,并且通常速度更快,无论是在读取还是写入方面。

  2. 在启用防病毒保护的Windows上,SQLite比直接写入磁盘的速度快得多。由于防病毒软件在Windows上默认启用并且应该启用,这意味着SQLite通常比Windows上的直接磁盘写入速度快得多。

  3. 对于所有系统以及SQLite和直接写入磁盘的I/O,读取速度大约比写入快一个数量级。

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

  5. 一些其他SQL数据库引擎建议开发人员将Blob存储在单独的文件中,然后将文件名存储在数据库中。在这种情况下,数据库必须首先查询以查找文件名,然后才能打开和读取文件,只需将整个Blob存储在数据库中,即可使用SQLite获得更快的读取和写入性能。有关更多信息,请参阅内部Blob与外部Blob文章。

4. 其他说明

4.1. 在Android上编译和测试

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主机上一样开始运行测试。

此页面上次修改于2023-12-05 14:43:20 UTC