iOS9 sqlite3 執行查詢語句十分緩慢的解決方案

一、問題來源

我所就職的公司是一家醫療互聯網企業,作爲一名iOS開發,目前我參與開發迭代的是一款面向醫生用戶的APP,這款APP的主要功能之一就是隨手記病歷,前不久我們的運營同學接到用戶反饋,在病歷數量較多(達到數千份),一些操作十分卡頓。
這個問題很容易就解決了(一些耗時操作卡住了主線程),不過在解決這個問題的過程中,反而發現了更大了一個問題,在sqlite中iOS8和iOS9執行一些相同的查詢語句,效率相差十分巨大!

二、問題表現

我使用了4000左右的數據量進行了測試,發現執行同一條包含子查詢共四個在表在內的查詢語句:

  • 在iPhone6 Plus iOS8.0.2真機上面花費0.8秒左右的時間。
  • 在iPhone6 iOS9.1真機上面花費48秒左右的時間。

三、問題定位

作爲一名開發者,應該都會對這類怪異的問題感興趣(難道是惡趣味),可能是解決這類問題比較有成就感吧。
我也不例外,我就又花費了一些時間嘗試去查一些解決方案,遺憾的是,百度、google之後並沒有找到太有用的東西,只在stackoverflow上面找到一個相同的問題,然而並沒有人回答,只是本人通過修改SQL查詢語句解決了在iOS9查詢速度十分慢的問題。

1.總體問題

沒辦法,看來不能走捷徑了,於是做了進一步測試,發現分別在iOS8和iOS9的真機上面執行一些查詢語句:

  • 單表查詢數據量即使達到上萬,執行相同的sqlite查詢操作性能並沒有明顯的差別,耗時並不會很多(取決於查詢條件和單表列數等)。
  • 大部分多表查詢在數據量大時(數千),性能並不會有明顯差異。
  • 部分多表查詢數據量大時(數千),執行相同的sqlite查詢操作性能上會有較大差別。

看來問題就出在多表查詢上了。

2.具體問題

由於公司相關信息不便公開,這裏以另外的類似場景舉例做描述。

  1. 假如對全國的中學做了以下一些信息統計:學校信息、學校班級班級、班級學生信息,學生期末考試成績信息。共使用4個數據表進行數據存儲。

  2. 需要做的查詢:列出所有——學校基本信息+學校平均成績最好的一個班級+學校某年級總成績最好的一名學生

  3. 發現的問題:原來的查詢會在一條sql語句中完成,執行這條sql時在iOS8和iOS9中時間相差極大(如二、描述)。通過拆分發現,把查詢“學校某年級總成績最好的一名學生”的子查詢拆分出去以後iOS8和iOS9的查詢速度就接近了。

那麼,看來查詢“學校某年級總成績最好的一名學生”的子查詢就是問題的突破點了。

四、解決方案

我先後嘗試了兩種解決方案:

  1. 改變sql語句,做分表查詢,內存組合數據;

  2. 不改變sql語句,添加索引,加快查詢速度。

1.分表查詢,內存組合數據(未選用)

在我們的APP中,在滿足數據展示的前提下,我嘗試將原來的一條查詢拆分兩個查詢,分別對兩個子查詢的速度進行了測試,發現無論在iOS8或iOS9上面速度都是沒問題的。
之所以放棄這個方案,是因爲SQL執行速度的問題解決了,不過新的問題出現了,那就是內存組合數據的效率問題。
內存組合數據就是將兩次的查詢結果所得的兩個model數組組合成一個新的model數組。
以id做關鍵字匹配,效率最低的情況是兩次全量for循環嵌套即n²,實測兩個4000的model數組組合花費了15s左右的時間,後來簡單做了優化,降低到了10s左右。這也是不能接受的。

2.建立索引(選用)

作爲一名客戶端開發,我對數據庫並沒有深入的學習,對數據庫的性能優化並不擅長,於是和一些對數據庫知識有較深瞭解的同事討論這個問題,一位同事提醒我,你可以試試建立索引。
於是,我試着給影響性能的那個表單個列建立了索引,實測,無論在iOS8還是iOS9執行速度上面都沒有問題。

部分測試數據表:
Tip:首次查詢需要建立數據庫連接,耗時較多。

iPhone4s iOS 7.0.4 iPhone 6 Plus iOS 8.0.2 iPhone 6 iOS 9.2
第n次執行測試 添加索引前 添加索引後 添加索引前 添加索引後 添加索引前 添加索引後
1 8.077 7.824 2.216 2.049 49.183 1.969
2 3.256 2.730 0.798 0.768 48.019 0.503
3 3.230 2.629 0.793 0.650 48.443 0.501
4 3.317 2.628 0.823 0.658 47.888 0.564
5 3.249 2.627 0.842 0.646 48.530 0.460
6 3.262 2.629 0.812 0.649 48.479 0.504

部分測試代碼:

NSString *sqlStr = @"SELECT ...";
sqlite3 *database = nil;
if (sqlite3_open([DatabasePath UTF8String], &database) == SQLITE_OK) {
    sqlite3_stmt *statement = nil;
    int status = sqlite3_prepare_v2(database, [sqlStr UTF8String], -1, &statement, NULL);
    if (status == SQLITE_OK) {
        NSInteger j = 0;
        while (sqlite3_step(statement) == SQLITE_ROW) {
            j++;
            NSMutableArray *row = [[NSMutableArray alloc] init];
            for(int i = 0; i < 10; i++){
                [row addObject:[NSString stringWithFormat:@"%s", sqlite3_column_text(statement, i)]];
            }
        }
    } else {
        NSLog(@"Error: failed to prepare");
    }
    sqlite3_finalize(statement);
} else {
    sqlite3_close(database);
    NSAssert1(0, @"Failed to open database with message '%s'.", sqlite3_errmsg(database));
}
sqlite3_close(database);

五、知識拓展

1.如何添加索引

給某個表某列添加索引

CREATE INDEX Index_Name ON Table_Name (Column_Name);

給某個表添加唯一索引(不能有重複的記錄)

CREATE UNIQUE INDEX Index_Name on Table_Name (Column_Name);

給某個表添加組合索引

CREATE INDEX Index_Name on Table_Name (Column_Name1, Column_Name2, ...);

2.可以給任意表添加索引麼

使用索引時,應遵守的準則:

  • 索引不應該使用在較小的表上。
  • 索引不應該使用在有頻繁的大批量的更新或插入操作的表上。
  • 索引不應該使用在含有大量的NULL值的列上。
  • 索引不應該使用在頻繁操作的列上。

3.怎麼使用索引

sqlite中(其它的沒有做了解)不需要顯式的用

SELECT ... FROM Table_Name INDEX BY ...

的語法去使用索引,假如你添加了索引,執行sql查詢語句時會自動使用。

4.怎麼看是否使用了索引

你可以使用這種方法查看你的sql語句是否使用了索引進行查詢:

EXPLAIN QUERY PLAN SelectQueryString;

結果表樣式

selectid order form detail

如果你建立了索引,執行查詢語句時使用到了索引時,detail列中結果樣式

SEARCH TABLE Table_Name USING INDEX sqlite_autoindex_Table_Name_n (Column_Name=?) (~1 rows)

六、小結

iOS9中部分sql的執行速度異常慢的問題應該是apple的一個BUG,目前我暫未聯繫apple確定,可以考慮的解決方案就是在數據庫搜索本身的性能上下功夫,假如你也遇到類似的問題,而且發現了更好的解決方案,希望可以評論告訴我。

參考:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章