AndroidでSQLiteのパフォーマンス比較(INSERT編)

いまさらながら、AndroidのSQLiteのパフォーマンスを比較してみた。

#### 環境
Nexus5, Android 4.4.4

#### テーブル
こんなかんじ。深い意味はない。

|カラム名|型|制約|
|:--|:--|:--|
|id|TEXT|PRIMARY KEY|
|name|TEXT|NOT NULL|
|address|TEXT||
|count|INTEGER|NOT NULL|
|flag|INTEGER|NOT NULL|

#### 1. SQLiteDatabase#insert
おそらく、ほとんどの人が最初に学ぶ方法。
SQLを知らなくても書けるし、わかりやすいが…

```java
db.beginTransaction();
try {
    final int columnCount = 5;
    final ContentValues values = new ContentValues(columnCount);
    for (int i = 0; i < totalCount; ++i) {
        values.clear();
        values.put("id", "id" + i);
        values.put("name", "name" + i);
        values.put("address", (String) null);
        values.put("count", i);
        values.put("flag", 0);
        db.insert("table_name", null, values);
    }
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
```
#### 2. SQLiteDatabase#execSQL(単一行)
execSQLを使った方法。
[Android Developers](http://developer.android.com)には、SELECT/INSERT/UPDATE/DELETE以外のときに使う、との記載があるが、戻り値不要なら使える。

```java
db.beginTransaction();
try {
    for (int i = 0; i < totalCount; ++i) {
        db.execSQL("INSERT INTO table_name VALUES (?,?,?,?,?)",
            new String[] {"id" + i, "name" + i, null, String.valueOf(i), String.valueOf(0)});
    }
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
```
#### 3. SQLiteDatabase#execSQL(複数行)
ずいぶん長くなってきた。
SQLite3.7.11から使える1回のINSERTで複数行追加できるSQL文。
Android 4.1以降はSQLite3.7.11以降らしいが、それより前のバージョンだとエラーになるので注意。
下記の例では10行ずつINSERTしている。

```java
db.beginTransaction();
try {
    final int columnCount = 5;
    final int rowCount = 10;
    final String[] bindArgs = new String[columnCount * rowCount];
    final StringBuilder sql = new StringBuilder("INSERT INTO table_name VALUES (?,?,?,?,?)");
    for (int i = 0; i < rowCount - 1; ++i) {
        sql.append(", (?,?,?,?,?)");
    }
    for (int i = 0; i < totalCount; ++i) {
        final int index = columnCount * (i % rowCount);
        bindArgs[index] = "id" + i;
        bindArgs[index + 1] = "name" + i;
        bindArgs[index + 2] = null;
        bindArgs[index + 3] = String.valueOf(i);
        bindArgs[index + 4] = String.valueOf(0);
        if (i % rowCount == rowCount - 1) {
            db.execSQL(sql.toString(), bindArgs);
        }
    }
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
```
#### 4. SQLiteDatabase#execSQL(UNION)
SQLite3.7.11以前でも使えるUNIONを用いた1回のINSERTで複数行追加できるSQL文。
Android2.3.3でも使えた。

```java
db.beginTransaction();
try {
    final int columnCount = 5;
    final int rowCount = 10;
    final StringBuilder sql = new StringBuilder("INSERT INTO table_name SELECT ?,?,?,?,?");
    final String[] bindArgs = new String[columnCount * rowCount];
    for (int i = 0; i < rowCount - 1; ++i) {
        sql.append(" UNION ALL SELECT ?,?,?,?,?");
    }
    for (int i = 0; i < totalCount; ++i) {
        final int index = columnCount * (i % rowCount);
        bindArgs[index] = "id" + i;
        bindArgs[index + 1] = "name" + i;
        bindArgs[index + 2] = null;
        bindArgs[index + 3] = String.valueOf(i);
        bindArgs[index + 4] = String.valueOf(0);
        if (i % rowCount == rowCount - 1) {
            db.execSQL(sql.toString(), bindArgs);
        }
    }
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
```
#### 5. SQLiteStatement#executeInsert(単一行)
SQLiteStatementを使った方法。
closeが必要なため、try-finallyが増えるのと、型毎にbindメソッドが異なるのが面倒。

```java
db.beginTransaction();
try {
    final SQLiteStatement statement = db.compileStatement("INSERT INTO table_name VALUES (?,?,?,?,?)");
    try {
        for (int i = 0; i < totalCount; ++i) {
            statement.bindString(1, "id" + i);
            statement.bindString(2, "name" + i);
            statement.bindNull(3);
            statement.bindLong(4, i);
            statement.bindLong(5, 0);
            statement.executeInsert();
        }
    } finally {
        statement.close();
    }
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
```
#### 6. SQLiteStatement#executeInsert(複数行)
3と同じ複数行INSERTを使った方法。
1回のINSERTで追加する行数が追加する行数の約数になっていないと
2回SQLiteStatementを作成しないといけないのが、大きな欠点。

```java
db.beginTransaction();
try {
    final int columnCount = 5;
    final int rowCount = 10;
    final StringBuilder sql = new StringBuilder("INSERT INTO table_name VALUES (?,?,?,?,?)");
    for (int i = 0; i < rowCount - 1; ++i) {
        sql.append(", (?,?,?,?,?)");
    }
    final SQLiteStatement statement = db.compileStatement(sql.toString());
    try {
        for (int i = 0; i < totalCount; ++i) {
            final int index = columnCount * (i % rowCount);
            statement.bindString(index + 1, "id" + i);
            statement.bindString(index + 2, "name" + i);
            statement.bindNull(index + 3);
            statement.bindLong(index + 4, i);
            statement.bindLong(index + 5, 0);
            if (i % rowCount == rowCount - 1) {
                 statement.executeInsert();
            }
        } finally {
            statement.close();
        } 
    }
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
```
#### 7. SQLiteStatement#executeInsert(UNION)
4と同じUNIONを使った方法。
6と同じく、2回SQLiteStatementを使わないといけない可能性がある、という欠点がある。

```java
db.beginTransaction();
try {
    final int columnCount = 5;
    final int rowCount = 10;
    final StringBuilder sql = new StringBuilder("INSERT INTO table_name SELECT ?,?,?,?,?");
    for (int i = 0; i < rowCount - 1; ++i) {
        sql.append(" UNION ALL SELECT ?,?,?,?,?");
    }
    final SQLiteStatement statement = db.compileStatement(sql.toString());
    try {
        for (int i = 0; i < totalCount; ++i) {
            final int index = columnCount * (i % rowCount);
            statement.bindString(index + 1, "id" + i);
            statement.bindString(index + 2, "name" + i);
            statement.bindNull(index + 3);
            statement.bindLong(index + 4, i);
            statement.bindLong(index + 5, 0);
            if (i % rowCount == rowCount - 1) {
                statement.executeInsert();
            }
        } finally {
            statement.close();
        }
    }
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
```
#### 結果
それぞれの方法で100行、1000行、10000行追加した場合の処理時間(msec)は以下の通り。
1回のINSERTで複数行追加できる方法では10行、50行、100行で試した。

単一行については、5(executeInsert)がそれぞれ1(insert)、2(execSQL)の1.6倍、1.3倍程度と最も速い。
複数行についても、6,7(executeInsert)が最も速い。また、追加する全行数が多くなるほど、5に対しても速くなる。今回は、1回あたりの追加行数は50行が一番速いケースが多かったが、テーブルに依存すると思われる。

6,7は上にあげた欠点があるのと、SQL文が長くなりすぎると落ちるため、
基本的には5の方法を使い、1度に大量データを保存するようなアプリでパフォーマンスが求められる場合は6,7の方法を検討しようと思う。

|方法|1度に追加する行数|100行|1000行|10000行|
|:--|:--|:--|:--|:--|
|1. SQLiteDatabase#insert|1|29.61|234.2|2166.4|
|2. SQLiteDatabase#execSQL(単一行)|1|21.95|175.0|1717.9|
|3. SQLiteDatabase#execSQL(複数行)|10|14.43|93.2|876.8|
||50|13.83|75.5|823.5|
||100|15.21|88.6|799.8|
|4. SQLiteDatabase#execSQL(UNION)|10|14.65|92.1|798.1|
||50|14.34|83.1|784.1|
||100|14.61|83.0|835.1|
|5. SQLiteStatement#executeInsert(単一行)|1|17.28|126.1|1122.3|
|6. SQLiteStatement#executeInsert(複数行)|10|13.27|77.9|697.2|
||50|13.04|**54.6**|667.5|
||100|13.5|59.8|691.7|
|7. SQLiteStatement#executeInsert(UNION)|10|13.93|81.2|681.0|
||50|**13.02**|97.8|**661.7**|
||100|13.7|98.4|672.7|