Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Room 時代にも役に立つかも知れない SQLite の Tips / Tips for SQLite which may be useful in Room-era

Sansan
December 17, 2021

Room 時代にも役に立つかも知れない SQLite の Tips / Tips for SQLite which may be useful in Room-era

■イベント
【YUMEMI x Sansan Tech Meetup】Android開発におけるチャレンジを語る
https://sansan.connpass.com/event/229780/

■登壇概要

タイトル:Room 時代にも役に立つかも知れない SQLite の Tips

登壇者: 技術本部 Bill One Engineeringグループ 

辰濱 健一

▼Sansan Engineering
https://jp.corp-sansan.com/engineering/

Sansan

December 17, 2021
Tweet

More Decks by Sansan

Other Decks in Technology

Transcript

  1. Room 時代にも役に立つかも知れない SQLite の Tips
    【 Y U M E M I x S a n s a n T e c h M e e t u p 】
    A n d r o i d 開 発 に お け る チ ャ レ ン ジ を 語 る
    Sansan株式会社 技術本部 Bill One Engineeringグループ
    エンジニア
    辰濱 健一

    View full-size slide

  2. 2014年にSansanに入社し、徳島県にある「Sansan 神山ラボ」にて勤務。
    Sansan、Eight のアプリ開発を担当し、インドにも赴いて国際化対応を担当
    した。
    グローバル戦略統括部を経て、現在はBill Oneの国際化対応を担当。エンジニ
    アコミュニティの運営も行っている。
    辰濱 健一
    Sansan株式会社 技術本部 Bill One Engineeringグループ
    エンジニア

    View full-size slide

  3. Sansan神山ラボ

    View full-size slide

  4. Agenda
    - ANR について
    - SQLite の詳細について
    - クエリのパフォーマンス計測
    - パフォーマンス改善
    - Room での tips
    本セッションでは SQLite が原因で ANR が出ていた問題を解決した話をします。
    (Room が出ていなかったときの話です)
    近年は Room が補完してくれている部分もありますが、Room のコアとなる
    SQLite の事も知っておくと役に立つかも知れません。

    View full-size slide

  5. ANR について

    View full-size slide

  6. • ANR とは、Application Not Responding の略
    • UI スレッドが長時間(5秒以上)ブロックされると発生
    • ANR が発生すると、ユーザーはアプリを強制終了できる
    ANR について

    View full-size slide

  7. ANR について
    ANR を発生させないためには
    • 通信処理やファイルの読み書きはメインスレッド
    で行わない
    • 重たい計算処理もメインスレッドで行わない

    View full-size slide

  8. ANR の検出
    • Play Console の Android Vitals
    • Firebase Crashlytics (new! 2021/秋〜)
    ANR について

    View full-size slide

  9. ANR のスタックトレースからは、どの UI スレッドが待たされたかが分かるが、
    どの処理によって待たされているのかは分からないことがある
    → CPU プロファイラを使ってどの処理が遅いか特定
    【参考 / CPU Profiler を使用して CPU アクティビティを検査する】
    https://developer.android.com/studio/profile/cpu-profiler
    SQLite まわりがあやしかったので、より具体的にどのクエリが遅いか特定
    → SQLite をきちんと理解する必要があった
    ANR について

    View full-size slide

  10. SQLite の詳細について

    View full-size slide

  11. • オープンソースで軽量なリレーショナルデータベースライブラリ
    • マルチプラットフォーム
    • SQL が使える
    • データは一つのファイルに格納される
    • セキュリティ機能は搭載されていない
    【参考 / What Is SQLite?】
    https://sqlite.org/index.html
    • Android OS に標準で搭載されている
    • 現在では Room が SQLite の抽象化レイヤーとして提供されているので、
    Room を利用することが強く推奨されている
    SQLite について

    View full-size slide

  12. Android では Room の利用が強く推奨されている
    出典:SQLite を使用してデータを保存する / Android デベロッパー
    https://developer.android.com/training/data-storage/sqlite

    View full-size slide

  13. • マルチスレッド対応をしている
    【参考 / Using SQLite In Multi-Threaded Applications】
    https://www.sqlite.org/threadsafe.html
    • ロックはファイル単位
    • テーブルや列単位のロックは存在しない
    • スレッドセーフだが、ロックの単位が大きいので待ち時間が多くなる
    → 1トランザクションごとにファイルのロックが発生している
    【参考 / Appropriate Uses For SQLite】
    https://www.sqlite.org/whentouse.html
    SQLite の詳細について

    View full-size slide

  14. • ジャーナルモード
    ファイルへの更新方法を指定できる
    (DELETE | TRUNCATE | PERSIST | MEMORY | WAL | OFF)
    【参考 / Pragma statements supported by SQLite】
    https://www.sqlite.org/pragma.html#pragma_journal_mode
    • WAL モード:書き込みの反映は早い(が、読み込みは遅い)
    • SQLIte 3.7.0 (2010-07-21) or later でしか使えない
    • MEMORY モード:高速(だが揮発する)
    • 上限に注意。テストには向いてそう
    SQLite の詳細について

    View full-size slide

  15. • Android OS のバージョンと SQLite バージョン
    • SQLite は Android OS にバンドルされている
    • Realm のように、アプリ側でバージョンを指定できない
    • 前述の WAL モードも API 11 からしか使えない
    • Room はこのあたりのバージョン差異を吸収している
    SQLite の詳細について
    出典:android.database.sqlite / Android デベロッパー
    https://developer.android.com/reference/android/database/sqlite/package-summary.html

    View full-size slide

  16. • Android で SQLite を使うコードの例
    SQLite の詳細について
    トランザクションの開始
    トランザクションの終了

    View full-size slide

  17. • トランザクションモード
    • Exclusive モード(default)
    • 他の全てのトランザクションをロックする
    • データの一貫性は向上するが、パフォーマンスが出ない
    • beginTransaction ではこのモードになる
    • Immediate モード
    • 他のトランザクション書き込みはロックするが、読み込みはできる
    • トランザクションの並列度が増し、パフォーマンスが向上する
    • Phantom Read(更新される前の値の読み込み)が発生するリスクを伴う
    • beginTransactionNonExclusive という API で呼び出し可能
    SQLite の詳細について

    View full-size slide

  18. • TransactionListener
    • トランザクションのコールバック
    を受け取ることができる
    • onBegin()
    • onCommit()
    • onRollback()
    • beginTransactionWithListener で
    Listener を渡して呼び出す
    • パフォーマンス測定に活用した
    SQLite の詳細について

    View full-size slide

  19. クエリのパフォーマンス計測

    View full-size slide

  20. 前述の通り、SQLite ではファイル単
    位のロックになってしまう。
    実処理を短時間で行えるクエリであっ
    たとしても、直前にスロークエリが実
    行されていたら、処理完了まではかな
    りの時間になってしまう。
    → 次の2カ所の時間が欲しい
    • beginTransaction() → onBegin()
    • onBegin() -> endTransaction()
    クエリのパフォーマンス計測
    これにより、「UX から推測される遅い処理」と「実処理の時間」を明確にする

    View full-size slide

  21. そこで、SQLiteTransactionListener の出番
    クエリのパフォーマンス計測 db.beginTransaction()
    (transaction lock...)
    listener.onBegin()
    (execute ...)
    db.endTransaction()
    listener.onCommit()
    or
    listener.onRollbaack()

    View full-size slide

  22. クエリのパフォーマンス計測
    独自のプロファイラを作った
    db.beginTransaction()
    (transaction lock...)
    listener.onBegin()
    (execute ...)
    db.endTransaction()
    listener.onCommit()
    or
    listener.onRollbaack()
    1
    2

    View full-size slide

  23. クエリのパフォーマンス計測
    独自のプロファイラを実装に仕込んでいく(地道に…)

    View full-size slide

  24. クエリのパフォーマンス計測
    アプリを動かしてみたログ
    Main thread で DB を触っている!!
    ANR のトリガーになるので要改修
    • Logcat に出力しているので、DB アクセスがリアルタイムで分かる
    • どの操作をしたときに、どのクエリが実行されているか把握しやすい

    View full-size slide

  25. クエリのパフォーマンス計測
    アプリを動かしてみたログ
    db.beginTransaction()
    (transaction lock...)
    listener.onBegin()
    (execute ...)
    db.endTransaction()
    listener.onCommit()
    or
    listener.onRollbaack()
    1
    2
    重たい処理!!
    待ちが発生
    → 実処理が短くても、長時間処理が返ってこないクエリが多々存在する

    View full-size slide

  26. 他にも、
    • SQL の実行計画を分析(EXPLAIN)で実行計画を見る
    • .timer on … .timer off で実行時間を測る
    という手段も使いました。
    DB Browser for SQLiteも便利
    クエリのパフォーマンス計測
    クエリ
    クエリ結果
    クエリの実行情報

    View full-size slide

  27. パフォーマンス改善

    View full-size slide

  28. • メインスレッドでの呼び出し
    • Transaction 内で不必要な処理をしていた
    • 複数書き込みで都度 Transaction を張っていた
    • 不必要な再読込の抑制
    • FirstScroll のための全件読み込み
    ボトルネックのパターン

    View full-size slide

  29. • メインスレッド以外に切り替える方法
    • RxJava であれば、observeOn
    • Coroutine であれば、withContext
    • Room だとデフォルトではメインスレッドで呼び出すとクラッシュするの
    ですぐ分かる(安心)
    メインスレッドでの呼び出しへの対応

    View full-size slide

  30. Transaction 内で不必要な処理をしていた
    加工処理などは Transaction 外に移動

    View full-size slide

  31. 複数書き込みで都度 Transaction を張っていた
    一つのオブジェクトを保存する API を何度も呼ばずに
    List で受け取れる API も提供する
    他にも、複数のテーブルをまとめて一つの Transaction で保存するのも有効

    View full-size slide

  32. • Activity#onResume で DB にアクセスしていて、
    Activity に返ってくるたびに DB にアクセス
    → ViewModel などの中間層でキャッシュして、キャッシュ値を読み込む
    (キャッシュの更新タイミングには気をつけないといけない)
    不必要な再読込の抑制

    View full-size slide

  33. • View に出していないデータの更新による再読み込み
    • バックグランド処理がトリガーになっていた
    • 対応
    • テーブルを一覧と詳細で分割
    • 一覧更新を DB からの変更通知ではなく、
    アプリ内イベントでトリガーに更新するようにした
    不必要な再読込の抑制
    一覧に出している
    プロパティ
    変更されても
    一覧は更新不要

    View full-size slide

  34. • FirstScroll のための全件読み込み
    • 全件読み込まないと、FirstScroll のための
    情報を作れない
    • ページングと相性が悪く、全件読み込むしかない…
    • 対応
    • 全プロパティではなく、FirstScroll 構築に必要な
    サブセット(id と 日付)だけ一旦読み込む
    • 他の情報は表示時に遅延読み込み
    不必要な再読込の抑制

    View full-size slide

  35. サブセットだけの読み込みの例
    不必要な再読込の抑制

    View full-size slide

  36. • id と FirstScroll に必要な情
    報だけを全件取得する
    • 表示領域の id のみ、初回表
    示時に遅延読み込み
    • スクロールのパフォーマン
    スへの影響は軽微&一覧が
    数万件ある場合などは大き
    な速度改善になった
    • Realm のプロパティの遅延
    評価みたいですね
    全件読み込みの高速化&遅延読み込み

    View full-size slide

  37. これらの改善を行って、
    20倍の高速化になりました!

    View full-size slide

  38. Room での tips

    View full-size slide

  39. ビルド時に生成されるクラスで SQLite を叩いている
    生成されたコードを見ると
    何をやっているのかよくわかる

    View full-size slide

  40. • ある程度カスタマイズができる(が、通常そんなに触らなくても良さそう)
    • setQueryCallback(…)
    • いつ、どのクエリが実行されたのかを把握するには便利
    • CRUD のクエリだけなく、BEGIN TRANSACTION なども出る
    • setJournalMode(…)
    • デフォルトで最適なモードがあたっている(ので、気にすることはないはず)
    【参考 / RoomDatabase.Builder】
    https://developer.android.com/reference/androidx/room/RoomDatabase.Builder
    Roomdatabase.Builder

    View full-size slide

  41. @Transaction をつけると1つのトランザクションにまとめられる
    (ファイルへの反映回数が1回にまとまるので高速化)
    複数の処理を1つのトランザクションにまとめる

    View full-size slide

  42. • Dao で SELECT する列を明示して、サブセットの型で返す
    テーブルのサブセットだけ取得したいとき
    出典:YouTube / What’s new in Room (Android Dev Summit '19)
    https://youtu.be/_aJsh6P00c0?t=784

    View full-size slide

  43. • Repository 生成時に SupportSQLiteOpenHelper も渡す
    • Repository 内で SupportSQLiteOpenHelper#getWritableDatabase などを使う
    Dao で SQLite を直で触りたい場合(あるのか?)

    View full-size slide

  44. • Room は Flowable や LiveData で DB の変更通知を受け取れるが、
    テーブルが更新されればとにかく通知される仕組みになっている
    • 関心のないオブジェクトや列の変更でも emit されてしまい、意図しない
    再読み込み(さらなる DB のアクセス)を誘発してしまう
    • InvalidationTracker や InvalidationTracker.Oberver の実装を見ればよくわかる
    • 対策
    • Dao の Flowable に distinctUntilChanged() をかまして使う
    • LiveData の場合は MediatorLiveData で distinct して流す
    【参考 / Avoid false positive notifications for observable queries】
    https://medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1#5e38
    Room の変更通知について

    View full-size slide

  45. まとめ
    - Room は SQLite のラッパーライブラリ
    - SQLite の知見があると Room の挙動をよく理解できる
    - Room の変更通知を過信すると、意図しない emit が含まれることがある
    - これからの Room の進化に期待!
    - 暗号化の提供…まだかな?

    View full-size slide