インフラ

Stream Firestore to BigQuery(Firestore BigQuery Export)がパーティションテーブルを作成しない

Stream Firestore to BigQuery(旧Firestore BigQuery Export)はFirestoreの更新ログをBigQueryにストリームするFirebaseの拡張です。Firestore自身はその性質から集計や分析が非常に難しいため、上記の拡張を利用することでFirestore上のデータをBigQuery上のテーブルに流すことで集計が可能になります。

一方でFirestoreの変更のログがすべて吐き出されるため行数が多くなる・カラム型データストアであるBigQueryに対してFirestoreのデータはdataカラムという1つのカラムに入るといったコスト面での課題があります。

その課題を解決する1つの策としてBigQueryのパーティション機能があります。簡単に言えば取り込み日時(取り込み日時以外も可能・数字も可能)によってテーブルを分割し、検索時に日付を絞ることで通常はテーブルの全行を検索するところパーティションで区切った範囲のみ検索対象になり、結果的にコストを抑えることができます。
Stream Firestore to BigQueryにも作成するテーブルをパーティション分割テーブルにする機能があります。今回はこの機能についての設定の注意点と備忘録です。

状況

Stream Firestore to BigQueryには下記の4つのパーティションに関する設定値があります。

  • Time partitioning option type
    • 設定のTABLE_PARTITIONING
    • テーブルを区切る単位(時間・日・月・年)
  • Time partitioning column name
    • 設定の TIME_PARTITIONING_FIELD
    • カラムの名前
  • Time partitioning table schema
    • 設定の TIME_PARTITIONING_FIELD_TYPE
    • カラムの型。omitを指定するのが良い
  • Firestore document field name
    • 設定の TIME_PARTITIONING_FIRESTORE_FIELD
    • Firestoreの特定のフィールドを指定して、それを基準にパーティションを分割する設定

これらの設定を記載しているのですが、パーティションが設定されない状態です。

結論

パーティションが適切に設定されるためには下記の2つのどちらかの状態でなければなりません

  • TABLE_PARTITIONING のみ設定されている
    • ちなみにTIME_PARTITIONING_FIELD_TYPEomitを指定すると内部的には「指定なし」という状況になります
  • TABLE_PARTITIONINGTIME_PARTITIONING_FIELDTIME_PARTITIONING_FIELD_TYPETIME_PARTITIONING_FIRESTORE_FIELD のすべてが設定されている

私の場合はTABLE_PARTITIONINGDAYTIME_PARTITIONING_FIELD_TYPETIMESTAMPを設定していたため作成されませんでした。

また上記以外でも作成されない条件があるので記載します。物によってはエラーが出るのでinitBigQuerySyncから始まるCloud Functionのログを見てみると良いです。下記のようなケースがあります。

  • TABLE_PARTITIONING が設定されていない
  • 既に同名のテーブルが有る
  • TABLE_PARTITIONINGを時間単位(HOUR)で設定しているが、TIME_PARTITIONING_FIELD_TYPEの型がDATE

詳細

Firebaseの拡張はextension.yamlを見ると構成や設定値が分かります。今回はインストール時のBigQueryのテーブル作成が問題であると考え、lifecycleEventsonInstallであるinitBigQuerySyncを探しました。それが下記です。

initBigQuerySyncの一部
/** Init the BigQuery sync */ await eventTracker.initialize();

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
export const initBigQuerySync = functions.tasks .taskQueue() .onDispatch(async () => { /** Setup runtime environment */ const runtime = getExtensions().runtime(); /** Init the BigQuery sync */ await eventTracker.initialize(); /** Run Backfill */ if (config.doBackfill) { await getFunctions() .taskQueue( `locations/${config.location}/functions/fsimportexistingdocs`, config.instanceId ) .enqueue({ offset: 0, docsCount: 0 }); return; } await runtime.setProcessingState( "PROCESSING_COMPLETE", "Sync setup completed" ); return; });

読んでいって、パーティションを作成していそうな関数を見つけます。

initializeRawChangeLogTableの一部
//Add partitioning await partitioning.addPartitioningToSchema(schema.fields); await partitioning.updateTableMetadata(options);

400 401 402 403
//Add partitioning await partitioning.addPartitioningToSchema(schema.fields); await partitioning.updateTableMetadata(options);

上記ですが2つ関数があります。BigQueryのパーティションを雰囲気で理解していたので気づかなかったのですが、実はBigQueryの2種類のパーティショニングをサポートしており、それぞれの関数が対応しています。

上は 時間単位列パーティショニングであり、ユーザーが指定したカラムを利用してパーティショニングを行うものです。下は取り込み時間パーティショニングであり、BigQueryに取り込まれた時間を利用してパーティショニングを行います。_PARTITIONTIMEという特別なカラムが作成されます。

addPartitioningToSchemaupdateTableMetadataも複数の条件により設定値を確認しています。
私はこの中のhasValidCustomPartitionConfigに引っかかってパーティションが作成されていませんでした。

hasValidCustomPartitionConfig
private hasValidCustomPartitionConfig() { /* Return false if partition type option has not been set*/ if (!this.isPartitioningEnabled()) return false; const { timePartitioningField, timePartitioningFieldType, timePartitioningFirestoreField, } = this.config; const hasNoCustomOptions = !timePartitioningField && !timePartitioningFieldType && !timePartitioningFirestoreField; /* No custom config has been set, use partition value option only */ if (hasNoCustomOptions) return true; /* check if all valid combinations have been provided*/ const hasOnlyTimestamp = timePartitioningField === "timestamp" && !timePartitioningFieldType && !timePartitioningFirestoreField; return ( hasOnlyTimestamp || (!!timePartitioningField && !!timePartitioningFieldType && !!timePartitioningFirestoreField) ); }

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
private hasValidCustomPartitionConfig() { /* Return false if partition type option has not been set*/ if (!this.isPartitioningEnabled()) return false; const { timePartitioningField, timePartitioningFieldType, timePartitioningFirestoreField, } = this.config; const hasNoCustomOptions = !timePartitioningField && !timePartitioningFieldType && !timePartitioningFirestoreField; /* No custom config has been set, use partition value option only */ if (hasNoCustomOptions) return true; /* check if all valid combinations have been provided*/ const hasOnlyTimestamp = timePartitioningField === "timestamp" && !timePartitioningFieldType && !timePartitioningFirestoreField; return ( hasOnlyTimestamp || (!!timePartitioningField && !!timePartitioningFieldType && !!timePartitioningFirestoreField) ); }

若干条件文が分かりづらいですが、hasNoCustomOptionsTABLE_PARTITIONINGのみが設定されているケース。それ以外では3つのプロパティがすべて設定されているケースです。
が、hasOnlyTimestampという文言を見るとTIME_PARTITIONING_FIELD_TYPETIMESTAMPでも問題ないように見えますが、timePartitioningFieldtimestampの場合という感じです(timestampはこの拡張のデフォルトで作成するカラム)。
色々設定を変更して試していたところTIME_PARTITIONING_FIELD_TYPEだけ設定した状態を引き継いでいたせいでこの様な状態になっていました。