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
MacOSX的測試結果: