<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                [TOC] ## 進程與線程并發 如果我們在項目中有使用 SQLite,那么下面這個SQLiteDatabaseLockedException就是經常會出現的一個問題。 ``` android.database.sqlite.SQLiteDatabaseLockedException: database is locked at android.database.sqlite.SQLiteDatabase.dbopen at android.database.sqlite.SQLiteDatabase.openDatabase at android.database.sqlite.SQLiteDatabase.openDatabaseSQLiteDatabaseLockedException ``` 歸根到底是因為并發導致,而 SQLite 的并發有兩個維度,一個是多進程并發,一個是多線程并發。下面我們分別來講一下它們的關鍵點。 ### 多進程并發 多進程并發SQLite 默認是**支持多進程并發操作**的,它通過文件鎖來控制多進程的并發。SQLite 鎖的粒度并沒有非常細,它針對的是整個 DB 文件。 簡單來說,多進程可以同時獲取 SHARED 鎖來讀取數據,但是只有一個進程可以獲取 EXCLUSIVE 鎖來寫數據庫。 ### 多線程并發 SQLite 支持多線程并發模式,需要開啟下面的配置,當然系統 SQLite 會默認開啟多線程Multi-thread 模式。 ``` PRAGMA SQLITE\_THREADSAFE = 2 ``` 跟多進程的鎖機制一樣,為了實現簡單,SQLite 鎖的粒度都是數據庫文件級別,并沒有實現表級甚至行級的鎖。還有需要說明的是,同一個句柄同一時間只有一個線程在操作,這個時候我們需要打開連接池 Connection Pool。 跟多進程類似,多線程可以同時讀取數據庫數據,但是寫數據庫依然是互斥的。SQLite 提供了 Busy Retry 的方案,即發生阻塞時會觸發 Busy Handler,此時可以讓線程休眠一段時間后,重新嘗試操作,你可以參考《微信 iOS SQLite 源碼優化實踐》這篇文章。為了進一步提高并發性能,我們還可以打開WAL(Write-Ahead Logging)模式。WAL 模式會將修改的數據單獨寫到一個 WAL 文件中,同時也會引入了 WAL 日志文件鎖。通過 WAL 模式讀和寫可以完全地并發執行,不會互相阻塞。 為了進一步提高并發性能,我們還可以打開WAL(Write-Ahead Logging)模式。WAL 模式會將修改的數據單獨寫到一個 WAL 文件中,同時也會引入了 WAL 日志文件鎖。通過 WAL 模式讀和寫可以完全地并發執行,不會互相阻塞。 ``` PRAGMA schema.journal_mode = WAL ``` 但是需要注意的是,寫之間是仍然不能并發。如果出現多個寫并發的情況,依然有可能會出現 SQLiteDatabaseLockedException。這個時候我們可以讓應用中捕獲這個異常,然后等待一段時間再重試。 ``` } catch (SQLiteDatabaseLockedException e) { if (sqliteLockedExceptionTimes < (tryTimes - 1)) { try { Thread.sleep(100); } catch (InterruptedException e1) { } } sqliteLockedExceptionTimes++; } ``` 總的來說通過連接池與 WAL 模式,我們可以很大程度上增加 SQLite 的讀寫并發,大大減少由于并發導致的等待耗時,建議大家在應用中可以嘗試開啟。 ## 組件優化 ### mmap接口 使用語句“PRAGMA mmap\_size=268435456;”開啟內存映射。 在讀取和寫入過程中,每次都將用戶空間的數據和內核空間的數據拷貝一次,能否直接將文件讀取到用戶空間?SQLite3提供了mmap方式的IO。 理論上mmap方式能減少內核和用戶空間的IO,但在iOS系統中,這個從我這里測試效果看,影響并不大。 ### synchronous?關閉文件強制同步 總體數據占用量大,但是可重建恢復的數據庫,可以關閉synchronous以提高性能。 * PRAGMA synchronous = FULL; (2)? * PRAGMA synchronous = NORMAL; (1)? * PRAGMA synchronous = OFF; (0) #### 參數含義 當synchronous設置為FULL (2), SQLite數據庫引擎在緊急時刻會暫停以確定數據已經寫入磁盤。這使系統崩潰或電源出問題時能確保數據庫在重起后不會損壞。FULL synchronous很安全但很慢。 當synchronous設置為NORMAL, SQLite數據庫引擎在大部分緊急時刻會暫停,但不像FULL模式下那么頻繁。 NORMAL模式下有很小的幾率(但不是不存在)發生電源故障導致數據庫損壞的情況。但實際上,在這種情況 下很可能你的硬盤已經不能使用,或者發生了其他的不可恢復的硬件錯誤。 設置為synchronous OFF (0)時,SQLite在傳遞數據給系統以后直接繼續而不暫停。若運行SQLite的應用程序崩潰, 數據不會損傷,但在系統崩潰或寫入數據時意外斷電的情況下數據庫可能會損壞。另一方面,在synchronous OFF時 一些操作可能會快50倍甚至更多。在SQLite 2中,缺省值為NORMAL.而在3中修改為FULL。 ### WAL模式 WAL模式會在共享內存中根據數據順序建立索引,每個讀操作都會記錄一下最新的數據更改索引,讀操作只會讀取此索引之前的數據,而寫操作可以繼續在WAL中追加數據,并發性能有一定提升。 ##### 缺點 當每個事務數據量比較大時,接近或超過1000頁的數據量時,會導致WAL內容頻繁同步至實際數據庫文件,導致性能下降。 #### 測試總結 * 異步IO似乎并不能提高多少性能,官方已經deprecate它了,推薦使用WAL模式。 * 大量小記錄寫入(不合并為事務)時,一般模式即使關閉文件sync,還沒有WAL全sync模式快。 * 操作頻繁,單條記錄數據量小的,建議使用WAL模式。 * 操作少,單條記錄數據量大,建議使用一般數據庫,不要使用WAL模式。 ### 頁大小與緩存大小 在 I/O 文件系統中,我講過數據庫就像一個小文件系統一樣,事實上它內部也有頁和緩存的概念。 對于 SQLite 的 DB 文件來說,頁(page)是最小的存儲單位,如下圖所示每個表對應的數據在整個 DB 文件中都是通過一個一個的頁存儲,屬于同一個表不同的頁以 B 樹(B-tree)的方式組織索引,每一個表都是一棵 B 樹。 ![](https://img.kancloud.cn/27/89/278931243cdf9ffba2fac2750040d961_567x384.png) 跟文件系統的頁緩存(Page Cache)一樣,SQLite 會將讀過的頁緩存起來,用來加快下一次讀取速度。頁大小默認是 1024Byte,緩存大小默認是 1000 頁。 ``` PRAGMA page_size = 1024 PRAGMA cache_size = 1000 ``` 每個頁永遠只存放一個表或者一組索引的數據,即不可能同一個頁存放多個表或索引的數據,表在整個 DB 文件的第一個頁就是這棵 B 樹的根頁。繼續以上圖為例,如果想查詢 rowID 為 N+2 的數據,我們首先要從 sqlite\_master 查找出 table 的 root page 的位置,然后讀取 root page、page4 這兩個頁,所以一共會需要 3 次 I/O。 ![](https://img.kancloud.cn/3d/ea/3deaa8fe2b9523666da18ad4e065d512_696x468.png) 從上表可以看到,增大 page size 并不能不斷地提升性能,在拐點以后可能還會有副作用。我們可以通過 PRAGMA 改變默認 page size 的大小,也可以再創建 DB 文件的時候進行設置。但是需要注意如果存在老的數據,需要調用 vacuum 對數據表對應的節點重新計算分配大小。 在微信的內部測試中,如果使用 4KB 的 page size 性能提升可以在 5%~10%。但是考慮到歷史數據的遷移成本,最終還是使用 1024Byte。所以這里建議大家在新建數據庫的時候,就提前選擇 4KB 作為默認的 page size 以獲得更好的性能。 ### Cursor 實現優化 wcdb方案 Android 框架查詢數據庫使用的是 Cursor 接口,調用 SQLiteDatabase.query(...) 會返回一個Cursor 對象,之后就可以使用 Cursor 遍歷結果集了。Android SDK SQLite Cursor 的實現是分配一個固定 2MB 大小的緩沖區,稱作 Cursor Window,用于存放查詢結果集。 查詢時,先分配Cursor Window,然后執行 SQL 獲取結果集填充之,直到 Cursor Window 放滿或者遍歷完結果集,之后將 Cursor 返回給調用者。 假如 Cursor 遍歷到緩沖區以外的行,Cursor 會丟棄之前緩沖區的所有內容,重新查詢,跳過前面的行,重新選定一個開始位置填充 Cursor Window 直到緩沖區再次填滿或遍歷完結果集。 這樣的實現能保證大部分情況正常工作,在很多情況下卻不是最優實現。微信對 DB 操作最多的場景是獲取 Cursor 直接遍歷獲取數據后關閉,獲取到的數據,一般是生成對應的實體對象(通過 ORM 或者自行從 Cursor 轉換)后放到 List 或 Map 等容器里返回,或用于顯示,或用于其他邏輯。 在這種場景下,先將數據保存到 Cursor Window 后再取出,中間要經歷兩次內存拷貝和轉換(SQLite → CursorWindow → Java),這是完全沒有必要的。另外,由于 Cursor Window 是定長的,對于較小的結果集,需要無故分配 2MB 內存,對于大結果集,如果 2MB 不足以放下,遍歷到途中還會引發 Cursor 重查詢,這個消耗就相當大了。 Cursor Window,其實也是在 JNI 層通過 SQLite 庫的 Statement 填充的,Statement 這里可以理解為一個輕量但只能往前遍歷,沒有緩存的 Cursor。這個不就跟我們的場景一致嗎?何不直接使用底層的 Statement 呢?我們對 Statement 做了簡單的封裝,暴露了 Cursor 接口, SQLiteDirectCursor 就誕生了,它直接操作底層 SQLite 獲取數據,只能執行往前迭代的操作,但這完全滿足需要。 ## 讀寫優化 ### 索引優化 正確使用索引在大部分的場景可以大大降低查詢速度,微信的數據庫優化也是通過索引開始。下面是索引使用非常簡單的一個例子,我們先從索引表找到數據對應的 rowid,然后再從原數據表直接通過 rowid 查詢結果。 ![](https://img.kancloud.cn/04/4f/044f30f296c61e7baa398fbbf11e4332_672x141.png) 建立索引是有代價的,需要一直維護索引表的更新。比如對于一個很小的表來說就沒必要建索引;如果一個表經常是執行插入更新操作,那么也需要節制的建立索引。總的來說有幾個原則: * 建立正確的索引。這里不僅需要確保索引在查詢中真正生效,我們還希望可以選擇最高效的索引。如果一個表建立太多的索引,那么在查詢的時候 SQLite 可能不會選擇最好的來執行。 * 單列索引、多列索引與復合索引的選擇。索引要綜合數據表中不同的查詢與排序語句一起考慮,如果查詢結果集過大,還是希望可以通過復合索引直接在索引表返回查詢結果。 * 索引字段的選擇。整型類型索引效率會遠高于字符串索引,而對于主鍵 SQLite 會默認幫我們建立索引,所以主鍵盡量不要用復雜字段。 **總的來說索引優化是 SQLite 優化中最簡單同時也是最有效的,但是它并不是簡單的建一個索引就可以了,有的時候我們需要進一步調整查詢語句甚至是表的結構,這樣才能達到最好的效果。** ### 其他優化 * 慎用“select\*”,需要使用多少列,就選取多少列。 * 正確地使用事務。 * 預編譯與參數綁定,緩存被編譯后的 SQL 語句。對于 blob 或超大的 Text 列,可能會超出一個頁的大小,導致出現超大頁。建議將這些列單獨拆表,或者放到表字段的后面。 * 定期整理或者清理無用或可刪除的數據,例如朋友圈數據庫會刪除比較久遠的數據,如果用戶訪問到這部分數據,重新從網絡拉取即可。 * 分庫 ## 檢測 ### 檢測索引使用問題 索引的使用問題是數據庫最常見的問題,也是最直接影響性能的問題。SQLiteLint 的分析主要基于 SQLite3 的 "explain query plan" ,即 sql 的查詢計劃。先簡單說下查詢計劃的最常見的幾個關鍵字: * * * SCAN TABLE: 全表掃描,遍歷數據表查找結果集,復雜度 O(n) SEARCH TABLE: 利用索引查找,一般除了 without rowid 表或覆蓋索引等,會對索引樹先一次 Binary Search 找到 rowid ,然后根據得到 rowid 去數據表做一次 Binary Search 得到目標結果集,復雜度為 O(logn) USE TEMP B-TREE: 對結果集臨時建樹排序,額外需要空間和時間。比如有 Order By 關鍵字,就有可能出現這樣查詢計劃 * * * 通過分析查詢計劃,SQLiteLint 目前主要檢查以下幾個索引問題: #### 1\. 未建索引導致的全表掃描(對應查詢計劃的 SCAN TABLE... ) 雖然建立索引是最基本優化技巧,但實際開發中,很多同學因為意識不夠或者需求太緊急,而疏漏了建立合適的索引,SQLiteLint 幫助提醒這種疏漏。問題雖小,解決也簡單,但最普遍存在。 這里也順帶討論下一般不適合建立索引的情況:寫多讀少以及表行數很小。但對于客戶端而言,寫多讀少的表應該不常見。而表行數很小的情況,建索引是有可能導致查詢更慢的(因為索引的載入需要的時間可能大過全表掃描了),但是這個差別是微乎其微的。所以這里認為一般情況下,客戶端的查詢還是盡量使用索引優化,如果確定預估表數量很小或者寫多讀少,也可以將這個表加到不檢測的白名單。 解決這類問題,當然是建立對應的索引。 #### 2\. 索引未生效導致的全表掃描(對應查詢計劃的 SCAN TABLE... ) 有些情況即便建立了索引,但依然可能不生效,而這種情況有時候是可以通過優化 sql 語句去用上索引的。舉個例子: ![](https://img.kancloud.cn/18/8a/188a147725c7bcd91ce0911b1ec4980a_1080x268.png) 以上看到,即便已建立了索引,但實際沒有使用索引來查詢。 如對于這個 case ,可以把 like 變成不等式的比較: ![](https://img.kancloud.cn/1e/c7/1ec7510621f5f8c140d6a97ced222d30_1080x72.png) 這里看到已經是使用索引來 SEARCH TABLE ,避免了全表掃描。但值得注意的是并不是所有 like 的情況都可以這樣優化,如 like '%lo' 或 like '%lo%' ,不等式就做不到了。 再看個位操作導致索引不生效的例子: ![](https://img.kancloud.cn/08/cc/08cca8fc6db477b26152b7d9cd215b0b_1080x219.png) 位操作是最常見的導致索引不生效的語句之一。但有些時候也是有些技巧的利用上索引的,假如這個 case 里 flag 的業務取值只有 0x1,0x2,0x4,0x8 ,那么這條語句就可以通過窮舉值的方式等效: ![](https://img.kancloud.cn/a2/5d/a25d86b181b69fa5dd6089c0410f05e2_1080x100.png) 以上看到,把位操作轉成 in 窮舉就能利用索引了。 解決這類索引未生效導致的全表掃描 的問題,需要結合實際業務好好優化sql語句,甚至使用一些比較trick的技巧。也有可能沒辦法優化,這時需要添加到白名單。 #### 3\. 不必要的臨時建樹排序(對應查詢計劃的 USE TEMP B-TREE... )。 比如sql語句中 order by 、distinct 、group by 等就有可能引起對結果集臨時額外建樹排序,當然很多情況都是可以通過建立恰當的索引去優化的。舉個例子: ![](https://img.kancloud.cn/af/3a/af3ab8444237280bd4d85d262ecd9833_1080x238.png) 以上看到,即便id和mark都分別建立了索引,即便只需要一行結果,依然會引起重新建樹排序( USE TEMP B-TREE FOR ORDER BY )。當然這個case非常簡單,不過如果對 SQLite 的索引不熟悉或者開發時松懈了,確實很容易發生這樣的問題。同樣這個問題也很容易優化: ![](https://img.kancloud.cn/d2/45/d245ef8eec30f0358548cecd0ecff900_1080x94.png) 這樣就避免了重新建樹排序,這對于數據量大的表查詢,優化效果是立竿見影的好。 解決這類問題,一般就是建立合適的索引。 #### 4\. 不足夠的索引組合 這個主要指已經建立了索引,但索引組合的列并沒有覆蓋足夠 where 子句的條件式中的列。SQLiteLint 檢測出這種問題,建議先關注該 sql 語句是否有性能問題,再決定是否建立一個更長的索引。舉個例子: ![](https://img.kancloud.cn/02/a8/02a8d1208a21f119af4760ba412bfff5_1080x269.png) 以上看到,確實是利用了索引 genderIndex 來查詢,但看到where子句里還有一個 mark=60 的條件,所以還有一次遍歷判斷操作才能得到最終需要的結果集。尤其對于這個 case,gender 也就是性別,那么最多 3種情況,這個時候單獨的 gender 索引的優化效果的已經不明顯了。而同樣,優化也是很容易的: ![](https://img.kancloud.cn/42/02/42024f0268f81fb609100e89cf815082_1080x112.png) 解決這類問題,一般就是建立一個更大的組合索引。 #### 5\. 怎么降低誤報 現在看到 SQLiteLint 主要根據查詢計劃的某些關鍵字去發現這些問題,但SQLite支持的查詢語法是非常復雜的,而對應的查詢計劃也是無窮變化的。所以對查詢計劃自動且正確的分析,不是一件容易的事。SQLiteLint 很大的功夫也在這件事情上 所以對查詢計劃自動且正確的分析,不是一件容易的事。SQLiteLint 很大的功夫也在這件事情上。SQLiteLint 這里主要對輸出的查詢計劃重新構建了一棵有一定的特點的分析樹,并結合sql語句的語法樹,依據一定的算法及規則進行分析檢測。建分析樹的過程會使用到每條查詢計劃前面如 "0|1|0" 的數字,這里不具體展開了。 舉個例子:是不是所有帶有 "SCAN TABLE" 前綴的查詢計劃,都認為是需要優化的呢?明顯不是。具體看個 case : ![](https://img.kancloud.cn/84/0e/840e7bb47b9816f883b234b4b9e54ed7_1080x182.png) 這是一個聯表查詢,在 SQLite 的實現里一般就是嵌套循環。在這個語句中里, t3.id 列建了索引,并且在第二層循環中用上了,但第一層循環的 SCAN TABLE是無法優化的。比如嘗試給t4的id列也建立索引: ![](https://img.kancloud.cn/95/09/95095a4a815b45254eb4b0b3fd18990e_1080x161.png) 可以看出,依然無法避免 SCAN TABLE 。對于這種 SCAN TABLE 無法優化的情況,SQLiteLint 不應該誤報。前面提到,會對查詢計劃組織成樹的結構。比如對于這個 case ,最后構建的查詢計劃分析樹為: ![](https://img.kancloud.cn/c2/02/c20298ed96e6e395da1275a53a61b7e9_540x305.png) 分析樹,有個主要的特點:葉子節點有兄弟節點的是聯表查詢,其循環順序對應從左往右,而無兄弟節點是單表查詢。而最后的分析會落地到葉子節點的分析。遍歷葉子節點時,有一條規則(不完整描述)是: *葉子節點有兄弟節點的,且是最左節點即第一層循環,且 where 子句中不含有相關常量條件表達式時,SCAN TABLE 不認為是質量問題。* 這里有兩個條件必須同時滿足,SCAN TABLE 才不報問題:第一層循環 & 無相關常量表達式。第一層循環前面已經描述,這里再解釋下后面一個條件。 ![](https://img.kancloud.cn/95/09/95095a4a815b45254eb4b0b3fd18990e_1080x161.png) 由上看到,當select子句中出現常量條件表達式 “t4.id=666” , 若 t3.id,t4.id 都建了索引,是可以優化成沒有 SCAN TABLE 。 ![](https://img.kancloud.cn/ce/f4/cef4b433af27ef810e11465c24dd5087_1080x122.png) 而把 t4.id 的索引刪除后,又出現了 SCAN TABLE 。而這種 SCAN TABLE 的情況,不滿足規則里的的第二個條件,SQLiteLint 就會報出可以使用索引優化了。 這里介紹了一個較簡單語句的查詢計劃的分析,當然還有更復雜的語句,還有子查詢、組合等等,這里不展開討論了。巨大的復雜性,無疑對準確率有很大的挑戰,需要對分析規則不斷地迭代完善。當前 SQLiteLint 的分析算法依然不足夠嚴謹,還有很大的優化空間。 這里還有另一個思路去應對準確性的問題:對所有上報的問題,結合耗時、是否主線程、問題等級等信息,進行優先級排序。這個“曲線救國”來降低誤報的策略也適用本文介紹的所有檢測問題。 ### 檢測冗余索引問題 SQLiteLint 會在應用啟動后對所有的表檢測一次是否存在冗余索引,并建議保留最大那個索引組合。 先定義什么是冗余索引:如對于某個表,如果索引組合 index1,index2 是另一個索引組合 index3 的前綴,那么一般情況下 index3 可以替代掉 index1 和 index2 的作用,所以 index1,index2 就冗余了。而多余的索引就會有多余的插入消耗和空間消耗,一般就建議只保留索引 index3 。 看個例子: ![](https://img.kancloud.cn/c1/7b/c17b8fea4457e04baaa817521118d7bc_990x276.png) 以上看到,如果已經有一個 length 和 type 的組合索引,就已經滿足了單 length 列條件式的查詢,沒必要再為 length 再建一個索引。 ### 檢測 select \* 問題 SQLiteLint這里通過掃描 sql 語法樹,若發現 select \* 子句,就會報問題,建議盡量避免使用 select \* ,而是按需 select 對應的列。 select \* 是SQLite最常用的語句之一,也非常方便,為什么還認為是問題的呢?這里有必要辯駁一下: 1. 對于 select \* ,SQLite 底層依然存在一步把 \* 展開成表的全部列。 2. select \* 也減少了可以使用覆蓋索引的機會。覆蓋索引指索引包含的列已經覆蓋了 select 所需要的列,而使用上覆蓋索引就可以減少一次數據表的查詢。 3. 對于 Android 平臺而言,select \* 就會投射所有的列,那么每行結果占據的內存就會相對更大,那么 CursorWindow(緩沖區)的容納條數就變少,那么 SQLiteQuery.fillWindow 的次數就可能變多,這也有一定的性能影響。 基于以上原因,出于 SQLiteLint 目標最佳實踐的原則,這里依然報問題。 ### 檢測 Autoincrement 問題 SQLiteLint 在應用啟動后會檢測一次所有表的創建語句,發現 AUTOINCREMENT 關鍵字,就會報問題,建議避免使用 Autoincrement 。 這里看下為什么要檢測這個問題,下面引用 SQLite 的官方文檔: > The AUTOINCREMENT keyword imposes extra CPU, memory, disk space, and disk I/O overhead and should be avoided if not strictly needed. It is usually not needed. 可以看出 Auto Increment 確實不是個好東西。 ps. 我這里補充說明一下 strictly needed 是什么是意思,也就是為什么它不必要。通常 AUTOINCREMENT 用于修飾 INTEGER PRIMARY KEY 列,后簡稱IPK 列。而 IPK 列等同于 rowid 別名,本身也具有自增屬性,但會復用刪除的 rowid 號。比如當前有 4 行,最大的rowid是 4,這時把第 4 行刪掉,再插入一行,新插入行的 rowid 取值是比當前最大的 rowid 加 1,也就 3+1=4 ,所以復用了 rowid 號 4 。而如果加以 AUTOINCREMENT 修飾就是阻止了復用,在這個情況,rowid 號是 5 。也就是說,AUTOINCREMENT 可以保證了歷史自增的唯一性,但對于客戶端應用有多少這樣的場景呢? ### 檢測建議使用 prepared statement SQLiteLint 會以抽樣的時機去檢測這個問題,比如每 50 條執行語句,分析一次執行序列,如果發現連續執行次數超過一定閾值的相同的(當然實參可以不同)而未使用 prepared statement 的 sql 語句,就報問題,建議使用 prepared statement 優化。 如閾值是 3 ,那么連續執行下面的語句,就會報問題: ![](https://img.kancloud.cn/e9/ec/e9ecee6d65720177dab33565029b6dfa_722x132.png) 使用 prepared statement 優化的好處有兩個: 1. 對于相同(實參不同)的 sql 語句多次執行,會有性能提升 2. 如果參數是不可信或不可控輸入,還防止了注入問題 ### 檢測建議使用 without rowid 特性 SQLiteLint 會在應用啟動后檢測一次所有表的創建語句,發現未使用 without rowid 技巧且根據表信息判斷適合使用 without rowid 優化的表,就報問題,建議使用 without rowid 優化。 這是 SQLiteLint 的另一個思路,就是發現是否可以應用上一些 SQLite 的高級特性。 without rowid 在某些情況下可以同時帶來空間以及時間上將近一半的優化。簡單說下原理,如: ![](https://img.kancloud.cn/0c/73/0c737497395e59087450b33572f3d709_1020x104.png) 對于這個含有 rowid 的表( rowid 是自動生成的),這時這里涉及到兩次查詢,一次在 name 的索引樹上找到對應的 rowid ,一次是用這個 rowid 在數據樹上查詢到 mark 列。 而使用 without rowid 來建表: ![](https://img.kancloud.cn/79/ba/79ba428580fe7f426b6bfa9f5c2b2f01_1080x57.png) 數據樹構建是以 name 為 key ,mark 為 data 的,并且是以普通 B-tree 的方式存儲。這樣對于剛剛同樣的查詢,就需要只有一次數據樹的查詢就得到了 mark 列,所以算法復雜度上已經省了一個 O(logn)。另外又少維護了一個 name 的索引樹,插入消耗和空間上也有了節省。 當然 withou rowid 不是處處適用的,不然肯定是默認屬性了。SQLiteLint 判斷如果同時滿足以下兩個條件,就建議使用 without rowid : 1. 表含有 non-integer or composite (multi-column) PRIMARY KEY 2. 表每行數據大小不大,一個比較好的標準是行數據大小小于二十分之一的page size 。ps.默認 page size SQLite 版本3.12.0以后(對應 Android O 以上)是 4096 bytes ,以前是 1024 。而由于行數據大小業務相關,為了降低誤報,SQLiteLint 使用更嚴格的判定標準:表不含有 BLOB 列且不含有非 PRIMARY KEY TEXT 列。 簡單說下原因: 對于1,假如沒有 PRIMARY KEY ,無法使用 without rowid 特性;假如有 INTEGER PRIMARY KEY ,前面也說過,這時也已經等同于 rowid 。 對于 2,小于 20 分之一 pagesize 是官方給出的建議。 這里說下我理解的原因。page 是 SQLite 一般的讀寫單位(實際上磁盤的讀寫 block 更關鍵,而磁盤的消耗更多在定位上,更多的page就有可能需要更多的定位)。without rowid 的表是以普通 B-Tree 存儲的,而這時數據也存儲在所有樹結點上,那么假如數據比較大,一個 page 存儲的結點變少,那么查找的過程就需要讀更多的 page ,從而查找的消耗更大。當然這是相對 rowid 表 B\*-Tree 的存儲來說的,因為這時數據都在葉子結點,搜索路徑上的結點只有 KEY ,那么一個page能存的結點就多了很多,查找磁盤消耗變小。這里注意的是,不要以純內存的算法復雜度去考量這個問題。以上是推論不一定正確,歡迎指教。 引申一下,這也就是為什么 SQLite 的索引樹以 B-Tree 組織,而 rowid 表樹以 B\*-Tree 組織,因為索引樹每個結點的存主要是索引列和 rowid ,往往沒這么大,相對 B\*-Tree 優勢就在于不用一直查找到葉子結點就能結束查找。與 without rowid 同樣的限制,不建議用大 String 作為索引列,這當然也可以加入到 SQLiteLint 的檢測。 ## 異常修復 ### 官方的Dump恢復方案 Google 一下SQLite DB恢復,不難搜到使用`.dump`命令恢復DB的方法。`.dump`命令的作用是將 整個數據庫的內容輸出為很多 SQL 語句,只要對空 DB 執行這些語句就能得到一個一樣的 DB。 `.dump`命令原理很簡單:每個SQLite DB都有一個`sqlite_master`表,里面保存著全部table 和index的信息(table本身的信息,不包括里面的數據哦),遍歷它就可以得到所有表的名稱和`CREATE TABLE ...`的SQL語句,輸出`CREATE TABLE`語句,接著使用`SELECT * FROM ...`通過表名遍歷整個表,每讀出一行就輸出一個`INSERT`語句,遍歷完后就把整個DB dump出來了。 這樣的操作,和普通查表是一樣的,遇到損壞一樣會返回`SQLITE_CORRUPT`,我們忽略掉損壞錯誤, 繼續遍歷下個表,最終可以把所有沒損壞的表以及**損壞了的表的前半部分**讀取出來。將dump 出來的SQL語句逐行執行,最終可以得到一個等效的新DB。由于直接跑在SQLite上層,所以天然 就支持加密SQLCipher,不需要額外處理。 ![](https://img.kancloud.cn/8d/2b/8d2b2982ba19e5f431c25f7ef40aa60c_640x276.png) 這個方案不需要任何準備,只有壞DB的用戶要花好幾分鐘跑恢復,大部分用戶是不感知的。 數據量大小,主要影響恢復需要的臨時空間:先要保存dump 出來的SQL的空間,這個 大概一倍DB大小,還要另外一倍 DB大小來新建 DB恢復。至于我們最關心的成功率呢?上線后,**成功率約為30%**。這個成功率的定義是至少恢復了一條記錄,也就是說一大半用戶 一條都恢復不成功! 研究一下就發現,恢復失敗的用戶,原因都是`sqlite_master`表讀不出來,特別是第一頁損壞, 會導致后續所有內容無法讀出,那就完全不能恢復了。恢復率這么低的尷尬狀況維持了好久, 其他方案才漸漸露出水面。 ### 備份恢復方案 定期備份`sqlite_master`表 ### 刪除wal文件 ## 全文搜索 ## 參考資料 [微信iOS SQLite源碼優化實踐](https://mp.weixin.qq.com/s/8FjDqPtXWWqOInsiV79Chg) [Matrix SQLiteLint -- SQLite 使用質量檢測](https://mp.weixin.qq.com/s/laUgOmAcMiZIOfM2sWrQgw) [微信移動端數據庫組件WCDB系列(二) — 數據庫修復三板斧](https://mp.weixin.qq.com/s/Ln7kNOn3zx589ACmn5ESQA) [WCDB 的 WAL 模式和異步 Checkpoint](https://cloud.tencent.com/developer/article/1031030) [Sqlite3并發讀寫注意事項](https://blog.csdn.net/hqfok/article/details/75020563) [SQLite 數據庫 WAL 工作模式原理簡介](https://www.jianshu.com/p/b47986e7e734)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看