Android 存儲(chǔ)選項(xiàng)之 SQLite 優(yōu)化那些事兒

閃存
Android 存儲(chǔ)優(yōu)化系列專題
  • SharedPreferences 系列

Android 之不要濫用 SharedPreferences
Android 之不要濫用 SharedPreferences(2)— 數(shù)據(jù)丟失

  • ContentProvider 系列(待更)

《Android 存儲(chǔ)選項(xiàng)之 ContentProvider 啟動(dòng)過(guò)程源碼分析》
《Android 存儲(chǔ)選項(xiàng)之 ContentProvider 深入分析》

  • 對(duì)象序列化系列

Android 對(duì)象序列化之你不知道的 Serializable
Android 對(duì)象序列化之 Parcelable 深入分析
Android 對(duì)象序列化之追求完美的 Serial

  • 數(shù)據(jù)序列化系列(待更)

《Android 數(shù)據(jù)序列化之 JSON》
《Android 數(shù)據(jù)序列化之 Protocol Buffer 使用》
《Android 數(shù)據(jù)序列化之 Protocol Buffer 源碼分析》

  • SQLite 存儲(chǔ)系列

Android 存儲(chǔ)選項(xiàng)之 SQLiteDatabase 創(chuàng)建過(guò)程源碼分析
Android 存儲(chǔ)選項(xiàng)之 SQLiteDatabase 源碼分析
數(shù)據(jù)庫(kù)連接池 SQLiteConnectionPool 源碼分析
SQLiteDatabase 啟用事務(wù)源碼分析
SQLite 數(shù)據(jù)庫(kù) WAL 模式工作原理簡(jiǎn)介
SQLite 數(shù)據(jù)庫(kù)鎖機(jī)制與事務(wù)簡(jiǎn)介
SQLite 數(shù)據(jù)庫(kù)優(yōu)化那些事兒


該篇文章屬于 SQLite 存儲(chǔ)系列的最后一篇,簡(jiǎn)單回顧下前面, Android 系統(tǒng)為支撐 SQLite 提供了 SQLiteDatabase 框架,它可以說(shuō)是整個(gè)數(shù)據(jù)庫(kù)框架最重要的一個(gè)類,內(nèi)部維護(hù)了數(shù)據(jù)庫(kù)連接池管理、并發(fā)訪問(wèn)、事務(wù)等核心管理。整體上看這套框架可以較高效的完成 SQLite 數(shù)據(jù)庫(kù)的訪問(wèn)操作。不過(guò)它仍然存在一些注意和優(yōu)化的地方,今天就來(lái)聊一聊 SQLite 優(yōu)化相關(guān)內(nèi)容。

關(guān)于 SQLite 的優(yōu)化內(nèi)容真的非常多,個(gè)人也在不斷地學(xué)習(xí)和探索中,好在它有大量的資料供我們參考,遇到陌生或者不懂的地方還需要結(jié)合參考資料反復(fù)學(xué)習(xí)理解。今天我就選擇一些相對(duì)比較重要的優(yōu)化點(diǎn)整理出來(lái)供大家參考。

1. ORM

可能大部分應(yīng)用為了提高開(kāi)發(fā)效率,會(huì)引入 ORM 框架。ORM(Object Relational Mapping)也就是對(duì)象關(guān)系映射,用面向?qū)ο蟮母拍畎褦?shù)據(jù)庫(kù)中表和對(duì)象關(guān)聯(lián)起來(lái),可以讓我們不用關(guān)心數(shù)據(jù)庫(kù)底層的實(shí)現(xiàn)。

Android 中最常用的 ORM 框架有開(kāi)源 greenDAO 和 Google 官方的 Room,那使用 ORM 框架會(huì)帶來(lái)什么問(wèn)題呢?

使用 ORM 框架真的非常簡(jiǎn)單,但是這種簡(jiǎn)單易用性是需要犧牲部分執(zhí)行效率為代價(jià)的,具體的損耗跟 ORM 框架寫的好不好很有關(guān)系。但可能更大的問(wèn)題是讓很多的開(kāi)發(fā)者的思維固化,不但不能正確地寫出高效的 SQL 語(yǔ)句,最后可能連簡(jiǎn)單的 SQL 語(yǔ)句都不會(huì)寫了

為了提高開(kāi)發(fā)效率,應(yīng)用的確應(yīng)該引入 ORM 框架。但是這不能是我們不去學(xué)習(xí)數(shù)據(jù)庫(kù)基礎(chǔ)知識(shí)的理由,只有理解底層的一些機(jī)制,我們才能更加得心應(yīng)手地解決疑難問(wèn)題。

2. 進(jìn)程與線程并發(fā)

如果我們?cè)陧?xiàng)目中使用 SQLite,那么在下面這個(gè) SQLiteDatabaseLockedExecption 就是經(jīng)常會(huì)出現(xiàn)的一個(gè)問(wèn)題。

android.database.sqlite.SQLiteDatabaseLoekedException: database is locked 
    at android.database.sqlite.SQLiteDatabase.dbopen
    at android.database.sqlite.SQLiteDatabase.openDatabase
    at android.database.sqlite.SQLiteDatabase.openDatabase

SQLiteDatabaseLockedException 歸根到底是因?yàn)椴l(fā)導(dǎo)致,而 SQLite 的并發(fā)有兩個(gè)維度,一個(gè)是多進(jìn)程并發(fā),一個(gè)是多線程并發(fā)。

多進(jìn)程并發(fā)

SQLite 默認(rèn)是支持多進(jìn)程并發(fā)操作的,它通過(guò)文件鎖來(lái)控制多進(jìn)程并發(fā)。SQLite 鎖的粒度并沒(méi)有非常細(xì),它針對(duì)的是整個(gè) DB 文件,內(nèi)部有 5 個(gè)狀態(tài),具體你可以參考下面的文章。

簡(jiǎn)單來(lái)說(shuō),多進(jìn)程可以同時(shí)獲取 SHARED 鎖來(lái)讀取數(shù)據(jù),但是只有一個(gè)進(jìn)程可以獲取 EXCLUSIVE 鎖來(lái)寫數(shù)據(jù)庫(kù)。并且 EXCLUSIVE 會(huì)阻止其它進(jìn)程再獲取 SHARED 鎖來(lái)讀取數(shù)據(jù)。對(duì)于 iOS 來(lái)說(shuō)可能沒(méi)有多進(jìn)程訪問(wèn)數(shù)據(jù)庫(kù)的場(chǎng)景,可以把 locking_mode 的默認(rèn)值改為 EXCLUSIVE。

PRAGMA locking_mode = EXCLUSIVE

在 EXCLUSIVE 模式下,數(shù)據(jù)庫(kù)連接在斷開(kāi)前都不會(huì)釋放 SQLite 文件鎖,從而避免不必要的沖突,提高數(shù)據(jù)庫(kù)訪問(wèn)的速度。

多線程并發(fā)

相比多進(jìn)程,多線程的數(shù)據(jù)庫(kù)訪問(wèn)可能會(huì)更加常見(jiàn)。SQLite 支持多線程并發(fā)模式,需要開(kāi)啟下面的配置,當(dāng)然系統(tǒng) SQLite 會(huì)默認(rèn)開(kāi)啟多線程 Multi-thread模式。

PRAGMA SQLITE_THREADSAFE = 2

跟多進(jìn)程的鎖機(jī)制一樣,為了實(shí)現(xiàn)簡(jiǎn)單,SQLite 鎖的粒度都是數(shù)據(jù)庫(kù)文件級(jí)別,并沒(méi)有實(shí)現(xiàn)表級(jí)甚至行級(jí)的鎖。還有需要說(shuō)明的是,同一個(gè)句柄同一時(shí)間只有一個(gè)線程在操作,這個(gè)時(shí)候我們需要打開(kāi)數(shù)據(jù)庫(kù)連接池 Connection Pool。

跟多進(jìn)程類似,多線程可以同時(shí)讀取數(shù)據(jù)庫(kù)數(shù)據(jù),但是寫數(shù)據(jù)庫(kù)依然是互斥的。SQLite 提供了 Busy Retry 的方案,即發(fā)生阻塞時(shí)會(huì)觸發(fā) Busy Handler,此時(shí)可以讓線程休眠一段時(shí)間后,重新嘗試操作。

需要說(shuō)明的是,首先 SQLite 的 Busy Retry 的方案雖然基本能解決問(wèn)題,但對(duì)性能的壓榨做的不夠極致,可以參考 微信 iOS SQLite 源碼優(yōu)化實(shí)踐。它的核心問(wèn)題在 Retry 過(guò)程中,休眠時(shí)間的長(zhǎng)短和重試次數(shù),是決定性能和操作成功率的關(guān)鍵。不過(guò)在 Android 平臺(tái)提供的 SQLiteConnectionPool 中通過(guò)休眠-喚醒的方式能夠保證第一時(shí)間喚醒休眠中的線程,來(lái)提高數(shù)據(jù)庫(kù)執(zhí)行效率??梢詤⒖记懊娴姆治觥?a href="http://www.lxweimin.com/p/f88de2c1343f" target="_blank">Android 數(shù)據(jù)庫(kù)之 SQLiteConnectionPool 源碼分析》。

為了進(jìn)一步提高并發(fā)性能,我們可以打開(kāi) WAL (Write-Ahead-Logging)模式。WAL 模式會(huì)將修改的數(shù)據(jù)單獨(dú)寫到一個(gè) WAL 文件中,而讀操作開(kāi)始時(shí),會(huì)記下當(dāng)前的 WAL 文件狀態(tài),并且只訪問(wèn)在此之前的數(shù)據(jù),同時(shí)也會(huì)引入 WAL 日志文件鎖。通過(guò) WAL 模式讀和寫也可以完全地并發(fā)執(zhí)行,不會(huì)互相阻塞

PRAGMA schema.journal_mode = WAL

但是需要注意的是,寫之間是仍然不能并發(fā)。如果出現(xiàn)多個(gè)寫并發(fā)操作的情況,依然有可能出現(xiàn) SQLiteDatabaseLockedException。這個(gè)時(shí)候我們可以讓應(yīng)用中捕獲這個(gè)異常,然后等待一段時(shí)間再重試。

} catch (SQLiteDatabaseLockedException e) {
    if (sliteLockedExceptionTimes < (tryTimes - 1)) {
        try{
            Thread.sleep(100);
        }catch(InterruptedException el){
            
        }
    }
    sliteLockedExceptionTimes++;
}

這里還需要說(shuō)明的是,Android 平臺(tái)提供的數(shù)據(jù)連接池 SQLiteConnectionPool,由于其內(nèi)部保證只有一個(gè)主連接,多個(gè)寫操作通過(guò)等待-喚醒方式競(jìng)爭(zhēng)該連接以獲得數(shù)據(jù)庫(kù)寫操作,故在單進(jìn)程情況下不會(huì)發(fā)生上述的 SQLiteDatabaseLockedException 異常,但是多進(jìn)程情況下依然有可能發(fā)生。另外關(guān)于連接池大小設(shè)置建議使用 4,不過(guò)系統(tǒng)默認(rèn)好像并沒(méi)有提供設(shè)置連接池大小的接口,默認(rèn)與 WAL 模式一起開(kāi)啟。

這里推薦在 2017 年微信開(kāi)源了內(nèi)部使用 SQLite 數(shù)據(jù)庫(kù) WCDB,由于 Android 系統(tǒng)版本的不同導(dǎo)致 SQLite 的實(shí)現(xiàn)也有所差異,經(jīng)常會(huì)出現(xiàn)一些兼容性問(wèn)題,所以 WCDB 單獨(dú)引入了自己的 SQLite 版本。這樣就有了”源碼在手,天下我有“。例如 SQLiteDatabase 框架查詢數(shù)據(jù)庫(kù)使用的是 Cursor 接口,Cursor 的實(shí)現(xiàn)是分配一個(gè)固定 2MB 大小的緩沖區(qū) Cursor Window,這在查詢數(shù)據(jù)量較小時(shí)可能不一定劃算;對(duì)于結(jié)果集大于 2MB 的情況,遍歷途中還會(huì)引發(fā) Cursor 重查詢,這個(gè)消耗就相當(dāng)大了,而且數(shù)據(jù)的獲取中間要經(jīng)歷兩次內(nèi)存拷貝。 WCDB 就對(duì)此作了優(yōu)化,你可以參考 Cursor 優(yōu)化實(shí)現(xiàn)。

總的來(lái)說(shuō)通過(guò)連接池與 WAL 模式,我們可以很大程度上提高 SQLite 的讀寫并發(fā),大大減少由于并發(fā)導(dǎo)致的等待耗時(shí),建議大家在應(yīng)用中嘗試開(kāi)啟。

掌握了 SQLite 數(shù)據(jù)庫(kù)并發(fā)的機(jī)制,在某些時(shí)候我們可以更好地決策應(yīng)該拆數(shù)據(jù)表還是拆數(shù)據(jù)庫(kù)。新建一個(gè)數(shù)據(jù)庫(kù)好處是可以隔離其它庫(kù)并發(fā)或者損壞的情況,而壞處是數(shù)據(jù)庫(kù)初始化耗時(shí)以及更多的內(nèi)存占用。一般來(lái)說(shuō),單獨(dú)的業(yè)務(wù)都會(huì)使用獨(dú)立數(shù)據(jù)庫(kù)。

3. 查詢優(yōu)化

說(shuō)到數(shù)據(jù)庫(kù)的查詢優(yōu)化,你第一個(gè)想到的肯定是建索引,那就先聊聊 SQLite 的索引優(yōu)化。

(1) 索引優(yōu)化

正確使用索引在大部分場(chǎng)景可以大大降低查詢速度,下面是索引使用非常簡(jiǎn)單的例子,我們先從索引表找到數(shù)據(jù)對(duì)應(yīng)的 rowid,然后再?gòu)脑瓟?shù)據(jù)表直接通過(guò) rowid 查詢結(jié)果。

索引的使用

關(guān)于 SQLite 索引的原理網(wǎng)上有很多文章,這里推薦一些參考資料

重點(diǎn)要說(shuō)的是很多時(shí)候我們以為已經(jīng)建立了索引,但事實(shí)上并沒(méi)有真正生效。這里關(guān)鍵在于如何正確的建立索引。例如使用了 BETWEEN、LIKE、OR 這些操作符、使用表達(dá)式或者 case when 等。更詳細(xì)的規(guī)則可以參考官方文檔 The SQLite Query Optimizer Overview,下面是一個(gè)通過(guò)優(yōu)化轉(zhuǎn)換達(dá)到使用索引的例子。

BETWEEN:myfied1 索引無(wú)法生效
SELECT * FROM mytable WHERE myfield BETWEEN 10 and 20;
轉(zhuǎn)換成:myfied1 索引可以生效
SELECT * FROM mytable WHERE myfield >= 10 AND myfield <= 20;

建立索引是有代價(jià)的,需要一直維護(hù)索引表的更新,比如對(duì)于一個(gè)很小的表來(lái)說(shuō)就沒(méi)有必要建索引;如果一個(gè)表經(jīng)常是執(zhí)行插入更新操作,那么也需要節(jié)制的建立索引??偟膩?lái)說(shuō)有幾個(gè)原則:

  • 建立正確的索引。這里不僅需要確保索引在查詢中真正生效,我們還希望可以選擇最高效的索引。如果一個(gè)表建立太多的索引,那么在查詢的時(shí)候 SQLite 可能不會(huì)選擇最好的來(lái)執(zhí)行。

  • 單列索引、多列索引與復(fù)合索引的選擇。索引要綜合數(shù)據(jù)表中不同的查詢與排序語(yǔ)句一起考慮,如果查詢結(jié)果集過(guò)大,還是希望可以通過(guò)符合索引直接在索引表返回查詢結(jié)果。

  • 索引字段的選擇。整型類型索引效率會(huì)遠(yuǎn)高于字符串索引,而對(duì)于主鍵 SQLite 會(huì)默認(rèn)幫我們建立索引,所以主鍵盡量不要使用復(fù)雜字段。

  • 總的來(lái)說(shuō)索引優(yōu)化是 SQLite 優(yōu)化中最簡(jiǎn)單同時(shí)也是最有效的,但是它并不是簡(jiǎn)單的建一個(gè)索引就可以了,有的時(shí)候我們需要進(jìn)一步調(diào)整查詢語(yǔ)句甚至是表的結(jié)構(gòu),這樣才能達(dá)到最好的效果。

關(guān)于索引優(yōu)化這里再補(bǔ)充說(shuō)明下
  • EXPLAIN QUERY PLAN

通過(guò) EXPLAIN QUERY PLAN 指令我們可以輕松解決大部分明顯 SQL 設(shè)計(jì)上的問(wèn)題。(該指令是查看 SQLite 在執(zhí)行 SQL 時(shí)所采用的計(jì)劃,例如可以看到執(zhí)行時(shí)所采用的 index,并且可以看到執(zhí)行 SQL 過(guò)程前 SQLite 對(duì)整個(gè)查詢所涉及的元數(shù)據(jù)條數(shù)的預(yù)估)。但是也有例外的情況是無(wú)法檢測(cè)到的,EXPLAIN QUERY PLAN 無(wú)法檢測(cè)到索引頁(yè)的加載數(shù)量,以至于即便使用了索引,效率也會(huì)變得低下

關(guān)于 SQLite 命令行的使用可以參考 Command Line Shell For SQLite。

  • 索引字段的選擇

上一條說(shuō)到使用 EXPLAIN QUERY PLAN 檢測(cè)看到實(shí)際已經(jīng)采用了索引,看上去是沒(méi)什么問(wèn)題,但最后可能還是會(huì)出現(xiàn)很多耗時(shí)的查詢操作。

其實(shí)在整個(gè) SQLite 的查詢過(guò)程中有兩個(gè)比較大的瓶頸需要解決,一個(gè)是磁盤 I/O 的數(shù)量,另外一個(gè)是引擎的計(jì)算量,而引擎計(jì)算量與查詢過(guò)程所需的用到 Page 的數(shù)量是成線性正比關(guān)系的,也就是說(shuō),要降低整個(gè)查詢時(shí)常,必須先想辦法降低整個(gè)查詢過(guò)程中需要用到的 Page 數(shù)量。關(guān)于這部分你可以參考《微信ANDROID客戶端-會(huì)話速度提升70%的背后》。

簡(jiǎn)單點(diǎn)說(shuō)就是單條索引占用越大,用于存儲(chǔ)索引的 Page 數(shù)量就越多,用于查詢加載的 Page 量增加導(dǎo)致整個(gè)查詢時(shí)間越長(zhǎng)。不建議用大 String 作為索引列,這里在介紹下 SQLite 可變長(zhǎng)整數(shù):

可變長(zhǎng)整數(shù)是 SQLite 的特色之一,使用它既可以處理大整數(shù),又可以節(jié)省存儲(chǔ)空間。由于單元中大量使用可變長(zhǎng)整數(shù)??勺冮L(zhǎng)整數(shù)由 1 ~ 9 個(gè)字節(jié)組成,每個(gè)字節(jié)的低 7 位有效,第 8 位是標(biāo)志位。在組成可變長(zhǎng)整數(shù)的各字節(jié)中,前面字節(jié)(整數(shù)的高位字節(jié))的第 8 位置 1,只有最低一個(gè)字節(jié)的第 8 位置 0,表示整數(shù)結(jié)束??勺冮L(zhǎng)可用于存儲(chǔ) rowid、字段的字節(jié)數(shù)或 BTree 單元中的數(shù)據(jù)。故實(shí)際每個(gè) byte 能夠表示的證書(shū)個(gè)數(shù)為 128(只有低 7 位可用)

(2)頁(yè)大小與緩存大小

數(shù)據(jù)庫(kù)就像一個(gè)小文件系統(tǒng)一樣,事實(shí)上它內(nèi)部也有頁(yè)和緩存的概念。

對(duì)于 SQLite 的 DB 文件來(lái)說(shuō),頁(yè)(page)是最小的存儲(chǔ)單位,如下圖所示每個(gè)表對(duì)應(yīng)數(shù)據(jù)在整個(gè) DB 文件中都是通過(guò)一個(gè)一個(gè)的頁(yè)存儲(chǔ),屬于同一個(gè)表不同的頁(yè)以 B 樹(shù)(B-tree)的方式組織索引,每一個(gè)表都是一棵 B 樹(shù)。

page 查找過(guò)程

跟文件系統(tǒng)的頁(yè)緩存(Page Cache)一樣,SQLite 會(huì)將讀過(guò)的頁(yè)緩存起來(lái),用來(lái)加快下一次讀取速度。頁(yè)大小默認(rèn)是 1024Byte,緩存大小默認(rèn)是 1000 頁(yè)。更多的編譯參數(shù)你可以查看官方文檔PRAGMA Statements。

PRAGMA page_size = 1024
PRAGMA cache_size = 1000

每個(gè)頁(yè)永遠(yuǎn)只存放一個(gè)表或者一組索引的數(shù)據(jù),即不可能同一個(gè)頁(yè)存放多個(gè)表或索引的數(shù)據(jù),表在整個(gè) DB 文件的第一個(gè)頁(yè)就是這棵 B 樹(shù)的根頁(yè)。繼續(xù)已上圖為例,如果想查詢 rowID 為 N+2 的數(shù)據(jù),我們首先要從 sqlite_master 查找出 table 的 root page 的位置,然后讀取 root page、page4 這兩個(gè)頁(yè),所以一共會(huì)需要 3 次 I/O。

page size(Byte) 插入 60000 行數(shù)據(jù)(ms)
1024 3426
2048 2772
4096 2506
8192 2304
32768 2673

從上表可以看到,增大 page size 并不能不斷地提升性能,在拐點(diǎn)以后可能還會(huì)有副作用。我們可以通過(guò) PRAGMA 改變默認(rèn) page size 的大小,也可以在創(chuàng)建 DB 文件的時(shí)候進(jìn)行設(shè)置。但是需要注意如果存在老的數(shù)據(jù),需要 vacuum 對(duì)數(shù)據(jù)表對(duì)應(yīng)的節(jié)點(diǎn)重新計(jì)算分配大小。這里建議大家在新建數(shù)據(jù)庫(kù)的時(shí)候,就提前選擇 4KB 作為默認(rèn)的 page size 以獲得更好的性能

其實(shí)這個(gè)優(yōu)化的原理就是讓 page 存儲(chǔ)更多的數(shù)據(jù),從而減少 page 頁(yè)的查找次數(shù),也就是降低 I/O 次數(shù)。但是頁(yè)過(guò)大(拐點(diǎn)位置)則會(huì)導(dǎo)致頁(yè)內(nèi)容過(guò)多而 I/O 變慢。

其他優(yōu)化

關(guān)于 SQLite 的使用優(yōu)化還有很多很多,下面再簡(jiǎn)單提幾個(gè)點(diǎn):

  • 慎用“SELECT *”,需要使用多少列,就選取多少列。

  • 正確使用事務(wù)。

  • 預(yù)編譯與參數(shù)綁定,緩存被編譯后的 SQL 語(yǔ)句。

  • 對(duì)于 BLOB 或超大的 Text 列,可能會(huì)超出一個(gè)頁(yè)的大小,導(dǎo)致出現(xiàn)超大頁(yè)。建議將這列單獨(dú)拆表,或者放大表字段的后面。

  • 定期整理或者清理無(wú)用或可刪除的數(shù)據(jù),例如刪除數(shù)據(jù)庫(kù)比較久遠(yuǎn)的數(shù)據(jù),如果用戶訪問(wèn)到這部分?jǐn)?shù)據(jù),重新從網(wǎng)絡(luò)拉取即可。

在日常的開(kāi)發(fā)中,我們都應(yīng)該對(duì)這些知識(shí)有所了解,再來(lái)復(fù)習(xí)一下上面整理的 SQLite 優(yōu)化方法,通過(guò)引進(jìn) ORM,可以大大的提升我們的開(kāi)發(fā)效率。通過(guò) WAL 模式和連接池,可以提高 SQLite 的并發(fā)性能。通過(guò)正確的建立索引,可以提升 SQLite 查詢速度。通過(guò)調(diào)整默認(rèn)的頁(yè)大小和緩存大小,可以提升 SQLite 的整體性能。

SQLite 監(jiān)控

正確使用索引,正確使用事務(wù)。對(duì)于大型項(xiàng)目來(lái)說(shuō),參與的開(kāi)發(fā)人員可能有幾十上百人,開(kāi)發(fā)人員水平參差不齊,很難保證每個(gè)人都可以正確而高效的使用 SQLite,所以這時(shí)候需要建立完善的監(jiān)控體系。

  1. 本地測(cè)試

作為一名靠譜的開(kāi)發(fā)工程師,我們每寫一條 SQL 語(yǔ)句,都應(yīng)該先在本地測(cè)試。我們可以通過(guò)上面提到的 EXPLAIN QUERY PLAN 測(cè)試 SQL 語(yǔ)句的查詢計(jì)劃,是全表掃描還是使用了索引,以及具體使用了哪個(gè)索引等。

sqlite> EXPLAIN QUERY PLAN SELECT * FROM name WHERE age = 20 AND sex = '男';
QUERY PALN
| -- SEARCH TABLE t1 USING INDEX name-index (age=? AND sex=?)
  1. 耗時(shí)監(jiān)控

不過(guò)本地測(cè)試過(guò)于依賴開(kāi)發(fā)人員的自覺(jué)性,所以很多時(shí)候我們需要建立線上大數(shù)據(jù)的監(jiān)控。微信開(kāi)源的 WCDB 集成了自己的 SQLite 源碼,所以可以非常方便的增加自己想要的監(jiān)控模塊。它內(nèi)部默認(rèn)增加了 SQLiteTrace 的監(jiān)控模塊,有以下四個(gè)接口:

 /**
 * 當(dāng)某條 SQL 語(yǔ)句執(zhí)行完畢
 *
 * @param db    database on which the statement was executed
 * @param sql   statement executed
 * @param type  type of the statement. See {@link com.tencent.wcdb.DatabaseUtils#getSqlStatementType}
 * @param time  time spent on execution, in milliseconds
 */
void onSQLExecuted(SQLiteDatabase db, String sql, int type, long time);

/**
 * 當(dāng)線程成功獲得數(shù)據(jù)庫(kù)連接時(shí)調(diào)用。
 *
 * @param db        database on which the connection was obtained
 * @param sql       statement about to be executed
 * @param waitTime  time spent on waiting for available connection, in milliseconds
 * @param isPrimary whether the primary connection (write connection) is obtained
 */
void onConnectionObtained(SQLiteDatabase db, String sql, long waitTime, boolean isPrimary);

/**
 * 當(dāng)前出現(xiàn)連接池被其他執(zhí)行語(yǔ)句阻塞時(shí)間過(guò)長(zhǎng)
 *
 * @param db        database on which connection pool is blocked
 * @param sql       statement to be executed
 * @param requests  list of statement being executed
 * @param message   message generated by the connection pool
 */
void onConnectionPoolBusy(SQLiteDatabase db, String sqlWaiting, long waitTime,
                          boolean wantPrimaryConnection,
                          List<TraceInfo<String>> sqlRunning,
                          List<TraceInfo<StackTraceElement[]>> longLastingActions);

/**
 * 當(dāng)出現(xiàn)數(shù)據(jù)庫(kù)損壞
 *
 * @param db    the corrupted database
 */
void onDatabaseCorrupted(SQLiteDatabase db);

我們可以通過(guò)這些接口監(jiān)控?cái)?shù)據(jù)庫(kù) busy、損耗以及執(zhí)行耗時(shí)。針對(duì)耗時(shí)比較長(zhǎng)的 SQL 語(yǔ)句,需要進(jìn)一步檢查是 SQL 語(yǔ)句寫的不好,還是需要建立索引。

  1. 智能監(jiān)控

跟隨 WCDB 開(kāi)源的還包括一個(gè)智能化分析 SQLite 語(yǔ)句的工具 Matrix SQLiteLint -- SQLite 使用質(zhì)量檢測(cè)。雖然名字帶 “l(fā)int”,但它并不是靜態(tài)代碼檢查,它在 APP 運(yùn)行時(shí)進(jìn)行檢測(cè),而且大部分檢測(cè)算法與數(shù)據(jù)量無(wú)關(guān),即不依賴線上的數(shù)據(jù)狀態(tài)。只要你觸發(fā)了某條 SQL 語(yǔ)句的執(zhí)行,SQLiteLint 就會(huì)幫你 review 這條語(yǔ)句。它根據(jù)分析 SQL 語(yǔ)句的語(yǔ)法樹(shù),結(jié)合我們?nèi)粘?shù)據(jù)庫(kù)使用的經(jīng)驗(yàn),抽象出索引使用不當(dāng)、SELECT * 等六大問(wèn)題

SQLiteLint

它內(nèi)部通過(guò)收集 APP 運(yùn)行時(shí)的 SQL 執(zhí)行信息包括執(zhí)行語(yǔ)句、創(chuàng)建的表信息等。如果使用 Android 默認(rèn)的 DB 框架,SQLiteLint 提供了一種無(wú)侵入的獲取執(zhí)行 SQL 語(yǔ)句以及耗時(shí)等信息的方式。內(nèi)部通過(guò) hook 向 SQLite3 C 層注冊(cè)回調(diào)。從而無(wú)需開(kāi)發(fā)者額外的打點(diǎn)統(tǒng)計(jì)代碼。

另外美團(tuán)也開(kāi)源了它們內(nèi)部的 SQL 優(yōu)化工具 SQLAdvisor,你可以參考這些資料:

總結(jié)

數(shù)據(jù)庫(kù)存儲(chǔ)應(yīng)該是每一個(gè)開(kāi)發(fā)人員掌握的基本功,比掌握更重要的是,清楚 SQLite 的底層機(jī)制對(duì)我們的工作會(huì)有很大的指導(dǎo)意義。SQLite 優(yōu)化真的是一個(gè)很大的話題,可能我們還需要結(jié)合參考資料再進(jìn)一步反復(fù)學(xué)習(xí)理解。另外推薦一些 SQLite 進(jìn)階學(xué)習(xí)資料,感興趣的朋友可以繼續(xù)深入學(xué)習(xí)。


以上便是個(gè)人在學(xué)習(xí) SQLite 優(yōu)化過(guò)程中的體會(huì)和總結(jié),文中如有不妥或有更好的分析結(jié)果,還請(qǐng)大家指出。

文章如果對(duì)你有幫助,就請(qǐng)留個(gè) 贊 吧!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • --- layout: post title: "如果有人問(wèn)你關(guān)系型數(shù)據(jù)庫(kù)的原理,叫他看這篇文章(轉(zhuǎn))" date...
    藍(lán)墜星閱讀 824評(píng)論 0 3
  • SQLite 憑借著輕量級(jí)、可嵌入的特性成為了很多移動(dòng)端產(chǎn)品數(shù)據(jù)存儲(chǔ)的首選。但由于 SQLite 是純 C 語(yǔ)言開(kāi)...
    PerTerbin閱讀 5,719評(píng)論 2 13
  • 你好,WCDB WCDB是一個(gè)高效、完整、易用的移動(dòng)數(shù)據(jù)庫(kù)框架,基于SQLCipher,支持iOS, macOS和...
    he15his閱讀 5,862評(píng)論 4 4
  • 時(shí)昌虎|上海立泉環(huán)境科技有限公司|六項(xiàng)精進(jìn)打卡【第241天】 【知~學(xué)習(xí)】 《六項(xiàng)精進(jìn)》大綱:今日2遍 累計(jì)46...
    虎_933b閱讀 145評(píng)論 0 0
  • 七絕·春望 文 李伯強(qiáng) 花開(kāi)雨落浥芽浸,紫燕斜織梢微新。 乍暖春風(fēng)湖皺綠,世事起伏獨(dú)平心。 2018 04 1...
    大雨落幽燕李佰強(qiáng)閱讀 301評(píng)論 0 11