SQLite性能 - 它不是內存數據庫,不要對IN-MEMORY望文生意。


SQLite創建的數據庫有一種模式IN-MEMORY,但是它並不表示SQLite就成了一個內存數據庫。IN-MEMORY模式可以簡單地理解為,本來創建的數據庫文件是基於磁盤的,現在整個文件使用內存空間來代替磁盤空間,其它操作保持一致。也就是數據庫的設計沒有根本改變。

提到內存,許多人就會簡單地理解為,內存比磁盤速度快很多,所以內存模式比磁盤模式的數據庫速度也快很多,甚至有人望文生意就把它變成等同於內存數據庫。

它並不是為內存數據庫應用而設計的,本質還是文件數據庫。它的數據庫存儲文件有將近一半的空間是空置的,這是它的B樹存儲決定的,請參看上一篇SQLite存儲格式。內存模式只是將數據庫存儲文件放入內存空間,但並不考慮最有效管理你的內存空間,其它臨時文件也要使用內存,事務回滾日志一樣要生成,只是使用了內存空間。它的作用應該偏向於臨時性的用途。

我們先來看一下下面的測試結果,分別往memory和disk模式的sqlite數據庫進行1w, 10w以及100w條數據的插入,采用一次性提交事務。另外使用commit_hook捕捉事務提交次數。

(注:測試場景為在新建的數據庫做插入操作,所以回滾日志是很小的,並且無需要在插入過程中查找而從數據庫加載頁面,因此測試也並不全面)

內存模式

磁盤模式

在事務提交前的耗時 (事務提交后的總耗時):

  1w 10w 100w
內存模式 0.04s 0.35s 3.60s
磁盤模式 0.06s (0.27s) 0.47s (0.72s) 3.95s (4.62s)

 

 

 

可以看到當操作的數據越少時,內存模式的性能提高得越明顯,事務IO的同步時間消耗越顯注。

上圖還有一組數據比較,就是在單次事務提交中,如果要為每條插入語句准備的話

  1w 10w 100w
內存模式 0.19s 1.92s 19.46s
磁盤模式 0.21s (0.35s) 2.06s (2.26s) 19.88s (20.41s)

 

 

 

我們從SQLite的設計來分析,一次插入操作,SQLite到底做了些什么。首先SQLite的數據庫操作是以頁面大小為單位的。在單條記錄插入的事務中,回滾日志文件被創建。在B樹中查找目標頁面,要讀入一些頁面,然后將目標頁面以及要修改的父級頁面寫出到回滾日志。操作目標頁面的內存映像,插入一條記錄,並在頁面內重排序(索引排序,無索引做自增計數排序,參看上一篇《SQLite數據庫存儲格式》)。最后事務提交將修改的頁面寫出到數據庫文件,成功后再刪除日志文件。在這過程中顯式進行了2次寫磁盤(1次寫日志文件,1次同步寫數據庫),還有2次隱式寫磁盤(日志文件的創建和刪除),這是在操作目錄節點。以及為查找加載的頁面讀操作。更加詳細可以參看官方文檔的討論章節《Atomic Commit In SQLite》

如果假設插入100條記錄,每條記錄都要提交一次事務就很不划算,所以需要批量操作來減少事務提交次數。假設頁面大小為4KB,記錄長度在20字節內,每頁可放多於200條記錄,一次事務提交插入100條記錄,假設這100條記錄正好能放入到同一頁面又沒有產生頁面分裂,這樣就可以在單條記錄插入事務的IO開銷耗損代價中完成100條記錄插入。

當我們的事務中,插入的數據越多,事務的IO代價就會攤得越薄,所以在插入100w條記錄的測試結果中,內存模式和磁盤模式的耗時都十分接近。實際應用場合中也很少會需要一次插入100w的數據。有這樣的需要就不要考慮SQLite。

(補充說明一下,事務IO指代同步數據庫的IO,以及回滾日志的IO,只在本文使用)

 

除了IO外,還有沒有其它地方也影響着性能。那就是語句執行。其實反觀一切,都是在對循環進行優化。

for (i = 0; i < repeat; ++i)
{
    exec("BEGIN TRANS");
    exec("INSERT INTO ...");
    exec("END TRANS");
}

批量插入:

exec("BEGIN TRANS");
for (i = 0; i < repeat; ++i)
{
    exec("INSERT INTO ...");
}
exec("END TRANS");

當我們展開插入語句的執行

