小巧、快速、可靠。
任選三項。
比檔案系統快 35%

1. 總結

SQLite 讀取和寫入小型二進位大物件(例如縮圖影像)的速度比使用 fread() 或 fwrite() 從磁碟上的個別檔案讀取或寫入相同二進位大物件快 35%¹

此外,儲存 10 KB 二進位大物件的單一 SQLite 資料庫使用的磁碟空間比將二進位大物件儲存在個別檔案中減少約 20%。

我們認為效能差異的產生原因是,從 SQLite 資料庫處理時,僅呼叫一次 open() 和 close() 系統呼叫,而使用儲存在個別檔案中的二進位大物件時,會為每個二進位大物件呼叫一次 open() 和 close()。呼叫 open() 和 close() 的開銷似乎大於使用資料庫的開銷。大小縮減的原因是,個別檔案會填補到檔案系統區塊大小的下一倍數,而二進位大物件會更緊密地封裝在 SQLite 資料庫中。

本文中的測量結果是在 2017 年 6 月 5 日那一週,使用介於 3.19.2 和 3.20.0 之間版本的 SQLite 所測量。您可期待未來版本的 SQLite 效能會更好。

1.1. 注意事項

¹上述的 35% 數字為近似值。實際執行時間會依硬體、作業系統和實驗的詳細資料而有所不同,而且也受到實際硬體上隨機效能波動的影響。有關更多詳細資料,請參閱下方的文字。請自行嘗試這些實驗。若有發現重大偏差,請在 SQLite 論壇 中回報。

35% 的數字是根據作者手邊容易取得的每部機器上執行的測試結果而來。本文的某些評論者回報,在他們的系統上,SQLite 的延遲比直接 I/O 高。我們尚未了解其中的差異。我們也看到一些跡象顯示,當使用冷檔案系統快取執行實驗時,SQLite 的效能不如直接 I/O。

因此,您的結論應該是:SQLite 的讀寫延遲與磁碟上個別檔案的讀寫延遲具有競爭力。SQLite 通常比較快。有時 SQLite 幾乎一樣快。無論如何,本文推翻了一般人認為關係資料庫一定比直接檔案系統 I/O 慢的假設。

2022 年研究 (GitHub 上的備用連結) 發現,在 Linux 上,SQLite 在實際工作負載方面的速度大約是 Btrfs 和 Ext4 的兩倍。

Jim Gray 等人研究了 Microsoft SQL Server 的 BLOB 與檔案 I/O 的讀取效能,發現對於小於 250KiB 到 1MiB 之間的 BLOB 大小,從資料庫讀取 BLOB 的速度較快。(論文)。在該研究中,即使內容保存在個別檔案中,資料庫仍會儲存內容的檔案名稱。因此,資料庫會針對每個 BLOB 進行查詢,即使只是為了擷取檔案名稱。在本文中,BLOB 的金鑰是檔案名稱,因此不需要先進行資料庫存取。由於在本文中讀取個別檔案中的內容時完全不會使用資料庫,因此直接檔案 I/O 變快的臨界值比 Gray 論文中的臨界值小。

本網站上的內部與外部 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 個檔案和/或子目錄。可以使用「export」指令的 --tree 命令列選項建立 blob 的替代磁碟表示,如下所示

./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 在讀取測試中執行得更快。您可以省略該選項,以比較執行 SQL 陳述式的 SQLite 效能。在這種情況下,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 正在執行 電力安全交易,而直接寫入磁碟則不然。若要讓測試更公平,請將 --nosync 選項加入 SQLite 寫入以停用呼叫 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 之間,總計約 1 gigabyte 的內容。資料庫頁面大小為 4KiB。所有這些測試都使用了 -DSQLITE_DIRECT_OVERFLOW_READ 編譯時期選項。測試執行多次。第一次執行用於預熱快取,且其計時已捨棄。

下方的圖表顯示從檔案系統直接讀取 blob 的平均時間,相對於從 SQLite 資料庫讀取相同 blob 所需時間。實際時間會因系統而異(例如,Ubuntu 桌面比 Galaxy S3 手機快很多)。此圖表顯示從檔案讀取 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() 從記憶體對映資料庫。

第三個圖表顯示,在 Mac 和 Android 上,從 SQLite 讀取 blob 內容的速度可以比從磁碟上的個別檔案讀取快兩倍,而在 Windows 上則快了驚人的十倍。

2.2. 寫入效能度量

寫入速度較慢。在所有系統上,使用直接 I/O 和 SQLite,寫入效能比讀取慢 5 到 15 倍。

寫入效能度量是透過用不同的 blob 取代(覆寫)整個 blob 來進行。這些實驗中的所有 blob 都是隨機且不可壓縮的。由於寫入速度比讀取速度慢很多,因此資料庫中 100,000 個 blob 中只有 10,000 個被取代。要取代的 blob 是隨機選取的,沒有特定順序。

直接寫入磁碟是使用 fopen()/fwrite()/fclose() 來完成的。預設情況下,以及在下面顯示的所有結果中,作業系統檔案系統緩衝區從未使用 fsync() 或 FlushFileBuffers() 刷新到永久儲存空間。換句話說,沒有嘗試讓直接寫入磁碟成為交易式或電源安全。我們發現,對每個寫入的檔案呼叫 fsync() 或 FlushFileBuffers() 會導致直接寫入磁碟儲存的速度比寫入 SQLite 慢約 10 倍或更多。

下一個圖表比較了 WAL 模式 中的 SQLite 資料庫更新,相對於磁碟上個別檔案的直接寫入覆蓋。 PRAGMA synchronous 設定為 NORMAL。所有資料庫寫入都在單一交易中。資料庫寫入的計時器在交易提交後停止,但在執行 檢查點 之前。請注意,與直接寫入磁碟不同,SQLite 寫入是 交易式電源安全 的,儘管同步設定為 NORMAL 而不是 FULL,但交易並非持久的。


圖表 4:SQLite 寫入延遲相對於直接檔案系統寫入。
10K blob,平均大小 10KB,隨機順序,
WAL 模式,同步 NORMAL,
不包括檢查點時間

寫入實驗的 Android 效能數字被省略,因為 Galaxy S3 上的效能測試非常隨機。完全相同的實驗連續執行兩次會產生截然不同的時間。公平地說,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. 當防毒保護開啟時,SQLite 比 Windows 上的直接磁碟寫入快很多。由於防毒軟體在 Windows 中預設為開啟,而且應該開啟,這表示 SQLite 通常比 Windows 上的直接磁碟寫入快很多。

  3. 對於所有系統以及 SQLite 和直接到磁碟的 I/O,讀取的速度比寫入快大約一個數量級。

  4. I/O 效能會根據作業系統和硬體而有很大的不同。在得出結論之前,請自行進行測量。

  5. 其他一些 SQL 資料庫引擎建議開發人員將 blob 儲存在個別檔案中,然後將檔名儲存在資料庫中。在這種情況下,資料庫必須先查詢以在開啟和讀取檔案前找到檔名,而僅將整個 blob 儲存在資料庫中會讓 SQLite 的讀寫效能快上許多。請參閱 內部與外部 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