【Firestore】記録系アプリのパフォーマンスをUX的にもコスト的にも改善した話

2024/12/08に公開

Firestoreはドキュメントや投稿記事も充実しており、個人開発でも非常に導入しやすいですが、ドキュメントDBのため使い方によっては読み取り回数がいとも簡単に増えてしまうため、データベース設計は非常に重要になってきます。
今回はデータベース設計初心者の私が実際にやった失敗・問題分析・対処についてまとめました。

事の発端

ライブの思い出を記録するアプリを個人で開発しています。
https://apps.apple.com/jp/app/ticketlist-チケットリスト-ライブ参戦記録/id6470704235

ユーザーさんからトップページのローディング時間が遅いというコメントが届きました。

仕様

トップページでは以下のように、ユーザーが追加したチケットが日付順でソートされて表示されます。

データベースではTicketコレクションとArtistコレクションが分かれており、Ticketには1~NのArtistが紐付いています。
1つのArtistが複数のTicketに登場することもあるため、TicketとArtistでコレクションを分けています。

問題

①データベース設計が適切ではない(正規化していた)

TicketとArtistは多対多の関係にありますが、Firestoreではjoinは使えないためTicketに紐づくArtistを1つずつ読み取る必要があります。
そのため、1つのチケットを読み取るためにN(Artist数)+1(Ticket)の読み取りが発生します。

この方法はコスト面においても適切でありません。Firestoreは読み取り回数が無料の割り当てを超えると課金が発生するため、頻繁に発生する処理の読み取りはできるだけ少ない回数で済ませたいです。

Cloud Firestore の課金について

// 【参考】複数ドキュメントの一括読み取り
// documentIds : getしたいドキュメントのidのリスト
db
.collection("Artist")
.where(FieldPath.documentId, whereIn: documentIds).get();

②データ取得のたびにコレクション内を全ソートしている

データの並びは追加・編集・削除をしない限り変更されないため、取得のたびにソートする必要はありません。
一覧取得はアプリのトップページで必ず呼ばれるため、高頻度にも関わらず、非常にコストの高い操作になっています。

③閲覧されないデータまで取得している

トップページにはチケットの概要が表示され、チケットを選択すると詳細が表示されます。
複数あるチケットのうち、実際に詳細を閲覧するのは一部ですが、毎回全てのチケットの詳細データまでを読み取って保持しています。これは過剰な読み取りと言えるでしょう。


総合して、トップページに情報を出す際に行う処理が多いことと、その情報量が多いことが遅い原因になっていました。
アプリの特性上、ライブ会場や帰り道の電車など回線状況が不安定な場所で利用されることが想定されるため、ネットワークをまたぐ通信の数やその情報量の大きさは改善すべき点です。

やったこと

トップページに情報を出すための処理と情報量を最低限にするために、「トップページ用の情報」として非正規化を行いました。

  • Artistデータの埋め込み
  • ソート済みで保存
  • 取得データをトップページに必要な情報に限定する

①データベース設計の変更

Artistのデータを埋め込む非正規化を行いました。
これにより冗長性は増しますが、Ticketに紐づくArtistを逐一探す必要がなくなるため、読み取り回数が減り、高速化が期待できます。

また、全チケットをarrayの形で保存するようにしました。
これによりトップページに必要なデータを1回で読み取ることができます。

※ドキュメントサイズには上限があるため、注意が必要(今回は超えるケースはないと判断)

Firestoreのドキュメント最大サイズ:1 MiB(1,048,576 バイト)
使用量と上限

こちらの記事を参考にしました。
https://zenn.dev/kokekokko/articles/8973a15c4ffb1f

②ソートした状態で保存する

先述の通り、チケットの並びは追加・編集・削除をしない限り変更されないため、ソートした状態のデータを保存しておきます。

③必要最低限のデータだけを保存

情報量が概要<詳細のため、一覧表示用に取得するのは表示に必要な項目のみに省略することでデータ量を減らすことができます。
今回は概要≒詳細のためそこまで大きな効果はないかもしれないですが、概要<<<詳細であるほど恩恵を受けることができます。

④要件を見直し、機能を削減

ソートは【イベント開催日時】と【チケット追加日時】の2種類を用意していましたが、アプリとしてマストではない【チケット追加日時】のソート機能を削除しました。
なんでも削除すればいいという意味ではありませんが、機能を削除することで抱えていた問題がそもそも無くなることもあるのです。

⑤(おまけ)待機状態のUIを改良

サーキュラーインジケーター(circular indicator)からスケルトンスクリーン(skeleton screen)に変更しました。

定量的な評価ができるものではありませんが、

  • 画面のチープさを軽減させる
  • すでに一部の作業が進んでいるという印象を与え、待機時間が短く感じる

といった効果を与えます。


circular indicator

skeleton screen

結果

  • fetch所要時間(5個のチケット取得)※ある時点での計測
    • 改善前:1.45秒
    • 改善後:0.10秒
  • 読み取り回数(5個のチケット取得)
    • 改善前:最低10回
    • 改善後:1回

読み取りにかかる時間はもちろん、利用が増えるに従って爆発的に増加していたであろう読み取り回数を大幅に削減することに成功しました✨

今後の展望

今回は初めてのアプリ開発ということもあり、導入しやすいFirebaseを選択しましたが、データの性質から考えて、SupabaseなどのRDBを使うことを検討してもいいかもしれません。

Discussion