對於批量資料插入這種最常見的情況來說,我們來看兩種實現方式(兩種都用了事務)。
下面這種應該是最多人使用的插入資料的方法:
1 public long addByExec(List<Person> persons) { 2 3 long start = System.currentTimeMillis(); 4 db.beginTransaction(); 5 6 for (Person person : persons) { 7 db.execSQL(" INSERT INTO person(name,age,info) VALUES(?, ?, ?) ", 8 new Object[] { person.name, person.age, person.info }); 9 } 10 11 db.setTransactionSuccessful(); 12 long end = System.currentTimeMillis(); 13 db.endTransaction(); 14 return end - start; 15 16 }
再看一種比較少用的插入方法
1 public long addByStatement(List<Person> persons) { 2 long start = System.currentTimeMillis(); 3 db.beginTransaction(); 4 SQLiteStatement sqLiteStatement = db.compileStatement(sql); 5 6 for (Person person : persons) { 7 sqLiteStatement.bindString(1, person.name); 8 sqLiteStatement.bindString(2, person.age); 9 sqLiteStatement.bindString(3, person.info); 10 sqLiteStatement.executeInsert(); 11 } 12 db.setTransactionSuccessful(); 13 long end = System.currentTimeMillis(); 14 db.endTransaction(); 15 return end - start; 16 }
然後我們分別用這兩個方法 來向資料庫裡面插入一萬條資料 看看耗時多少。為了演示效果更加突出一點,我錄製了一個GIF,同時,
這2個方法我也沒有用子執行緒來操作他,直接在ui執行緒上操作 所以看起來效果會比較突出一些(但是自己寫程式碼的時候千萬別這麼寫小心ANR)。
可以看出來後者耗時幾乎只有前者的 一半(所以以後大家在做大批量資料插入的時候可以考慮後者的實現方式)。我們來看看原始碼為啥會這樣。
首先看前者的實現方法原始碼
1 public void execSQL(String sql, Object[] bindArgs) throws SQLException { 2 if (bindArgs == null) { 3 throw new IllegalArgumentException("Empty bindArgs"); 4 } 5 executeSql(sql, bindArgs); 6 } 7 8 private int executeSql(String sql, Object[] bindArgs) throws SQLException { 9 if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) { 10 disableWriteAheadLogging(); 11 mHasAttachedDbs = true; 12 } 13 SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs); 14 try { 15 return statement.executeUpdateDelete(); 16 } catch (SQLiteDatabaseCorruptException e) { 17 onCorruption(); 18 throw e; 19 } finally { 20 statement.close(); 21 } 22 }
我們發現 前者的實現 實際上最後也是通過SQLiteStatement 這個類是操作的。
而後者不過是
1 public SQLiteStatement compileStatement(String sql) throws SQLException { 2 verifyDbIsOpen(); 3 return new SQLiteStatement(this, sql, null); 4 }
所以實際上前者之所以比後者耗時 應該是下面這段程式碼的原因:
1 if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) { 2 disableWriteAheadLogging(); 3 mHasAttachedDbs = true; 4 }
1 public static int getSqlStatementType(String sql) { 2 sql = sql.trim(); 3 if (sql.length() < 3) { 4 return STATEMENT_OTHER; 5 } 6 String prefixSql = sql.substring(0, 3).toUpperCase(); 7 if (prefixSql.equals("SEL")) { 8 return STATEMENT_SELECT; 9 } else if (prefixSql.equals("INS") || 10 prefixSql.equals("UPD") || 11 prefixSql.equals("REP") || 12 prefixSql.equals("DEL")) { 13 return STATEMENT_UPDATE; 14 } else if (prefixSql.equals("ATT")) { 15 return STATEMENT_ATTACH; 16 } else if (prefixSql.equals("COM")) { 17 return STATEMENT_COMMIT; 18 } else if (prefixSql.equals("END")) { 19 return STATEMENT_COMMIT; 20 } else if (prefixSql.equals("ROL")) { 21 return STATEMENT_ABORT; 22 } else if (prefixSql.equals("BEG")) { 23 return STATEMENT_BEGIN; 24 } else if (prefixSql.equals("PRA")) { 25 return STATEMENT_PRAGMA; 26 } else if (prefixSql.equals("CRE") || prefixSql.equals("DRO") || 27 prefixSql.equals("ALT")) { 28 return STATEMENT_DDL; 29 } else if (prefixSql.equals("ANA") || prefixSql.equals("DET")) { 30 return STATEMENT_UNPREPARED; 31 } 32 return STATEMENT_OTHER; 33 }
實際上就是多了一個字串處理的函式。這就是為什麼前者耗時要比後者多。因為實際上直接呼叫executeSql的時候
他裡面是先做字串處理然後再呼叫SQLiteStatement來執行,這個過程當然是比我們直接呼叫SQLiteStatement
來執行速度慢的。
我們首先來看一下下面這個函式
1 public Cursor queryTest1() { 2 long start1 = System.currentTimeMillis(); 3 Cursor c = db.rawQuery("select * from t1,t3 where t1.num>t3.num", null); 4 long end1 = System.currentTimeMillis(); 5 Log.v("DBManager", "time1 need " + (end1 - start1)); 6 long start2 = System.currentTimeMillis(); 7 c.moveToNext(); 8 long end2 = System.currentTimeMillis(); 9 Log.v("DBManager", "time2 need" + (end2 - start2)); 10 long start3 = System.currentTimeMillis(); 11 c.moveToNext(); 12 long end3 = System.currentTimeMillis(); 13 Log.v("DBManager", "time3 need" + (end3 - start3)); 14 return c; 15 }
一個很常見的,多表查詢的函式,有些人可能會奇怪為啥在這個地方我要加那麼多日誌。實際上如果你t1和t3的資料都很多的話,這個查詢是可以預料到的會非常耗時。
很多人都會以為這個耗時是在下面這條語句做的:
1 Cursor c = db.rawQuery("select * from t1,t3 where t1.num>t3.num", null);
但是實際上這個查詢耗時是在你第一呼叫
1 c.moveToNext();
來做的,有興趣的同學可以自己試一下,我們這裡就不幫大家來演示這個效果了,但是可以幫助大家分析一下原始碼為什麼會是這樣奇怪的結果?
我們首先來分析一下rawQuery 這個函式
1 public Cursor rawQuery(String sql, String[] selectionArgs) { 2 return rawQueryWithFactory(null, sql, selectionArgs, null); 3 } 4 5 /** 6 * Runs the provided SQL and returns a cursor over the result set. 7 * 8 * @param cursorFactory the cursor factory to use, or null for the default factory 9 * @param sql the SQL query. The SQL string must not be ; terminated 10 * @param selectionArgs You may include ?s in where clause in the query, 11 * which will be replaced by the values from selectionArgs. The 12 * values will be bound as Strings. 13 * @param editTable the name of the first table, which is editable 14 * @return A {@link Cursor} object, which is positioned before the first entry. Note that 15 * {@link Cursor}s are not synchronized, see the documentation for more details. 16 */ 17 public Cursor rawQueryWithFactory( 18 CursorFactory cursorFactory, String sql, String[] selectionArgs, 19 String editTable) { 20 verifyDbIsOpen(); 21 BlockGuard.getThreadPolicy().onReadFromDisk(); 22 23 SQLiteDatabase db = getDbConnection(sql); 24 SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(db, sql, editTable); 25 26 Cursor cursor = null; 27 try { 28 cursor = driver.query( 29 cursorFactory != null ? cursorFactory : mFactory, 30 selectionArgs); 31 } finally { 32 releaseDbConnection(db); 33 } 34 return cursor; 35 }
看一下24行,發現是構造了一個driver物件 然後呼叫這個driver物件的query方法
我們繼續跟蹤原始碼
1 public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable, 2 CancellationSignal cancellationSignal) { 3 mDatabase = db; 4 mEditTable = editTable; 5 mSql = sql; 6 mCancellationSignal = cancellationSignal; 7 } 8 9 public Cursor query(CursorFactory factory, String[] selectionArgs) { 10 final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal); 11 final Cursor cursor; 12 try { 13 query.bindAllArgsAsStrings(selectionArgs); 14 15 if (factory == null) { 16 cursor = new SQLiteCursor(this, mEditTable, query); 17 } else { 18 cursor = factory.newCursor(mDatabase, this, mEditTable, query); 19 } 20 } catch (RuntimeException ex) { 21 query.close(); 22 throw ex; 23 } 24 25 mQuery = query; 26 return cursor; 27 }
發現這個返回的cursor實際上就是直接new出來的一個物件
1 public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { 2 if (query == null) { 3 throw new IllegalArgumentException("query object cannot be null"); 4 } 5 if (query.mDatabase == null) { 6 throw new IllegalArgumentException("query.mDatabase cannot be null"); 7 } 8 mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); 9 mDriver = driver; 10 mEditTable = editTable; 11 mColumnNameMap = null; 12 mQuery = query; 13 14 query.mDatabase.lock(query.mSql); 15 try { 16 // Setup the list of columns 17 int columnCount = mQuery.columnCountLocked(); 18 mColumns = new String[columnCount]; 19 20 // Read in all column names 21 for (int i = 0; i < columnCount; i++) { 22 String columnName = mQuery.columnNameLocked(i); 23 mColumns[i] = columnName; 24 if (false) { 25 Log.v("DatabaseWindow", "mColumns[" + i + "] is " 26 + mColumns[i]); 27 } 28 29 // Make note of the row ID column index for quick access to it 30 if ("_id".equals(columnName)) { 31 mRowIdColumnIndex = i; 32 } 33 } 34 } finally { 35 query.mDatabase.unlock(); 36 } 37 }
所以看到這裡我們就能確定的是rawquery這個方法 返回的cursor實際上就是一個物件,並沒有任何真正呼叫sql的地方。
然後我們來看看我們懷疑的moveToNext這個方法因為從日誌上看耗時的地方在第一次呼叫他的時候,所以我們懷疑真正呼叫查詢sql的地方
在這個函式裡面被觸發。
1 public final boolean moveToNext() { 2 return moveToPosition(mPos + 1); 3 }
1 public final boolean moveToPosition(int position) { 2 // Make sure position isn't past the end of the cursor 3 final int count = getCount(); 4 if (position >= count) { 5 mPos = count; 6 return false; 7 } 8 9 // Make sure position isn't before the beginning of the cursor 10 if (position < 0) { 11 mPos = -1; 12 return false; 13 } 14 15 // Check for no-op moves, and skip the rest of the work for them 16 if (position == mPos) { 17 return true; 18 } 19 20 boolean result = onMove(mPos, position); 21 if (result == false) { 22 mPos = -1; 23 } else { 24 mPos = position; 25 if (mRowIdColumnIndex != -1) { 26 mCurrentRowID = Long.valueOf(getLong(mRowIdColumnIndex)); 27 } 28 } 29 30 return result; 31 }
看一下那個getcount方法
1 @Override 2 public int getCount() { 3 if (mCount == NO_COUNT) { 4 fillWindow(0); 5 } 6 return mCount; 7 } 8 9 private void fillWindow(int startPos) { 10 clearOrCreateLocalWindow(getDatabase().getPath()); 11 mWindow.setStartPosition(startPos); 12 int count = getQuery().fillWindow(mWindow); 13 if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0 14 if (Log.isLoggable(TAG, Log.DEBUG)) { 15 Log.d(TAG, "received count(*) from native_fill_window: " + count); 16 } 17 mCount = count; 18 } else if (mCount <= 0) { 19 throw new IllegalStateException("Row count should never be zero or negative " 20 + "when the start position is non-zero"); 21 } 22 }
發現如果滿足某個條件的話 就呼叫fillwindow這個方法,我們來看看是什麼條件
1 /** The number of rows in the cursor */ 2 private volatile int mCount = NO_COUNT; 3 static final int NO_COUNT = -1;
看到這就明白了,如果你預設的mCount為-1的話就代表你這個cursor裡面還沒有查過嗎,所以必須要呼叫fillwindow方法
-
1 private synchronized SQLiteQuery getQuery() { 2 return mQuery; 3 }
我們來看看這個query是什麼
1 /** The query object for the cursor */ 2 private SQLiteQuery mQuery;
看看他的fillwindow方法
1 /** 2 * Reads rows into a buffer. This method acquires the database lock. 3 * 4 * @param window The window to fill into 5 * @return number of total rows in the query 6 */ 7 /* package */ int fillWindow(CursorWindow window) { 8 mDatabase.lock(mSql); 9 long timeStart = SystemClock.uptimeMillis(); 10 try { 11 acquireReference(); 12 try { 13 window.acquireReference(); 14 int startPos = window.getStartPosition(); 15 int numRows = nativeFillWindow(nHandle, nStatement, window.mWindowPtr, 16 startPos, mOffsetIndex); 17 if (SQLiteDebug.DEBUG_LOG_SLOW_QUERIES) { 18 long elapsed = SystemClock.uptimeMillis() - timeStart; 19 if (SQLiteDebug.shouldLogSlowQuery(elapsed)) { 20 Log.d(TAG, "fillWindow took " + elapsed 21 + " ms: window=\"" + window 22 + "\", startPos=" + startPos 23 + ", offset=" + mOffsetIndex 24 + ", filledRows=" + window.getNumRows() 25 + ", countedRows=" + numRows 26 + ", query=\"" + mSql + "\"" 27 + ", args=[" + (mBindArgs != null ? 28 TextUtils.join(", ", mBindArgs.values()) : "") 29 + "]"); 30 } 31 } 32 mDatabase.logTimeStat(mSql, timeStart); 33 return numRows; 34 } catch (IllegalStateException e){ 35 // simply ignore it 36 return 0; 37 } catch (SQLiteDatabaseCorruptException e) { 38 mDatabase.onCorruption(); 39 throw e; 40 } catch (SQLiteException e) { 41 Log.e(TAG, "exception: " + e.getMessage() + "; query: " + mSql); 42 throw e; 43 } finally { 44 window.releaseReference(); 45 } 46 } finally { 47 releaseReference(); 48 mDatabase.unlock(); 49 } 50 }
一目瞭然,其實就是rawquery返回的是一個沒有意義的cursor物件裡面什麼都沒有,當你呼叫movetonext之類的方法的時候,
會判斷是否裡面沒有資料 如果有資料就返回你要的資料,如果沒有的話,實際上最終呼叫的就是SQLiteQuery這個類的fillwindow方法
來最終執行你寫的sql語句~~耗時也就是在這裡耗時!!!!!切記!不是在rawquery裡耗時的!