Edited at
RealmDay 4

Realm Java 検索クエリの速度を活かすための注意点

More than 3 years have passed since last update.


はじめに

この記事はRealm Advent Calenderの12/4分です。

今年はAndroidアプリの開発でRealmにたくさんお世話になったので、参加してみました。

Advent Calenderに参加するのが初めてなので、軽い内容になってしまいますが、お手柔らかにお願いします。

今回はRealmをAndroidアプリで使っていて感じた、Realmの高速の検索クエリを生かした使い方や注意点について書きます。

Realmを使うにあたっては常識的な話かもしれませんが、新しく採用するに当たって参考になればと思います。


Realmの遅延ロード

Realmのクエリは、遅延ロードになっていて、検索した範囲内での単純な情報はとても早く取得できます。

しかし、だからと言って、全ての要素にすぐにアクセスできるようになったわけではなく、それぞれの要素にアクセスする時もすこし時間がかかります。

なのでRecyclerViewなどで表示する時は、RealmでクエリしたオブジェクトはそのままAdapterに渡してしまう、ListViewの時のCursorAdapterのような実装がRealmの速度を最も活かせる使い方でしょう。

Thread間でRealmResultsのようなクエリ後のオブジェクトを引き渡せないことなどは、理解していましたが、この点を留意しておらず、思うようなパフォーマンスが得られないことがありました。


SQLiteとのクエリ速度比較

散々行われていることですが、改めて実際にAndroid標準のSQLiteDatabaseのクエリと速度を比較してみました。

(他のライブラリとのもっと全般的な速度比較は他の人に任せます)

Cursorの取得に当たる、RealmResultsの取得を比較すると、

// Realm

RealmResults<User> results = realm.where(User.class).findAll();
// SQLite
sqliteDatabase.query(TABLE, null, null, null, null, null, null);

よく比較されるのはこの速度だと思います。

5回測ってみました。


10万行取得

Realm 77ms, 54, 37, 56, 36

SQLite 1102ms, 1015, 1040, 1054, 1071


と、Realmがとてつもなく早いことがわかります。

この取得をUIスレッドで行ってもほとんどブロックされないので問題ない、と言われても正しそうです。

しかし、遅延ロードを生かさずに、全ての要素にアクセスするとだいぶ差は縮まります。

// Realm

RealmResults<User> results = realm.where(User.class).findAll();
for (User user : results) {
// クエリだけでなく、わざわざ全ての列要素にもアクセスする
user.getAge();
user.getName();
user.getEmail();
}

// SQLite
Cursor cursor = sqliteDatabase.query(TABLE, null, null, null, null, null, null);
result = cursor.getCount();
cursor.moveToFirst();
int indexAge = cursor.getColumnIndex(UserColumns.AGE);
int indexName = cursor.getColumnIndex(UserColumns.NAME);
int indexEmail = cursor.getColumnIndex(UserColumns.EMAIL);
for (int i = 0; i < result; i++) {
// クエリだけでなく、わざわざ全ての列要素にもアクセスする
cursor.getInt(indexAge);
cursor.getString(indexName);
cursor.getString(indexEmail);
cursor.moveToNext();
}

上のような実行すると、(SQLiteの方は無駄が多そうですが)


10万行、それぞれの列にもアクセス

Realm 6725ms, 6526, 6000, 7108, 7016

SQLite 9997ms, 9913, 10084, 10332, 9632


とRealmも秒オーダーに成ってしまい、確かに早いですが、それほど差がなくなってしまいました。

これだとUIスレッドで全て実行するには、非効率なことがわかりました。


Adapterの実装方法

この点を留意して、Adapterの実装を考えると、ListViewの頃のCursorAdapterのようなクエリ後の情報をそのまま渡すのがいい、とわかります。

// クエリを何も処理せずそのまま渡します。

mRecyclerView.setAdapter(new MyItemRecyclerViewAdapter(mRealm.where(User.class).findAll()));

public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder> {

// RealmObjectをそのまま参照として持ちます
private final List<User> mValues;

public MyRecyclerViewAdapter(List<User> items) {
mValues = items;
}
}

実際にCursorAdapterのような動きをさせる場合は、AutoRequeryを実装するかなどを考える必要があると思います。

細かい実装例は、公式ブログのこちらも若干特殊な気がしますが、参考になると思います。

RealmRecyclerViewとRealmを使ったグリッドインタフェースの構築


検索や集計が限られている

ここまでだと、使い方通りに使えば、特にハマるものでもないと思いますが、自分の場合は、RealmのクエリでGroupbyなどを実行したかったため、はまってしまいました。

現状のRealm-javaだと、group byのような機能や、Tableのモデルに定義されていないような計算結果に対するクエリは実行できないので、insert時に検索しやすいようなフィールドをつけておくか、クエリ結果を処理したり複数回クエリを実行する必要があったりします。

Realm.io Query with GroupBy

// このクエリが発行できない。

// SELECT * FROM Account WHERE (date = MONTH(date))

// 月ごとにまとめる場合
accounts = realm.where(Account.class).between("date", monthStart, monthEnd).findAll();
accounts = realm.where(Account.class).findAllSorted("date")
Iterator<Account> it = accounts.iterator();
int previousMonth = it.next().getDate().getMonth();
while (it.hasNext) {
int month = it.next().getDate().getMonth();
if (month != previousMonth) {
// month changed
}
previousMonth = month;
}

実際に速度を比較してみました。

// 年齢ごとにまとめる場合

// このクエリも発行できない。
// SELECT * FROM User GROUP BY age
// Realmの場合
RealmResults<User> results1 = realm.where(User.class).findAll();
List<Integer> list1 = new ArrayList<>();
for (User o : results1) {
int age = o.getAge();
if (!list1.contains(age)) {
list1.add(age);
}
}
// SQLiteの場合
cursor = sqliteDatabase.rawQuery(cursor("select * from users group by age", null);

Realmの場合は余計なlistなど使っているので、もう少しへらせそうですが、


10万行

Realm 5304, 5338, 5403, 4592, 4494

SQLite 994ms, 858, 791, 661, 850


と、Realmの方が遅くなってしまいます。


まとめ

Realmはとても早くて便利ですが、全ての検索を実行するとさすがに時間がかかります。

また、集計系の処理はまだJava版には実装されていないようなので、実装を待つ必要がありそうです。

(実装されていたらコメントでご指摘ください)

モバイルアプリのDBの用途は、


  • UI表示のためのキャッシュ

  • イベントのログの保存

などがあると思いますが、特にRealmはUI表示の問題を解決することにフォーカスしているように思います。

検索クエリも簡単で、入れたオブジェクトをそのままの形で参照してUIに反映していく使い方を主な用途として設計されているようです。

自分はたまたま、イベントログを集めて複数の集計方法でUIを切り替えたりしましたが、このような用途ではまだRealmの速度を思うように活かすことができませんでした。

Realmをこのような場合につかうときは、UI表示される状態のテーブルなどを作ってリレーションで繋げたり、保存するときに集計したりといったことをしたほうがいい場合もあるでしょう。


今回使ったコード

今回、RealmとSQLiteにinsertしたりqueryの速度を比較したコードを挙げておきます。

RecyclerViewにそれぞれ入れてみた場合のコードも入れてあります。

wasnot/RealmAndroidTest

あと、FPSを図るためにwasabeef_jpさんのTaktを使ってみました。。