exec("BEGIN TRANS");
for (i = 0; i < repeat; ++i)
{
    // unwind exec("INSERT INTO ...");
    prepare("INSERT INTO ...");
    bind();
    step();
    finalize();
}
exec("END TRANS");

又發現循環內可以移出部分語句

exec("BEGIN TRANS");
// unwind exec("INSERT INTO ...");
prepare("INSERT INTO ...");
for (i = 0; i < repeat; ++i)
{
    bind();
    step();
}
finalize();
exec("END TRANS");

這樣就得到了批量插入的最終優化模式。

所以對sql語句的分析,編譯和釋放是直接在損耗CPU,而同步IO則是在飢餓CPU。

請看下圖

分別為內存模式1w和10w兩組測試,每組測試包括4項測試

1.只編譯一條語句,只提交一次事務

2.每次插入編譯語句,只提交一次事務

3.只編譯一條語句,但使用自動事務。

4.每次插入編譯語句,並使用自動事務。

可以看到測試項目4基本上就是測試項目2和測試項目3的結果的和。

測試項目1就是批量插入優化的最終結果。

 

 

下面是探討內存模式的使用:

經過上面的分析,內存模式在批量插入對比磁盤模式提升不是太顯注的,請現在開始關注未批量插入的結果。

下面給出的是磁盤模式0.1w和0.2w兩組測試,每組測試包括4項測試

可以看到在非批量插入情況,sqlite表現很差要100秒來完成1000次單條插入事務,但絕非sqlite很吃力,因為cpu在空載,IO阻塞了程序。

再來看內存模式20w測試

可以看到sqlite在內存模式,即使在20w次的單條插入事務,其耗時也不太遜於磁盤模式100w插入一次事務。

  0.1w 0.2w 20w
內存模式(非批量插入)     15.87s
磁盤模式(非批量插入) 97.4s 198.28s  

 

 

 

  編譯1次插入語句 每次插入編譯1次語句
內存模式(20w,20w次事務) 11.10s 15.87s
磁盤模式(100w,1次事務) 4.62s 20.41s

 

 

 

 

 不能給出內存模式100w次事務的測試結果是因為程序運行出問題。

在100w的插入一次性事務測試結果,內存模式和磁盤模式相差不到1秒,這1秒就是最后大量數據庫同步到數據庫的IO時間。

再回到上面兩圖兩表的測試結果,磁盤模式在執行多事務顯得偏癱,每秒不多於10個單條插入的事務。而內存模式下執行事務的能力仍然堅挺,每秒1w次單條插入的事務也不在話下。

在實際應用中,數據隨機實時,你又不想做批量插入控制,就可以考慮用內存模式將現有的數據馬上用事務提交,不管事務提交的數據是多是少。你只要定制計划,將內存模式的數據庫同步到你的外部數據庫。因為每個內存模式的數據庫是獨立的,你同步一個內存模式的數據庫到外部的期間,就可以同時使用另一個內存模式的數據庫緩沖數據。

(上面刪除段落是根據MinGW系統的測試結果。在真機環境測試了win 7 32bit和win 7 64bit,以及在它們之上使用mingw系統,測試結果是sqlite處理1000個單條插入事務總耗時在100秒級別。而在vm環境,vm虛擬磁盤上測試了xp,linux和macosx,測試結果是sqlite處理1000個單條插入事務總耗時在10秒級別內。值得注意的是,vm虛擬磁盤不是直接操作磁盤,所以我還要另找磁盤,掛接真實的磁盤對虛擬機環境進行測試。)

更多磁盤模式的測試結果在下一篇《意想不到,但又情理之中的測試結果》。


在經過慎重考慮后,在linux和mac環境下進行了測試,驗證了一句“數據庫都構建在痛苦的操作系統之上”。上面的測試環境是MinGw,痛苦的不是windows而是在windows之上加上的一層MinGw系統,磁盤操作十分痛苦。根據在linux和mac環境的測試結果,內存模式和磁盤模式在單條插入自動事務的性能更加接近,相差只有10倍左右,由於不用在MinGW這樣的適配系統痛苦地操作磁盤,所以在其它批量插入事務的測試項目中,兩種模式的測試結果更加趨於接近。

至於你想用sqlite的內存模式作持久用途或者去媲美內存數據庫,可能不是正確的選擇。sqlite是一個體積輕巧,可以幫你管理關系型數據的嵌入式數據庫。它適應嵌入式的空間小,耗電低和占用內存有限的特殊環境。它的高效是不因為它的簡單,而在基本的數據庫查詢功能上有打折扣。它在設計上有針對性的取舍,使它更適合某些應用場合,也必然在舍的部分蹩足。

 

