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

[INSERT編](http://qiita.com/alzybaad/items/b350b49740a3d25297c1)、[DELETE編](http://qiita.com/alzybaad/items/9356b5a651603a548278)に続き、SELECT編。

# 前提
## 環境
Nexus5, Android 4.4.4

## テーブル
こんなかんじ。

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

## INSERT済みのデータ
以下のようなデータ10000件。

|id|name|address|count|flag|
|:--|:--|:--|:--|:--|
|id0|name0|NULL|0|0|
|id1|name1|NULL|1|0|
|id2|name2|NULL|2|0|
|...|...|...|...|...|
|id9999|name9999|NULL|9999|0|

# `SQLiteDatebase#query()` vs `SQLiteDatabase#rawQuery`
`SQLiteStatement`はSELECTに対応していないため、この2つのメソッドの比較。

```java:SQLiteDatabase#query()を使った処理
Cursor cursor = db.query("table_name", null, "id=?", new String[]{"id5000"}, null, null, null, null);
try {
    if (cursor.moveToNext()) {
        Hoge hoge = new Hoge();
        hoge.id = cursor.getString(cursor.getColumnIndex("id"));
        hoge.name = cursor.getString(cursor.getColumnIndex("name"));
        hoge.address = cursor.getString(cursor.getColumnIndex("address"));
        hoge.count = cursor.getInt(cursor.getColumnIndex("count"));
        hoge.flag = cursor.getInt(cursor.getColumnIndex("flag"));
    }
} finally {
    cursor.close();
}
```

```java:SQLiteDatabase#rawQuery()を使った処理
Cursor cursor = db.rawQuery("SELECT * FROM table_name WHERE id=?", new String[]{"id5000"});
try {
    if (cursor.moveToNext()) {
        Hoge hoge = new Hoge();
        hoge.id = cursor.getString(cursor.getColumnIndex("id"));
        hoge.name = cursor.getString(cursor.getColumnIndex("name"));
        hoge.address = cursor.getString(cursor.getColumnIndex("address"));
        hoge.count = cursor.getInt(cursor.getColumnIndex("count"));
        hoge.flag = cursor.getInt(cursor.getColumnIndex("flag"));
    }
} finally {
    cursor.close();
}
```

## 結果
結果は以下の通り、rawQueryの方がわずかに速い。

|メソッド|処理時間[msec]|
|:--|:--|
|`SQLiteDatabase#query`|0.76|
|`SQLiteDatabase#rawQuery`|0.68|

これは`SQLiteDatabase#query`内部では以下のようにrawQueryを呼び出しているため。

```java:SQLiteDatabase#query()
public Cursor queryWithFactory(CursorFactory cursorFactory,
        boolean distinct, String table, String[] columns,
        String selection, String[] selectionArgs, String groupBy,
        String having, String orderBy, String limit, CancellationSignal cancellationSignal) {
    acquireReference();
    try {
        String sql = SQLiteQueryBuilder.buildQueryString(
                distinct, table, columns, selection, groupBy, having, orderBy, limit);

        return rawQueryWithFactory(cursorFactory, sql, selectionArgs,
                findEditTable(table), cancellationSignal);
    } finally {
        releaseReference();
    }
}
```

ただし、これは1行のみ取得しているケースであり、複数行の場合は相対的に差は小さくなる。
...と思ったが、100行取得するケースの測定結果は以下の通りであり、そんなことはないかも。

|メソッド|処理時間[msec]|
|:--|:--|
|`SQLiteDatabase#query`|7.69|
|`SQLiteDatabase#rawQuery`|6.31|

# 1行 vs N行
`ListView`などでDBからデータを取得して表示するケースは多いと思う。
このとき、データやレイアウトによっては`Adapter#getView`が呼ばれる度に毎回1行取得するとスクロール中にカクつくことがある。
そこで、1度に何行取得するのが良いのかを考える。

カラムでソート順が指定できる場合は以下のようなにLIMITとOFFSETを指定する。

```java:LIMIT/OFFSET指定
Cursor cursor = db.rawQuery("SELECT * FROM table_name ORDER BY id LIMIT ? OFFSET ?", new String[]{String.valueOf(count), String.valueOf(offset)});
try {
    List<Hoge> hoges = new ArrayList<>(cursor.getCount());
    while (cursor.moveToNext()) {
        Hoge hoge = new Hoge();
        hoge.id = cursor.getString(cursor.getColumnIndex("id"));
        hoge.name = cursor.getString(cursor.getColumnIndex("name"));
        hoge.address = cursor.getString(cursor.getColumnIndex("address"));
        hoge.count = cursor.getInt(cursor.getColumnIndex("count"));
        hoge.flag = cursor.getInt(cursor.getColumnIndex("flag"));
        hoges.add(hoge);
    }
} finally {
    cursor.close();
}
```

カラムでソートできない場合は別途並び順を保持しておいて、以下のようにINを使用する。
この場合は取得したデータの並び順は不定なため、最後にソートする必要がある。

```java:IN指定
StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM table_name WHERE id IN (");
for (int i = 0; i < ids.length; ++i) {
    sql.append("?");
    if (i != ids.length - 1) {
        sql.append(",");
    }
}
sql.append(")");

Cursor cursor = db.rawQuery(sql.toString() ids);
try {
    List<Hoge> hoges = new ArrayList<>(cursor.getCount());
    while (cursor.moveToNext()) {
        Hoge hoge = new Hoge();
        hoge.id = cursor.getString(cursor.getColumnIndex("id"));
        hoge.name = cursor.getString(cursor.getColumnIndex("name"));
        hoge.address = cursor.getString(cursor.getColumnIndex("address"));
        hoge.count = cursor.getInt(cursor.getColumnIndex("count"));
        hoge.flag = cursor.getInt(cursor.getColumnIndex("flag"));
        hoges.add(hoge);
    }

    Collections.sort(hoges, new Comparator<Hoge>() {
        @Override
        public int compare(String lhs, String rhs) {
            return ids.indexOf(lhs) - ids.indexOf(rhs);
        }
    });
} finally {
    cursor.close();
}
```

## 結果
LIMIT/OFFSETを指定した場合のみであるが、結果は以下の通り。
行数と処理時間が比例関係になく、100行の場合が最も効率が良い。ただし、実際、何行にするかは絶対的な処理時間とメモリ使用量等の考慮が必要になる。

|メソッド|1行[msec]|5行[msec]|10行[msec]|25行[msec]|50行[msec]|100行[msec]|
|:--|:--|:--|:--|:--|:--|:--|
|`SQLiteDatabase#rawQuery`|0.70|0.88|1.18|2.18|3.09|5.58|