Android 存儲優(yōu)化系列專題
- SharedPreferences 系列
《Android 之不要濫用 SharedPreferences》
《Android 之不要濫用 SharedPreferences(2)— 數(shù)據(jù)丟失》
- ContentProvider 系列(待更)
《Android 存儲選項之 ContentProvider 啟動過程源碼分析》
《Android 存儲選項之 ContentProvider 深入分析》
- 對象序列化系列
《Android 對象序列化之你不知道的 Serializable》
《Android 對象序列化之 Parcelable 深入分析》
《Android 對象序列化之追求完美的 Serial》
- 數(shù)據(jù)序列化系列(待更)
《Android 數(shù)據(jù)序列化之 JSON》
《Android 數(shù)據(jù)序列化之 Protocol Buffer 使用》
《Android 數(shù)據(jù)序列化之 Protocol Buffer 源碼分析》
- SQLite 存儲系列
《Android 存儲選項之 SQLiteDatabase 創(chuàng)建過程源碼分析》
《Android 存儲選項之 SQLiteDatabase 源碼分析》
《數(shù)據(jù)庫連接池 SQLiteConnectionPool 源碼分析》
《SQLiteDatabase 啟用事務源碼分析》
《SQLite 數(shù)據(jù)庫 WAL 模式工作原理簡介》
《SQLite 數(shù)據(jù)庫鎖機制與事務簡介》
《SQLite 數(shù)據(jù)庫優(yōu)化那些事兒》
該篇文章屬于 SQLite 存儲系列的最后一篇绕辖,簡單回顧下前面蛋逾, Android 系統(tǒng)為支撐 SQLite 提供了 SQLiteDatabase 框架,它可以說是整個數(shù)據(jù)庫框架最重要的一個類,內(nèi)部維護了數(shù)據(jù)庫連接池管理蒂窒、并發(fā)訪問、事務等核心管理万细。整體上看這套框架可以較高效的完成 SQLite 數(shù)據(jù)庫的訪問操作戒傻。不過它仍然存在一些注意和優(yōu)化的地方,今天就來聊一聊 SQLite 優(yōu)化相關內(nèi)容图呢。
關于 SQLite 的優(yōu)化內(nèi)容真的非常多条篷,個人也在不斷地學習和探索中,好在它有大量的資料供我們參考蛤织,遇到陌生或者不懂的地方還需要結(jié)合參考資料反復學習理解赴叹。今天我就選擇一些相對比較重要的優(yōu)化點整理出來供大家參考。
1. ORM
可能大部分應用為了提高開發(fā)效率指蚜,會引入 ORM 框架乞巧。ORM(Object Relational Mapping)也就是對象關系映射,用面向?qū)ο蟮母拍畎褦?shù)據(jù)庫中表和對象關聯(lián)起來摊鸡,可以讓我們不用關心數(shù)據(jù)庫底層的實現(xiàn)绽媒。
Android 中最常用的 ORM 框架有開源 greenDAO 和 Google 官方的 Room,那使用 ORM 框架會帶來什么問題呢免猾?
使用 ORM 框架真的非常簡單是辕,但是這種簡單易用性是需要犧牲部分執(zhí)行效率為代價的,具體的損耗跟 ORM 框架寫的好不好很有關系猎提。但可能更大的問題是讓很多的開發(fā)者的思維固化获三,不但不能正確地寫出高效的 SQL 語句,最后可能連簡單的 SQL 語句都不會寫了锨苏。
為了提高開發(fā)效率疙教,應用的確應該引入 ORM 框架。但是這不能是我們不去學習數(shù)據(jù)庫基礎知識的理由伞租,只有理解底層的一些機制松逊,我們才能更加得心應手地解決疑難問題。
2. 進程與線程并發(fā)
如果我們在項目中使用 SQLite肯夏,那么在下面這個 SQLiteDatabaseLockedExecption 就是經(jīng)常會出現(xià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 歸根到底是因為并發(fā)導致犀暑,而 SQLite 的并發(fā)有兩個維度,一個是多進程并發(fā)烁兰,一個是多線程并發(fā)耐亏。
多進程并發(fā)
SQLite 默認是支持多進程并發(fā)操作的,它通過文件鎖來控制多進程并發(fā)沪斟。SQLite 鎖的粒度并沒有非常細广辰,它針對的是整個 DB 文件,內(nèi)部有 5 個狀態(tài)主之,具體你可以參考下面的文章择吊。
- 官方文檔:SQLite Locking
- SQLite 源碼分析:SQLite 鎖機制簡介
- SQLite 封鎖機制
簡單來說,多進程可以同時獲取 SHARED 鎖來讀取數(shù)據(jù)槽奕,但是只有一個進程可以獲取 EXCLUSIVE 鎖來寫數(shù)據(jù)庫几睛。并且 EXCLUSIVE 會阻止其它進程再獲取 SHARED 鎖來讀取數(shù)據(jù)。對于 iOS 來說可能沒有多進程訪問數(shù)據(jù)庫的場景粤攒,可以把 locking_mode 的默認值改為 EXCLUSIVE所森。
PRAGMA locking_mode = EXCLUSIVE
在 EXCLUSIVE 模式下,數(shù)據(jù)庫連接在斷開前都不會釋放 SQLite 文件鎖夯接,從而避免不必要的沖突焕济,提高數(shù)據(jù)庫訪問的速度。
多線程并發(fā)
相比多進程盔几,多線程的數(shù)據(jù)庫訪問可能會更加常見晴弃。SQLite 支持多線程并發(fā)模式,需要開啟下面的配置逊拍,當然系統(tǒng) SQLite 會默認開啟多線程 Multi-thread模式肝匆。
PRAGMA SQLITE_THREADSAFE = 2
跟多進程的鎖機制一樣,為了實現(xiàn)簡單顺献,SQLite 鎖的粒度都是數(shù)據(jù)庫文件級別,并沒有實現(xiàn)表級甚至行級的鎖枯怖。還有需要說明的是注整,同一個句柄同一時間只有一個線程在操作,這個時候我們需要打開數(shù)據(jù)庫連接池 Connection Pool度硝。
跟多進程類似肿轨,多線程可以同時讀取數(shù)據(jù)庫數(shù)據(jù),但是寫數(shù)據(jù)庫依然是互斥的蕊程。SQLite 提供了 Busy Retry 的方案椒袍,即發(fā)生阻塞時會觸發(fā) Busy Handler,此時可以讓線程休眠一段時間后藻茂,重新嘗試操作驹暑。
需要說明的是玫恳,首先 SQLite 的 Busy Retry 的方案雖然基本能解決問題,但對性能的壓榨做的不夠極致优俘,可以參考 微信 iOS SQLite 源碼優(yōu)化實踐京办。它的核心問題在 Retry 過程中,休眠時間的長短和重試次數(shù)帆焕,是決定性能和操作成功率的關鍵惭婿。不過在 Android 平臺提供的 SQLiteConnectionPool 中通過休眠-喚醒的方式能夠保證第一時間喚醒休眠中的線程侥锦,來提高數(shù)據(jù)庫執(zhí)行效率冤竹。可以參考前面的分析《Android 數(shù)據(jù)庫之 SQLiteConnectionPool 源碼分析》务嫡。
為了進一步提高并發(fā)性能折晦,我們可以打開 WAL (Write-Ahead-Logging)模式钥星。WAL 模式會將修改的數(shù)據(jù)單獨寫到一個 WAL 文件中,而讀操作開始時筋遭,會記下當前的 WAL 文件狀態(tài)打颤,并且只訪問在此之前的數(shù)據(jù),同時也會引入 WAL 日志文件鎖漓滔。通過 WAL 模式讀和寫也可以完全地并發(fā)執(zhí)行编饺,不會互相阻塞。
PRAGMA schema.journal_mode = WAL
但是需要注意的是响驴,寫之間是仍然不能并發(fā)透且。如果出現(xiàn)多個寫并發(fā)操作的情況,依然有可能出現(xiàn) SQLiteDatabaseLockedException豁鲤。這個時候我們可以讓應用中捕獲這個異常秽誊,然后等待一段時間再重試。
} catch (SQLiteDatabaseLockedException e) {
if (sliteLockedExceptionTimes < (tryTimes - 1)) {
try{
Thread.sleep(100);
}catch(InterruptedException el){
}
}
sliteLockedExceptionTimes++;
}
這里還需要說明的是琳骡,Android 平臺提供的數(shù)據(jù)連接池 SQLiteConnectionPool锅论,由于其內(nèi)部保證只有一個主連接,多個寫操作通過等待-喚醒方式競爭該連接以獲得數(shù)據(jù)庫寫操作楣号,故在單進程情況下不會發(fā)生上述的 SQLiteDatabaseLockedException 異常最易,但是多進程情況下依然有可能發(fā)生。另外關于連接池大小設置建議使用 4炫狱,不過系統(tǒng)默認好像并沒有提供設置連接池大小的接口藻懒,默認與 WAL 模式一起開啟。
這里推薦在 2017 年微信開源了內(nèi)部使用 SQLite 數(shù)據(jù)庫 WCDB视译,由于 Android 系統(tǒng)版本的不同導致 SQLite 的實現(xiàn)也有所差異嬉荆,經(jīng)常會出現(xiàn)一些兼容性問題,所以 WCDB 單獨引入了自己的 SQLite 版本酷含。這樣就有了”源碼在手鄙早,天下我有“汪茧。例如 SQLiteDatabase 框架查詢數(shù)據(jù)庫使用的是 Cursor 接口,Cursor 的實現(xiàn)是分配一個固定 2MB 大小的緩沖區(qū) Cursor Window蝶锋,這在查詢數(shù)據(jù)量較小時可能不一定劃算陆爽;對于結(jié)果集大于 2MB 的情況,遍歷途中還會引發(fā) Cursor 重查詢扳缕,這個消耗就相當大了慌闭,而且數(shù)據(jù)的獲取中間要經(jīng)歷兩次內(nèi)存拷貝。 WCDB 就對此作了優(yōu)化躯舔,你可以參考 Cursor 優(yōu)化實現(xiàn)驴剔。
總的來說通過連接池與 WAL 模式,我們可以很大程度上提高 SQLite 的讀寫并發(fā)粥庄,大大減少由于并發(fā)導致的等待耗時丧失,建議大家在應用中嘗試開啟。
掌握了 SQLite 數(shù)據(jù)庫并發(fā)的機制惜互,在某些時候我們可以更好地決策應該拆數(shù)據(jù)表還是拆數(shù)據(jù)庫布讹。新建一個數(shù)據(jù)庫好處是可以隔離其它庫并發(fā)或者損壞的情況,而壞處是數(shù)據(jù)庫初始化耗時以及更多的內(nèi)存占用训堆。一般來說描验,單獨的業(yè)務都會使用獨立數(shù)據(jù)庫。
3. 查詢優(yōu)化
說到數(shù)據(jù)庫的查詢優(yōu)化坑鱼,你第一個想到的肯定是建索引膘流,那就先聊聊 SQLite 的索引優(yōu)化。
(1) 索引優(yōu)化
正確使用索引在大部分場景可以大大降低查詢速度鲁沥,下面是索引使用非常簡單的例子呼股,我們先從索引表找到數(shù)據(jù)對應的 rowid,然后再從原數(shù)據(jù)表直接通過 rowid 查詢結(jié)果画恰。
關于 SQLite 索引的原理網(wǎng)上有很多文章彭谁,這里推薦一些參考資料
重點要說的是很多時候我們以為已經(jīng)建立了索引,但事實上并沒有真正生效允扇。這里關鍵在于如何正確的建立索引缠局。例如使用了 BETWEEN、LIKE蔼两、OR 這些操作符、使用表達式或者 case when 等逞度。更詳細的規(guī)則可以參考官方文檔 The SQLite Query Optimizer Overview额划,下面是一個通過優(yōu)化轉(zhuǎn)換達到使用索引的例子。
BETWEEN:myfied1 索引無法生效
SELECT * FROM mytable WHERE myfield BETWEEN 10 and 20;
轉(zhuǎn)換成:myfied1 索引可以生效
SELECT * FROM mytable WHERE myfield >= 10 AND myfield <= 20;
建立索引是有代價的档泽,需要一直維護索引表的更新俊戳,比如對于一個很小的表來說就沒有必要建索引揖赴;如果一個表經(jīng)常是執(zhí)行插入更新操作,那么也需要節(jié)制的建立索引抑胎≡锘總的來說有幾個原則:
建立正確的索引。這里不僅需要確保索引在查詢中真正生效阿逃,我們還希望可以選擇最高效的索引铭拧。如果一個表建立太多的索引,那么在查詢的時候 SQLite 可能不會選擇最好的來執(zhí)行恃锉。
單列索引搀菩、多列索引與復合索引的選擇。索引要綜合數(shù)據(jù)表中不同的查詢與排序語句一起考慮破托,如果查詢結(jié)果集過大肪跋,還是希望可以通過符合索引直接在索引表返回查詢結(jié)果。
索引字段的選擇土砂。整型類型索引效率會遠高于字符串索引州既,而對于主鍵 SQLite 會默認幫我們建立索引,所以主鍵盡量不要使用復雜字段萝映。
總的來說索引優(yōu)化是 SQLite 優(yōu)化中最簡單同時也是最有效的吴叶,但是它并不是簡單的建一個索引就可以了锌俱,有的時候我們需要進一步調(diào)整查詢語句甚至是表的結(jié)構(gòu)贸宏,這樣才能達到最好的效果吭练。
關于索引優(yōu)化這里再補充說明下
- EXPLAIN QUERY PLAN
通過 EXPLAIN QUERY PLAN 指令我們可以輕松解決大部分明顯 SQL 設計上的問題鲫咽。(該指令是查看 SQLite 在執(zhí)行 SQL 時所采用的計劃锦聊,例如可以看到執(zhí)行時所采用的 index孔庭,并且可以看到執(zhí)行 SQL 過程前 SQLite 對整個查詢所涉及的元數(shù)據(jù)條數(shù)的預估)。但是也有例外的情況是無法檢測到的,EXPLAIN QUERY PLAN 無法檢測到索引頁的加載數(shù)量芽淡,以至于即便使用了索引挣菲,效率也會變得低下唉窃。
關于 SQLite 命令行的使用可以參考 Command Line Shell For SQLite纹份。
- 索引字段的選擇
上一條說到使用 EXPLAIN QUERY PLAN 檢測看到實際已經(jīng)采用了索引蔓涧,看上去是沒什么問題元暴,但最后可能還是會出現(xiàn)很多耗時的查詢操作茉盏。
其實在整個 SQLite 的查詢過程中有兩個比較大的瓶頸需要解決,一個是磁盤 I/O 的數(shù)量讶迁,另外一個是引擎的計算量巍糯,而引擎計算量與查詢過程所需的用到 Page 的數(shù)量是成線性正比關系的祟峦,也就是說宅楞,要降低整個查詢時常咱筛,必須先想辦法降低整個查詢過程中需要用到的 Page 數(shù)量。關于這部分你可以參考《微信ANDROID客戶端-會話速度提升70%的背后》饲趋。
簡單點說就是單條索引占用越大奕塑,用于存儲索引的 Page 數(shù)量就越多龄砰,用于查詢加載的 Page 量增加導致整個查詢時間越長换棚。不建議用大 String 作為索引列,這里在介紹下 SQLite 可變長整數(shù):
可變長整數(shù)是 SQLite 的特色之一夕玩,使用它既可以處理大整數(shù)燎孟,又可以節(jié)省存儲空間缤弦。由于單元中大量使用可變長整數(shù)±厶幔可變長整數(shù)由 1 ~ 9 個字節(jié)組成,每個字節(jié)的低 7 位有效无虚,第 8 位是標志位友题。在組成可變長整數(shù)的各字節(jié)中踢匣,前面字節(jié)(整數(shù)的高位字節(jié))的第 8 位置 1离唬,只有最低一個字節(jié)的第 8 位置 0,表示整數(shù)結(jié)束模闲∈郏可變長可用于存儲 rowid、字段的字節(jié)數(shù)或 BTree 單元中的數(shù)據(jù)亮航。故實際每個 byte 能夠表示的證書個數(shù)為 128(只有低 7 位可用)
(2)頁大小與緩存大小
數(shù)據(jù)庫就像一個小文件系統(tǒng)一樣,事實上它內(nèi)部也有頁和緩存的概念重抖。
對于 SQLite 的 DB 文件來說,頁(page)是最小的存儲單位恨统,如下圖所示每個表對應數(shù)據(jù)在整個 DB 文件中都是通過一個一個的頁存儲莫绣,屬于同一個表不同的頁以 B 樹(B-tree)的方式組織索引兔综,每一個表都是一棵 B 樹涧窒。
跟文件系統(tǒng)的頁緩存(Page Cache)一樣硬鞍,SQLite 會將讀過的頁緩存起來,用來加快下一次讀取速度伐坏。頁大小默認是 1024Byte,緩存大小默認是 1000 頁纯露。更多的編譯參數(shù)你可以查看官方文檔PRAGMA Statements。
PRAGMA page_size = 1024
PRAGMA cache_size = 1000
每個頁永遠只存放一個表或者一組索引的數(shù)據(jù),即不可能同一個頁存放多個表或索引的數(shù)據(jù),表在整個 DB 文件的第一個頁就是這棵 B 樹的根頁遵班。繼續(xù)已上圖為例腹暖,如果想查詢 rowID 為 N+2 的數(shù)據(jù),我們首先要從 sqlite_master 查找出 table 的 root page 的位置,然后讀取 root page雳锋、page4 這兩個頁,所以一共會需要 3 次 I/O。
page size(Byte) | 插入 60000 行數(shù)據(jù)(ms) |
---|---|
1024 | 3426 |
2048 | 2772 |
4096 | 2506 |
8192 | 2304 |
32768 | 2673 |
從上表可以看到,增大 page size 并不能不斷地提升性能谤狡,在拐點以后可能還會有副作用霉囚。我們可以通過 PRAGMA 改變默認 page size 的大小,也可以在創(chuàng)建 DB 文件的時候進行設置。但是需要注意如果存在老的數(shù)據(jù)础浮,需要 vacuum 對數(shù)據(jù)表對應的節(jié)點重新計算分配大小番刊。這里建議大家在新建數(shù)據(jù)庫的時候鸭廷,就提前選擇 4KB 作為默認的 page size 以獲得更好的性能。
其實這個優(yōu)化的原理就是讓 page 存儲更多的數(shù)據(jù)垂攘,從而減少 page 頁的查找次數(shù),也就是降低 I/O 次數(shù)灼伤。但是頁過大(拐點位置)則會導致頁內(nèi)容過多而 I/O 變慢撞鹉。
其他優(yōu)化
關于 SQLite 的使用優(yōu)化還有很多很多孝鹊,下面再簡單提幾個點:
慎用“SELECT *”悼泌,需要使用多少列夹界,就選取多少列馆里。
正確使用事務。
預編譯與參數(shù)綁定可柿,緩存被編譯后的 SQL 語句鸠踪。
對于 BLOB 或超大的 Text 列,可能會超出一個頁的大小复斥,導致出現(xiàn)超大頁营密。建議將這列單獨拆表,或者放大表字段的后面目锭。
定期整理或者清理無用或可刪除的數(shù)據(jù)评汰,例如刪除數(shù)據(jù)庫比較久遠的數(shù)據(jù),如果用戶訪問到這部分數(shù)據(jù)痢虹,重新從網(wǎng)絡拉取即可被去。
在日常的開發(fā)中,我們都應該對這些知識有所了解奖唯,再來復習一下上面整理的 SQLite 優(yōu)化方法惨缆,通過引進 ORM,可以大大的提升我們的開發(fā)效率丰捷。通過 WAL 模式和連接池坯墨,可以提高 SQLite 的并發(fā)性能。通過正確的建立索引病往,可以提升 SQLite 查詢速度停巷。通過調(diào)整默認的頁大小和緩存大小叠穆,可以提升 SQLite 的整體性能硼被。
SQLite 監(jiān)控
正確使用索引嚷硫,正確使用事務。對于大型項目來說医清,參與的開發(fā)人員可能有幾十上百人会烙,開發(fā)人員水平參差不齊柏腻,很難保證每個人都可以正確而高效的使用 SQLite五嫂,所以這時候需要建立完善的監(jiān)控體系沃缘。
- 本地測試
作為一名靠譜的開發(fā)工程師,我們每寫一條 SQL 語句峰档,都應該先在本地測試。我們可以通過上面提到的 EXPLAIN QUERY PLAN 測試 SQL 語句的查詢計劃舔哪,是全表掃描還是使用了索引,以及具體使用了哪個索引等缆巧。
sqlite> EXPLAIN QUERY PLAN SELECT * FROM name WHERE age = 20 AND sex = '男';
QUERY PALN
| -- SEARCH TABLE t1 USING INDEX name-index (age=? AND sex=?)
- 耗時監(jiān)控
不過本地測試過于依賴開發(fā)人員的自覺性,所以很多時候我們需要建立線上大數(shù)據(jù)的監(jiān)控捉超。微信開源的 WCDB 集成了自己的 SQLite 源碼,所以可以非常方便的增加自己想要的監(jiān)控模塊。它內(nèi)部默認增加了 SQLiteTrace 的監(jiān)控模塊叶撒,有以下四個接口:
/**
* 當某條 SQL 語句執(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);
/**
* 當線程成功獲得數(shù)據(jù)庫連接時調(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);
/**
* 當前出現(xiàn)連接池被其他執(zhí)行語句阻塞時間過長
*
* @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);
/**
* 當出現(xiàn)數(shù)據(jù)庫損壞
*
* @param db the corrupted database
*/
void onDatabaseCorrupted(SQLiteDatabase db);
我們可以通過這些接口監(jiān)控數(shù)據(jù)庫 busy、損耗以及執(zhí)行耗時虑瀑。針對耗時比較長的 SQL 語句叽奥,需要進一步檢查是 SQL 語句寫的不好朝氓,還是需要建立索引。
- 智能監(jiān)控
跟隨 WCDB 開源的還包括一個智能化分析 SQLite 語句的工具 Matrix SQLiteLint -- SQLite 使用質(zhì)量檢測。雖然名字帶 “l(fā)int”橡庞,但它并不是靜態(tài)代碼檢查,它在 APP 運行時進行檢測印蔗,而且大部分檢測算法與數(shù)據(jù)量無關扒最,即不依賴線上的數(shù)據(jù)狀態(tài)。只要你觸發(fā)了某條 SQL 語句的執(zhí)行华嘹,SQLiteLint 就會幫你 review 這條語句吧趣。它根據(jù)分析 SQL 語句的語法樹,結(jié)合我們?nèi)粘?shù)據(jù)庫使用的經(jīng)驗,抽象出索引使用不當再菊、SELECT * 等六大問題爪喘。
它內(nèi)部通過收集 APP 運行時的 SQL 執(zhí)行信息包括執(zhí)行語句、創(chuàng)建的表信息等纠拔。如果使用 Android 默認的 DB 框架秉剑,SQLiteLint 提供了一種無侵入的獲取執(zhí)行 SQL 語句以及耗時等信息的方式。內(nèi)部通過 hook 向 SQLite3 C 層注冊回調(diào)稠诲。從而無需開發(fā)者額外的打點統(tǒng)計代碼侦鹏。
另外美團也開源了它們內(nèi)部的 SQL 優(yōu)化工具 SQLAdvisor,你可以參考這些資料:
總結(jié)
數(shù)據(jù)庫存儲應該是每一個開發(fā)人員掌握的基本功臀叙,比掌握更重要的是略水,清楚 SQLite 的底層機制對我們的工作會有很大的指導意義。SQLite 優(yōu)化真的是一個很大的話題劝萤,可能我們還需要結(jié)合參考資料再進一步反復學習理解渊涝。另外推薦一些 SQLite 進階學習資料,感興趣的朋友可以繼續(xù)深入學習床嫌。
以上便是個人在學習 SQLite 優(yōu)化過程中的體會和總結(jié)跨释,文中如有不妥或有更好的分析結(jié)果,還請大家指出厌处。
文章如果對你有幫助鳖谈,就請留個 贊 吧!