本篇至此結束,謝謝觀看。

后續會有":memory:","file:whereIsDb?mode=memory"以及"disk.db"這三種模式的對比。

 

測試代碼在https://github.com/bbqz007/xw/test.sqlite.in-memory.zip。(不支持VC,需要自行下載編譯sqlite。支持VC編譯的測試代碼未上傳。)

mingw測試插入1000條數據使用自動事務,即一共提交1000次事務:

運行在      總耗時

xp (vm11)            9s

win 7 64bit        200s

win 7 32bit        100s

最后補上Linux (vm11)和MacOSX (vm11)的測試結果:

Linux 2.6.32-358.el6.x86_64
cpu MHz        : 3591.699
cpu MHz        : 3591.699
----- 10000 in memory ----
repeat insert 10000 times, in 1 trans, with 1 stmt prepared
 0.04s
 0.04s
commit: 1
repeat insert 10000 times, in 1 trans, with each stmt prepared
 0.06s
 0.06s
commit: 1
repeat insert 10000 times, in auto trans(s), with 1 stmt prepared
 0.02s
 0.02s
commit: 10000
repeat insert 10000 times, in auto trans(s), with each stmt prepared
 0.06s
 0.06s
commit: 10000
---- 100000 in memory ----
repeat insert 100000 times, in 1 trans, with 1 stmt prepared
 0.11s
 0.11s
commit: 1
repeat insert 100000 times, in 1 trans, with each stmt prepared
 0.40s
 0.40s
commit: 1
repeat insert 100000 times, in auto trans(s), with 1 stmt prepared
 1.28s
 1.28s
commit: 100000
repeat insert 100000 times, in auto trans(s), with each stmt prepared
 1.76s
 1.76s
commit: 100000
---- 200000 in memory ----
repeat insert 200000 times, in 1 trans, with 1 stmt prepared
 0.23s
 0.23s
commit: 1
repeat insert 200000 times, in 1 trans, with each stmt prepared
 0.87s
 0.87s
commit: 1
repeat insert 200000 times, in auto trans(s), with 1 stmt prepared
 7.35s
 7.35s
commit: 200000
repeat insert 200000 times, in auto trans(s), with each stmt prepared
 9.10s
 9.10s
commit: 200000
--- 1000000 in memory ----
repeat insert 1000000 times, in 1 trans, with 1 stmt prepared
 1.23s
 1.23s
commit: 1
repeat insert 1000000 times, in 1 trans, with each stmt prepared
 4.39s
 4.39s
commit: 1
 
 
------ 1000 in disk ----
rm: 無法刪除"test.db": 沒有那個文件或目錄
repeat insert 1000 times, in 1 trans, with 1 stmt prepared
 0.00s
 0.00s
commit: 1
repeat insert 1000 times, in 1 trans, with each stmt prepared
 0.00s
 0.00s
commit: 1
repeat insert 1000 times, in auto trans(s), with 1 stmt prepared
 0.80s
 0.80s
commit: 1000
repeat insert 1000 times, in auto trans(s), with each stmt prepared
 0.87s
 0.87s
commit: 1000
------ 2000 in disk ----
repeat insert 2000 times, in 1 trans, with 1 stmt prepared
 0.00s
 0.00s
commit: 1
repeat insert 2000 times, in 1 trans, with each stmt prepared
 0.01s
 0.02s
commit: 1
repeat insert 2000 times, in auto trans(s), with 1 stmt prepared
 1.60s
 1.60s
commit: 2000
repeat insert 2000 times, in auto trans(s), with each stmt prepared
 2.27s
 2.27s
commit: 2000
----- 10000 in disk ----
repeat insert 10000 times, in 1 trans, with 1 stmt prepared
 0.01s
 0.02s
commit: 1
repeat insert 10000 times, in 1 trans, with each stmt prepared
 0.04s
 0.04s
commit: 1
---- 100000 in disk ----
repeat insert 100000 times, in 1 trans, with 1 stmt prepared
 0.11s
 0.11s
commit: 1
repeat insert 100000 times, in 1 trans, with each stmt prepared
 0.45s
 0.45s
commit: 1
--- 1000000 in disk ----
repeat insert 1000000 times, in 1 trans, with 1 stmt prepared
 1.27s
 1.34s
commit: 1
repeat insert 1000000 times, in 1 trans, with each stmt prepared
 4.51s
 4.57s
commit: 1
Linux測試結果

MacOSX的測試結果:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM