『壹』 sqlitememory原理
SQLite創建的資料庫有一種模式IN-MEMORY,但是它並不表示SQLite就成了一個內存資料庫。IN-MEMORY模式可以簡單地理解為,(2020 表述勘誤:本來創建的資料庫文件是基於磁碟的,現在整個文件使用內存空間來代替磁碟空間,沒有了文件作為backingstore,不必在修改資料庫後將緩存頁提交到文件系統),其它操作保持一致。也就是資料庫的設計沒有根本改變。
inmemory與tempdb是兩種節約模式,節約的對象為(rollback)日誌文件以及資料庫文件,減少IO。inmemory將日誌寫在內存,並且去除資料庫文件作為backingStore,緩存頁不用提交到文件系統。tempdb只會在只會在臟的緩存頁超過當前總量的25%才會同步刷寫到文件,換句話說在臨時資料庫模式下,事務提交時並不總同步臟頁,因此減少了IO數量,事務日誌也受這種機制影響,所以在臨時資料庫模式下,事務日誌是不是MEMORY並不重要。回過頭來看,內存模式則是臨時模式的一種極致,杜絕所有的IO。這兩種模式都只能存在一個sqlite3連接,關閉時銷毀。
提到內存,許多人就會簡單地理解為,內存比磁碟速度快很多,所以內存模式比磁碟模式的資料庫速度也快很多,甚至有人望文生意就把它變成等同於內存資料庫。
它並不是為內存資料庫應用而設計的,本質還是文件資料庫。它的資料庫存儲文件有將近一半的空間是空置的,這是它的B樹存儲決定的,(2020 勘誤:對於固定長度記錄,頁面使用率最大化,對於非自增計數鍵的索引,頁面一般會保留20~扒襪60%的空間,方便插入)請參看上一篇SQLite存儲格式。內春睜激存模式只是將資料庫存儲文件放入內存空間,但並不考慮最有效管理你的內存空間,其它臨時文件也要使用內存,事務回滾日誌一樣要生成,只是使用了內存空間。它的作用應該偏向於臨時性的用途。
(2020 補充:下面的測試有局限性,)
我們先來看一下下面的測試結果,分別往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