はじめに
FirestoreのデータをBigQueryに連携し、分析・集計・ダッシュボード作成に利用する機会がありました。
構築自体はFirebase Extensionを使うことで比較的簡単に始められますが、実際に運用を考えると以下のような点で少し悩みました。
- Firestoreのデータ構造をBigQueryでどう扱いやすくするか
- changelog形式のテーブルから最新状態をどう取り出すか
- DELETEされたデータをBigQuery側でどう扱うか
- TTLでFirestore側のデータを削除した場合、BigQuery側の分析データも消すべきか
- 生データ、正規化データ、日次集計データをどう分けるか
この記事では、初めてFirestore → BigQuery連携を構築した際の備忘録として、考え方とView設計の流れをまとめます。
前提
Firestore → BigQuery連携には、Firebase Extensionsの Stream Firestore to BigQuery を利用しました。
このExtensionは、指定したFirestoreコレクションの変更をBigQueryへリアルタイム・増分で連携できます。
BigQuery側には、Firestoreドキュメントの変更履歴を保持するRawテーブルや、現在の状態を扱うためのViewが作成されます。
BigQuery側で整理した構造
FirestoreのデータをBigQueryへ連携したあと、分析しやすくするために、今回はざっくり次のような構造で整理しました。
Firestore Collection
↓
BigQuery Raw Changelog Table
↓
Raw Latest View
↓
Normalized View
↓
Aggregation / Report View
Raw Changelog Table
Firestore上のドキュメント変更履歴が蓄積されるテーブルです。
主に以下のようなメタ情報を持ちます。
document_name
document_id
timestamp
event_id
operation
data
old_data
operation には CREATE、UPDATE、DELETE、IMPORT などの変更種別が入ります。
data には現在のドキュメント内容がJSON文字列として入り、old_data には変更前の内容が入ります。
Raw Latest View
Raw Changelog Tableから、ドキュメントごとの最新状態を取り出すViewです。
Firestoreの変更履歴は追記形式で蓄積されるため、そのまま参照すると1ドキュメントに対して複数行が存在します。
そのため、document_name 単位で最新行を抽出するViewを用意します。
Normalized View
Raw Latest Viewの data はJSON文字列のため、そのままだとSQLで扱いづらいです。
そこで、JSON_VALUE や CAST / SAFE_CAST を使って、BigQuery上で扱いやすいカラムに展開します。
Aggregation / Report View
最後に、用途に応じて日次・ユーザー別・カテゴリ別などの集計Viewを作ります。
ここはアプリケーションや分析要件に依存するため、最初から作り込みすぎず、まずはRaw Latest ViewとNormalized Viewを安定させるのがよいと思いました。
設計時に悩んだこと
FirestoreのTTL削除をBigQuery側でも反映するべきか
一番悩んだのは、Firestore側でTTL削除されたデータをBigQuery側でも削除扱いにするかどうかです。
Firestore側では、保存期間やストレージコストの都合でTTLを設定することがあります。
ただし、TTLによる削除が「業務上そのデータが無効になった」という意味ではなく、単にFirestore上の保存期間を過ぎたという意味であれば、BigQuery側まで削除扱いにしてしまうと分析データが失われます。
今回の方針は以下にしました。
Firestore側:
TTLで古いドキュメントを削除する
BigQuery側:
TTL削除は分析データの削除とはみなさない
最後に存在していた有効データを保持する
つまり、Firestoreは短期保存、BigQueryは分析用の長期保存という役割分担です。
DELETEログの扱い
Firestoreでドキュメントが削除されると、BigQueryのchangelogにも DELETE 操作として記録されます。
このDELETEログを最新状態として扱うと、下流のViewでデータが消えたように見えます。
一方で、TTL削除をBigQuery側では無視したい場合、最上流のLatest Viewで DELETE を除外するのがシンプルでした。
WHERE operation != 'DELETE'
この条件をRaw Latest Viewの段階で入れておくと、下流の正規化Viewや集計ViewではDELETEを意識しなくて済みます。
ただし、この方法ではTTL削除だけでなく、明示的なDELETE操作もBigQuery側では無視されます。
そのため、業務上「削除されたデータは分析対象からも除外したい」場合や、個人情報削除などの要件がある場合は注意が必要です。
TTL削除と業務上の削除を区別したい場合は、削除理由を示すフィールドを持たせる、論理削除フラグを利用する、DELETEログを監査用Viewとして別途保持するなどの設計を検討します。
最新状態の取り出し方
Firestoreのchangelogからドキュメントごとの最新状態を取り出す場合は、document_name 単位で変更履歴をまとめ、更新時刻の降順に並べます。
PARTITION BY document_name
ORDER BY timestamp DESC
そのうえで、最新行だけを取得します。
ROW_NUMBER() OVER (
PARTITION BY document_name
ORDER BY timestamp DESC
) = 1
または、FIRST_VALUE() を使って、最新行に紐づく値を取り出す方法もあります。
FIRST_VALUE(data) OVER (
PARTITION BY document_name
ORDER BY timestamp DESC
)
重要なのは、timestamp だけを見るのではなく、data、operation、event_id なども同じ最新行に紐づく値として扱うことです。
JSONデータの正規化
Raw Latest Viewの data はJSON文字列なので、そのままだとSQLで扱いづらいです。
そのため、必要な項目だけをBigQuery上のカラムとして展開します。
JSON_VALUE(data, '$.fieldA') AS field_a
数値として扱いたい項目は、SAFE_CAST で型変換します。
SAFE_CAST(JSON_VALUE(data, '$.fieldB') AS INT64) AS field_b
SAFE_CAST を使っておくと、想定外の値が入った場合でもクエリ全体が失敗しづらくなります。
Rawデータを直接集計に使うのではなく、一度Normalized Viewで型付きカラムに変換しておくと、下流のSQLをシンプルにできます。
ハマりどころ
1. DELETEをそのまま最新状態として扱うとデータが消える
DELETEログを最新状態として扱うと、下流のViewではそのドキュメントが存在しないものとして扱われます。
TTL削除をBigQuery側では分析データの削除とみなさない場合は、Raw Latest Viewの段階でDELETEを除外する設計にしました。
ただし、通常のDELETEまで無視してよいかは別問題です。
明示的な削除や個人情報削除など、BigQuery側にも削除を反映すべきケースがある場合は、TTL削除と業務上の削除を区別できる設計にしておく必要があります。
2. BigQueryの連携時刻とデータ上のイベント時刻は分ける
FirestoreからBigQueryへ連携された時刻と、データ上のイベント時刻は別物です。
たとえば、日次集計を作る場合は、以下のどの日付を基準にするかを事前に決めておく必要があります。
- BigQueryに連携された日
- Firestoreドキュメントが更新された日
- データ上のイベント発生日
ここを曖昧にすると、ダッシュボードや日次レポートの数字が想定とズレることがあります。
そのため、集計用のViewでは、日付の基準となる時刻カラムを明確にしておくことが重要です。
3. Viewの責務を分ける
最初は1つのSQLで全部やりたくなりますが、以下のように責務を分けた方が後から直しやすいです。
Raw Latest View:
changelogから最新状態を取り出す
Normalized View:
JSONを型付きカラムへ変換する
Aggregation View:
日次・月次・ユーザー別など、用途ごとに集計する
特にFirestoreのスキーマ変更がある場合、正規化Viewだけ修正すればよい構成にしておくと影響範囲を抑えられます。
今回の結論
Firestore → BigQuery連携では、ただデータを流すだけでなく、BigQuery側でどう分析可能な形に整えるかが重要でした。
特に、Firestore側でTTLを使う場合は、削除の意味を明確にしておく必要があります。
今回のように、
Firestore:
短期保存・アプリ用
BigQuery:
長期保存・分析用
という役割分担にする場合は、BigQuery側のLatest Viewで operation != 'DELETE' を指定し、TTL削除ログを除外する設計がシンプルでした。
ただし、この設計は「DELETEをBigQuery側では最新状態として扱わない」という判断です。
業務上の削除や個人情報削除など、BigQuery側でも削除扱いにすべきデータがある場合は、その要件に合わせて別途設計する必要があります。
まとめ
Firestore → BigQuery連携を初めて構築してみて、以下が重要だと感じました。
- changelogをそのまま使わず、最新状態を扱うViewを作る
- JSON文字列は正規化Viewで型付きカラムに展開する
- TTL削除と分析データ削除は分けて考える
- DELETEログの扱いは最上流で決める
- ただし、TTL削除と業務上の削除を同じ扱いにしてよいかは確認する
- 日次集計では、どの日付を基準にするかを明確にする
- Raw、正規化、集計でViewの責務を分ける
Firestore → BigQuery連携はExtensionで簡単に始められますが、運用を考えるとView設計がかなり重要でした。
同じように、FirestoreのデータをBigQueryで分析したい場合の参考になれば幸いです。