AndroidアプリでSQLite使おうと思って、導入してみたものの、どうも処理が遅いように感じました。
というわけで、自分が使いたかったクエリについて、少し測定してみました。
プログラム
画面にボタン置いて、クエリを発行する処理を書いただけです。
public class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context, int version) {
super(context, "bench.db", null, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("create table bench (id int primary key, value varchar(64), created datetime)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
public class SQLiteBenchActivity extends Activity {
private TextView statusText = null;
private DatabaseHelper databaseHelper = null;
private SQLiteDatabase database = null;
private long startTime = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
statusText = (TextView) findViewById(R.id.status_text);
databaseHelper = new DatabaseHelper(this, 1);
database = databaseHelper.getWritableDatabase();
}
@Override
public void onDestroy() {
databaseHelper.close();
super.onDestroy();
}
public void onClearClick(View v) {
startTimer();
database.execSQL("delete from bench");
stopTimer("clear");
}
public void onSelectClick(View v) {
startTimer();
Cursor cursor = database.rawQuery("select value, created from bench limit 100", null);
if (!cursor.moveToFirst())
return;
do {
String value = cursor.getString(0);
String created = cursor.getString(1);
Log.d("LOG", "value: " + value + ", created: " + created);
} while (cursor.moveToNext());
stopTimer("select");
}
public void onWithoutTransactionClick(View v) {
startTimer();
for (int i = 0; i < 100; i++) {
database.execSQL("insert into bench (id, value, created) values (?, 'abcdefghijklmnop', datetime('now'))",
new String[] { String.valueOf(i + 1) });
}
stopTimer("insert w/o transaction");
}
public void onWithTransactionClick(View v) {
startTimer();
database.beginTransaction();
for (int i = 0; i < 100; i++) {
database.execSQL("insert into bench (id, value, created) values (?, 'abcdefghijklmnop', datetime('now'))",
new String[] { String.valueOf(i + 1) });
}
database.setTransactionSuccessful();
database.endTransaction();
stopTimer("insert w/ transaction");
}
public void onReplaceClick(View v) {
startTimer();
database.beginTransaction();
for (int i = 0; i < 100; i++) {
database.execSQL("replace into bench (id, value, created) values (?, 'abcdefghijklmnop', datetime('now'))",
new String[] { String.valueOf(i + 1) });
}
database.setTransactionSuccessful();
database.endTransaction();
stopTimer("replace w/ transaction");
}
public void onInsertWhileSelectClick(View v) {
startTimer();
database.beginTransaction();
for (int i = 0; i < 100; i++) {
Cursor cursor = database.rawQuery("select value, created from bench where id = ?", new String[] { String.valueOf(i + 1) });
if (!cursor.moveToFirst())
continue;
String value = cursor.getString(0);
String created = cursor.getString(1);
Log.d("LOG", "value: " + value + ", created: " + created);
database.execSQL("insert into bench (id, value, created) values (?, 'abcdefghijklmnop', datetime('now'))",
new String[] { String.valueOf(i + 100 + 1) });
}
database.setTransactionSuccessful();
database.endTransaction();
stopTimer("insert while select");
}
private void startTimer() {
startTime = System.currentTimeMillis();
}
private void stopTimer(String processName) {
long elapsedTime = System.currentTimeMillis() - startTime;
statusText.setText(statusText.getText() + "\n" + processName + ": " + elapsedTime + " msec (" + numOfRecords() + " records)");
}
private int numOfRecords() {
Cursor cursor = database.rawQuery("select count(*) from bench", null);
if (!cursor.moveToFirst())
return 0;
return cursor.getInt(0);
}
}
測定結果
あまり真面目に測ってませんが、それでも傾向は見えました。
実行端末はGalaxy NEXUS、Androidのバージョンは4.0.3です。
1件取得するSELECTクエリを100回
手始めに書き込んだ100件のデータを1件ずつ100回読み込んでみます。
select value, created from bench where id = ?;
これをforループで100回まわします。
結果: 153 msec
Cursorから読み出す処理も含めての時間です。
100件取得するSELECTクエリを1回
1件を100回SELECTするのではなく、100件を1回SELECTしてみました。
select value, created from bench limit 100;
今度はクエリを発行するのは1回きりです。cursorからの取得はループさせます。
結果: 50 msec
当たり前っぽいですが、短くなりました。たくさんのデータを取得する時はまとめて取得してクエリの発行回数を減らした方がお得です。
トランザクションなしのINSERTクエリ100件
次のような単純なINSERT文を100回叩きました。
insert into bench (id, value, created) values (?, 'abcdefghijklmnop', datetime('now'));
この時、トランザクションを有効にせずに実行しました。
結果: 6,349 msec
驚くべき遅さ!これはSQLiteがレコード1件ごとにファイルへの書き込みを実行するためで、100回のディスク書き込みが発生すると理解すれば、納得なのかなーという数字です。フラッシュメモリの性能次第なところもあると思いますが。
トランザクションありのINSERTクエリ100件
実際これはみなさんひっかかる罠のようで、「Android SQLite 遅い」とかで検索したら色々出てきました。
SQLiteでINSERTが激しく遅い件
この解決策はトランザクションを有効にすることで、ようするにディスクへの書き込みをトランザクションの終了時1回にまとめるということです。先ほどと同一のクエリです。
insert into bench (id, value, created) values (?, 'abcdefghijklmnop', datetime('now'));
この時、トランザクションを有効にして実行しました。
結果: 105 msec
先ほどの約60倍の速さです。これなら実用に耐えるスピードです。
REPLACEクエリ100件
INSERTで衝突するレコードがあった場合にDELETEもしてくれる便利クエリREPLACEだとどうでしょう。
replace into bench (id, value, created) values (?, 'abcdefghijklmnop', datetime('now'));
これも100回ループさせます。この時、事前に全てのidのレコードはセットされていて、100件回のDELETEがかかるような状態で測定しました。
結果: 104 msec
INSERTのみの時と変わらないですね。
SELECTした後にINSERTするのを100回
ちょっと意味分からないかもしれないですが、データベースレコードを上から順次書き換えていきたいなどの場合を想定しています。1件ずつSELECTして、それを何かしら書き換えたりした上で、INSERTなりREPLACEなりをするというイメージです。
結果: 248 msec
結果としては、100回SELECTして、100回INSERTしたのの合計と同じでした。100回SELECTクエリを発行する代わりに、100件取得するSELECTクエリを発行すると100ms早くなることがわかっているので、全てまとめてSELECTして処理してから、全てまとめてINSERTすればこれも100ms早くなると思われます。
まとめ
大雑把に、SELECTは1回100msくらい。INSERTも1回100msくらい。INSERTする時はトランザクションを無効にしているとそれぞれに100msずつかかるから注意。REPLACEもINSERTと変わらない。トランザクション中にSELECTしてもINSERTには影響しない。
・・・とこんな感じでした。
コメント