SQLite查詢大型數據集優化之CursorWindow
《SQLite權威指南》上曾提到過,「我們的建議是要時刻記住:在那些只有3英寸到4英寸大小的屏幕上顯示千行是不現實的。哪些用戶才會在一次操作中滾動或讀取如此大量的數據,特別是在這樣小屏幕的設備上?」但如果業務提出需求需要顯示如此大規模的數據,也許你會碰到與我們相似的問題,查詢大型數據集太慢。如果使用了CursorAdapter,那麼情況也許會更糟糕。
1.問題的起源
在一個包含了32000條記錄的資料庫表中查詢讀取30490條記錄,生成 30490個Java對象,耗時13S。耗時太長且無法忍受。
SQL語句 select
colum1..colum 15 from table1 where type = 2 or type = 4;如圖1.1所示,業務代碼只是簡單的new一個對象,並且把每列的值讀出來而已。
1.1時間花在哪了?
1.1.1 多次調用SQLiteQuery.fillWindow
SQLiteCursor通過CursorWindow管理數據。 CursorWindow實際上是對2MB內存進行封裝,用於存放sqlite3數據里查詢出的數據。當查詢的數據集大於CursorWindow的容量時,Cursor將會多次執行fillWindow方法(詳見第二章源碼分析) 。SQliteQuery.fillWindow方法是從查詢的數據集中提取數據放入CursorWindow中。
1.1.2 JNI間的數據轉換
Java層的CursorWindow為Native層的CursorWindow的一個外殼,它記錄了Native層CursorWindow的地址。因此cursor.getString()這些方法,實際上都是通過JNI把 C層的對象轉換成java對象進行付值。
2.為什麼這麼慢?
從源碼分析ANDROID系統是如何查詢DB數據的。
2.1 SQLiteDatabase.query
首先,分析
SQLiteDatabase類中query函數,其流程如圖 2.1。從閱讀源碼可以得知,SQLiteDatabase.query只會構造查詢信息,返回Cursor對象,並不會執行查詢操作。
2.2 SQLiteQuery.fillWindow—真正耗時的地方
真正查詢DB數據集的實現在SQLITECONNECTION.CPP的 excuterForCursorWindo方法中,fillWindow的調用順序如圖2.2。
每次執行fillWindow方法時都會先清空CursorWindow,然後,利用sqlite3_step(將指針移動到數據集中的下一條記錄)從查詢結果的數據集中第一條記錄開始遍曆數據集,依次將每行數據複製到CursorWindow中。中途如CursorWindow內存空間已滿且指定的記錄不在其中,則會清空CursorWindow繼續向下遍歷。
2.3 Cursor.onMove —刷新CursorWindow的時機
Cursor Move相關流程如圖2.4。
每次需要移動Cursor時,都會先執行onMove方法。onMove會判斷請求查詢的位置是否在cursorWindow範圍內,如在,則不會執行fillWindow方法。如超出cursorwindow的範圍,Cursor會調用fillWindow方法更新CursorWindow數據。
2.4 源碼分析總結
由於CursorWindow 2MB內存大小的限制。因此,在使用SQLITE查詢數據集時,一定要嚴格控制查詢結果數據集的大小。如果數據集過大,建議分頁查詢。因為數據集越大,導致每次執行fillwindow方法時,性能損耗越嚴重。具體數據說明詳見第四章實驗數據。
3.優化方案
經過上文分析,針對微云云相冊的業務數據,2MB的CursorWindow 最多只能容納6000多條記錄。利用cursor遍歷三萬多條數據集時,需要多次fillwindow操作。每次fillwindow開始時,都會從數據集的第一條記錄開始遍歷,無謂的性能消耗嚴重。
經過前兩章的分析,優化思路及方案如下:
1. 無需多次遍歷查詢結果,只需執行一次fillwindow方法,避免多次無效的sqlite3_step操作。
2. 如果不是及時需要用到的數據,可以暫時把其放在C層,尤其對於字元串數據。
具體優化方案為:在遍歷查詢結果數據集時,針對每條記錄在C層直接生成一個對象。然後利用CursorWindow將對象的指針地址返回給Java層。在此種方案中,CursorWindow中每行數據只存放了一個Long型數值。使得CursorWindow足夠存儲幾十萬條數據,避免無效的fillwindow性能消耗。
UML如圖3.1:
新的Cursor實現類SQLiteFastCursor通過SQLiteFastQuery實現的fillWindow方法執行fillWindow操作。SQLiteFastQuery的fillWindow方法則調用C層的executeForFastCursor方法執行具體的DB查詢操作。
其中executeForFastCursor方法為:遍曆數據集,並針對每行記錄生成一個CBean對象。然後把 CBean對象的指針地址儲存到CursorWindow中。最後Java層通過CBean指針地址建立CBean與Java層Bean對象的關聯關係。
4.實驗結果
表4.1是在小米4,android 4.4系統上做的優化實驗對比。
由上表實驗數據可知android系統的資料庫查詢方法出於對內存使用的嚴格控制,查詢性能及時間隨查詢結果數據集的增大,成災難性下降,性能損耗嚴重。而經過本文優化方案優化後,查詢性能不受數據集大小的影響,查詢耗時得到了明顯的提升。
推薦閱讀:
※Sqlite資料庫最大可以多大呀?會不會像acc資料庫那樣,幾十MB就暴掉了?
※c struct 結構體無定義, 但只用作指針, 這種用法怎麼理解?