Sqlite簡易性能優化方案,給你的應用插上“翅膀”

語言: CN / TW / HK

highlight: vs theme: devui-blue


本文正在參加「金石計劃 . 瓜分6萬現金大獎」

最近對數據庫進行了一番優化,增加耗時統計,以及優化現有的sql語句操作,減少讀寫耗時,這篇文章就從三個方面來講述下相關的知識點,希望能對你有所幫助。

準備工作

由於下面的優化操作需要一個操作環境,所以我説下當前我做了哪些準備工作:

  1. 數據表的結構

    image.png

  2. 映射的數據Model類

kotlin class UserInfo { @JvmField var xuhao = 0 @JvmField var name = "" @JvmField var height = 0L @JvmField var weight = 0.0f @JvmField var married = false @JvmField var update_time = "" }

  1. 準備好的數據

準備工作期間,為了下面的優化對比操作,我已經向該數據庫的表中插入了幾萬條數據:

image.png

接下來我們就開始具體的實驗工作吧。

一. 善於使用事務

操作數據庫讀寫時一定要記得開啟事務,事務的好處大家都知道,特別是某次sql指令執行的非常頻繁的時候,開啟事務能極大減少讀寫磁盤的次數,減少讀寫執行耗時

為了驗證上面的結論,我們分別通過開啟事務和不開啟事務,一次性向數據庫執行2500次update操作,對比下這兩個條件下具體的執行耗時。

1. 不開啟事務

代碼如下:

kotlin fun updateByCommon() { for (i in 0..2500) { val update = "update user_info set update_time = 'qy2_$i' where age = $i" userDBHelper.writableDatabase.execSQL(update) } }

耗時統計使用的是kotlin官方庫提供的擴展函數measureTimeMillis{}非常的方便:

kotlin mBinding.tsUpdate.setOnClickListener { measureTimeMillis { updateByTs() }.also { Log.i("SqliteActivity", "tsUpdate: time = $it") } }

接下來我們該場景下的耗時:

image.png

我總共執行了4次,差不多不開啟事務一次性執行2500次update操作的耗時平均為7.2s左右。可以看到,相當的耗時。

2. 開啟事務

實驗環境和上面的相同,除了開啟事務來執行2500次update更新操作,先看下實驗代碼:

kotlin fun updateByTs() { writeDb.beginTransaction() for (i in 0..2500) { val update = "update user_info set update_time = 'qy1_$i' where age = $i" userDBHelper.writableDatabase.execSQL(update) } writeDb.setTransactionSuccessful() writeDb.endTransaction() } 最終的執行耗時如下:

image.png

執行了4次,每次的平均耗時為5.2s左右,可以看到開啟事務相比較於不開啟事務執行,2500次的update操作耗時減少了大概2s

這就的優化就結束了嗎?不,我們還可以再減少幾百ms,下面的SQLiteStatement會帶給你更大的驚喜哈!!

二. 善於顯示創建SQLiteStatement

這個SQLiteStatement是什麼呢,其實平常我們調用SQLiteDatabaseinsertupdate等操作,最終都會將sql語句和對應的參數包裝成一個SQLiteStatement對象,通過該對象執行最終的增刪改查操作:

SQLiteDatabase.update()源碼:

image.png

image.png

SQLiteDatabase.insert()源碼:

image.png

image.png

從源碼中都會看到,每一次insertupdate操作都會創建一個SQLiteStatement對象,像我們第一小節做的實驗,執行了2500次update操作,那就相當於頻繁創建了2500個SQLiteStatement對象,想想都可怕。

所以這裏的優化就是:在執行update操作之前,顯示創建一個SQLiteStatement對象,通過?佔位符bindXXX系列方法實現一次創建、多次update的操作。

説的再多不如數説話,我們直接開啟測試,先看下相關代碼:

```kotlin fun updateBySQLiteStatement() { val statement = writeDb.compileStatement("update user_info set update_time = ? where age = ?") writeDb.beginTransaction()

for (i in 0..2500) {
    statement.clearBindings()
    statement.bindString(1, "qy2_$i")
    statement.bindLong(2, i.toLong())
    statement.executeUpdateDelete()
}
writeDb.setTransactionSuccessful()
writeDb.endTransaction()

} ```

通過compileStatement()方法顯示創建一個SQLiteStatement對象,其中clearBindings()是清理上次綁定的數據,接下來我們看下執行耗時:

image.png

平均耗時為5.1s,相比較開啟事務讀寫,減少了100ms左右,不多但也是一個優化角度。

PS:顯示創建SQLiteStatement的方式在我們項目中的優化很明顯,至少減少了百分之50的耗時,不知道這裏為啥就優化了那麼一丟丟哎!!

三. 如何準確統計query耗時?

如果我們要統計一個query查詢操作的耗時,直接在query相關的sql語句執行前和執行後統計耗時是不準確的,因為的查詢的耗時還要包括Cursor的讀取。最正確的方法是在Cursor執行close()操作後再去統計查詢耗時。

所以這裏的優化操作是,在基類查詢方法向外部暴漏Cursor對象時,對這個暴漏的Cursor藉助靜態代理的思想增加一層包裝,這樣就可以監聽到Cursorclose()方法執行時機,在這個方法回調中插入我們最終的 耗時統計代碼:

  1. 首先創建一個靜態代理對象,並支持傳入close()監聽回調

```kotlin class CursorProxy( private val cursor: Cursor, private val mCloseCallback: () -> Unit ) : Cursor by cursor {

override fun close() {
    cursor.close()
    mCloseCallback()
}

} ```

這裏我們藉助kotlin的by類委託關鍵字實現了更為簡單的靜態代理,以及通過mCloseCallback這個函數類型傳入外部的監聽回調代碼,比如這裏的最終耗時統計代碼。

  1. 在基類查詢方法對外暴漏CursorProxy

```kotlin public Cursor query(String sql) { //耗時統計開啟 Cursor cursor = db.rawQuery(sql, null);

return new CursorProxy(cursor, new Function0<Unit>() {
    @Override
    public Unit invoke() {
        //耗時統計結束代碼
        return null;
    }
});

} ```

總結

本篇文章主要是講解了sql操作的一些優化操作,減少執行耗時,同時也給出了準確統計query查詢耗時的優化方案,本篇文章不難理解,希望能對你有所幫助。