コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

BigQueryのタイムトラベル機能を使って、脳に優しくリカバリしたい

こんにちは、アナリティクスグループの若鶴です!
アナリティクスグループは開発本部ではなくコーポレート本部に所属していますが、記事を書きたくなってしまったため、コドモン Product Team Blog に記事を書いています。

アナリティクスグループは、データでコドモンをドライブさせていくことを目標に、「データ分析基盤の整備」と「データ活用の伴走」をメインで行なっています。


早朝6時にデータパイプラインで障害が発生したとします。寝ぼけ眼で本番環境を触るリスクは計り知れません。
本記事では、BigQuery(以下、BQ)のタイムトラベル機能を利用した場合の、「脳に優しい」リカバリ手順を解説します。

定義と対象範囲

「脳に優しい」リカバリの3要素

混乱した頭でも確実にリカバリを完遂させるため、以下の3要素を重視します。

  • 思考プロセスの排除
    • 複雑なロジックや条件分岐を排除し、定型作業で完結する
  • 「権限エラー」との決別
    • いざというときに「権限が足りません」で止まらないよう、権限確認が不要な手順(コンソール上の安全な操作など)となっている
  • 不可逆な操作をしない(チェックポイントの作成)
    • いつでも「リカバリのやり直し」ができる状態を担保する

対象範囲

本記事では、以下の知識を持つ読者を対象としています。

  • BQとは何か?を理解している
  • BQのコピーとクローンの違いを理解している
  • dbtとは何か?を理解している
  • dbt snapshotのストラテジーについて理解している
  • シェルスクリプトについて理解している

データパイプラインの全体像

ざっくりと、データパイプラインは全体で以下のようになります。

  • データ転送
    • 外部システムからクラウド基盤へ日次でデータをロード
  • 履歴管理
    • ロードされたデータをもとに、変更履歴テーブルを更新
  • データ活用
    • 履歴データをもとに、分析用テーブル(DWH)を作成

履歴管理・データ活用にてBQとdbtを利用している前提で、リカバリ手順を記載します。

2つのユースケースでのリカバリ手順

この記事では、次の2つのユースケースでの手順を解説します。

  • ユースケース1
    • 「履歴管理」のデータセット内の全てのテーブルで、実行前の状態に戻したい
  • ユースケース2
    • 「データ活用」にてdbtでsnapshot形式のcheckストラテジーを採用しているテーブルを特定し、実行前の状態に戻したい

ユースケース1:「履歴管理」のデータセット内の全てのテーブルで、実行前の状態に戻したい

ユースケースの概要

「履歴管理」の工程で誤ってデータパイプラインを実行してしまい、実行前の状態に戻したいケースを考えます。


具体的には、以下の状態になります。

  • 履歴管理用のデータセット
    • 複数のデータセットを作成している
    • データセットはすべて東京リージョンに存在している
  • 履歴管理用のテーブル
    • パーティション・クラスタはすべてのテーブルで同じとは限らない
    • 履歴管理用のテーブルのみを格納している
    • 1データセット内のテーブル数は数百テーブル
    • 全てのテーブルがそこまで巨大ではない
      • 少なくとも100GB以下

今回のケースでは、以下のプロジェクト名・データセット名と仮定し、今後の手順を記載します。

  • BQプロジェクト名
    • hoge_project
  • BQデータセット名
    • hoge_dataset
  • バックアップしたい日時
    • 2026-01-29 04:00:00 JST

手順

シェルスクリプトで、次の動作を実行します。

  1. 実行後の対象データセットのバックアップを作成する
  2. 実行前の対象データセットのバックアップを作成する
  3. 実行後の対象データセットを実行前のデータセットに置き換える

今回の手順では、手順2と手順3でBQのバックアップ機能を利用します。

事前にシェルスクリプトを作成しておき、次のようにコマンドを叩くことでデータセットごと復元します。

recover_dataset_with_timetravel.sh "hoge_dataset" "2026-01-29 04:00:00 JST"
#!/bin/bash

# ----------------------------------------------
# 1. 変数の設定(環境に合わせて書き換えてください)
# ----------------------------------------------
PROJECT_ID="hoge_project"
DATASET_ID=${1} # 復元したいデータセット名 ex."hoge_dataset"
TARGET_TIME=${2} # 復元したい時点の時刻 ex."2026-01-29 10:00:00 JST"
REGION="asia-northeast1"

# 自動生成される変数
DATE_SUFFIX=$(date -j -f "%Y-%m-%d %H:%M:%S %Z" "${TARGET_TIME}" "+%Y%m%d") # macOSのコマンドで、他の環境の場合はエラーになる
MS_TIME=$(date -j -f "%Y-%m-%d %H:%M:%S %Z" "${TARGET_TIME}" "+%s000") # macOSのコマンドで、他の環境の場合はエラーになる
BACKUP_DATASET="backup__${DATASET_ID}"
TIMETRAVEL_DATASET="timetravel_${DATE_SUFFIX}__${DATASET_ID}"

# ----------------------------------------------
# 2. バックアップ用データセットの作成
# ----------------------------------------------
# 存在する場合は削除してから新規に作成する
# バックアップにテーブルが残っていると後工程でエラーになるので、まず削除する
bq rm -r -f ${PROJECT_ID}:${BACKUP_DATASET}
bq rm -r -f ${PROJECT_ID}:${TIMETRAVEL_DATASET}

bq mk --location=${REGION} -d -f ${PROJECT_ID}:${BACKUP_DATASET}
bq mk --location=${REGION} -d -f ${PROJECT_ID}:${TIMETRAVEL_DATASET}

# ----------------------------------------------
# 3. テーブル一覧の取得
# ----------------------------------------------
# 指定したデータセット内のベーステーブル一覧を取得
TABLES=$(bq ls -a ${PROJECT_ID}:${DATASET_ID} | awk 'NR>2 {print $1}')

# ----------------------------------------------
# 4. 復元処理(ループ実行)
# ----------------------------------------------
for TABLE_ID in ${TABLES}; do
    echo "--------------------------------------------------"
    echo "Processing table: ${TABLE_ID}"
    
    # ① 現時点の状態をバックアップ (Backup)
    # no_clobberフラグをつけ、既存のテーブルが存在しない場合に成功する。それ以外ではエラーになる
    echo "[1/3] Backing up current state..."
    bq cp --clone --no_clobber "${PROJECT_ID}:${DATASET_ID}.${TABLE_ID}" "${PROJECT_ID}:${BACKUP_DATASET}.${TABLE_ID}"

    # ② 過去時点の状態を別保存 (Timetravel)
    # no_clobberフラグをつけ、既存のテーブルが存在しない場合に成功する。それ以外ではエラーになる
    echo "[2/3] Extracting past state..."
    bq cp --clone --no_clobber "${PROJECT_ID}:${DATASET_ID}.${TABLE_ID}@${MS_TIME}" "${PROJECT_ID}:${TIMETRAVEL_DATASET}.${TABLE_ID}"

    # ③ 本番テーブルを過去の状態に上書き復元 (Restore)
    echo "[3/3] Restoring main table..."
    bq cp -f "${PROJECT_ID}:${DATASET_ID}.${TABLE_ID}@${MS_TIME}" "${PROJECT_ID}:${DATASET_ID}.${TABLE_ID}"
    
    echo "Table ${TABLE_ID} has been restored successfully."
done

echo "=================================================="
echo "All tables in ${DATASET_ID} have been restored to ${TARGET_TIME}"

Q&A

実行前のバックアップは必要かどうか?

② 過去時点の状態を別保存 (Timetravel)の手順が必要かどうか気になる人もいると思います。

今回のリカバリ後に、参照元のテーブルを修正し、もう一度 dbt snapshotコマンドを実行します。
dbt snapshotの結果を比較するために必要だと考えています。

bq cp --clone --no_clobberは、どのような挙動になるか?

ここからは、このコマンドのフラグについて、順番に説明していきます。
基本的な内容はbq cpの公式docsに記載がありますが、別の記事を確認する必要もあります。

docs.cloud.google.com


--cloneは、テーブル クローンを作成するためのフラグです。
このフラグをつけない場合は、クローンではなく通常のテーブルになります。

クローンの場合、ベーステーブルとは異なるテーブル クローンのデータ ストレージに対してのみ課金されます。
最初はテーブル クローンに対するストレージ コストは発生しません。
バックアップ用のデータセットのテーブルではテーブル クローンを利用し、ストレージ金額を削減することを意図しています。

docs.cloud.google.com


--no_clobberは、宛先テーブルが存在する場合に上書きを禁止するフラグです。
--cloneを利用する場合は、--no_clobberフラグが必須となります。

docs.cloud.google.com

他の手順との比較

他の手順での主な候補は、

  • SQL文の中でCTAS(Create Table As Select)を利用する
  • SQL文の中でクローンを利用する

があると思います。

ここからは、採用した手順と他の手順のメリット・デメリットを比較します。

SQL文の中でCTASを利用する

BQのコンソール上で Create Table As Selectを利用し、新規にテーブルを作成するという方法です。

GoogleでBQ timetravelなどと検索し、検索上位の記事にアクセスしてみます。
すると、以下のようなSQL文で過去断面を取得するクエリが出てきます。

SELECT
  *
FROM
  `hoge_project.hoge_dataset.hoge_table`
  FOR SYSTEM_TIME AS OF "2026-01-17 05:00:00+09:00"

データを取得するだけであれば、SELECTを利用しても不便はありません。 むしろ、where句で抽出データを制限できるので、より柔軟にデータを確認できます。

しかし、この抽出結果をもとにテーブルを作成する場合、パーティションとクラスタを一致させる必要があります。
今回は前提条件として、「データセット内のテーブルのパーティション・クラスタは同じとは限らない」を置いているため、CTAS文を利用できないと判断しました。

次にSQL文でクローンを利用するケースを見ていきたいと思います。

SQL文でクローンを利用する

BQのクローンは、ざっくりいうと、ベーステーブルを元にテーブル及びレコードを複製するという方法です。
ベーステーブルから変更した箇所にのみコストがかかります。

詳しくは、公式ドキュメントを見てもらうのがおすすめです。

docs.cloud.google.com


bq cpコマンドと比較した場合の、クローンテーブルを利用するメリット・デメリットは次の通りです。 (公式ドキュメントから判断できない内容も多く、一部に推測を含みます)

  • メリット
    • ベーステーブルを元に複製するため、素早くリカバリできる
    • クローン後に変更されたデータの分だけ料金を支払うため、コスト効率が高い
  • デメリット
    • クローンテーブルのパーティション構成を後から調整できないため、クエリの修正に限界が生じる可能性がある
    • 多数のクローンを作成・維持すると、管理が複雑になり、予期せぬコスト(差分データの蓄積)が発生する可能性がある

今回のケースでは、前提として「全てのテーブルがそこまで巨大ではない」ことを置いています。
データパイプラインで将来的に予期せぬ悪影響が起こることを懸念し、今回はbq cpコマンドを採用しました。

ユースケース2:dbt snapshotでcheckストラテジーを採用しているテーブルを特定し、実行前の状態に戻したい

ユースケースの概要

dbt snapshotでcheckストラテジーを採用しているケースを考えます。
この記事では、このテーブルをcheckタイプの履歴テーブルと呼ぶことにします。

今回のケースでは、分析用テーブル(DWH)を作成する工程でデータパイプラインを実行してしまい、checkタイプの履歴テーブルを実行前の状態に戻したいケースを考えます。
今回のブログ記事では「なぜ実行前に戻す必要があるのか?」については、解説を省略します。


具体的には、以下の状態になります。

  • 全体
    • どのデータセットにcheckタイプの履歴テーブルが存在するかわかっていない
    • 1つのデータセットの中に、通常のテーブルとcheckタイプの履歴テーブルが混在している
    • checkタイプの履歴テーブルは、そこまで多くは利用していない
      • 最大で3テーブル程度
  • BQ
    • 一時的なテーブル作成のために、tempというデータセットが存在している
  • dbtディレクトリ
    • 分析用のcheckタイプの履歴テーブルは、models配下に存在している

また、BQプロジェクト名は、ユースケース1と同じくhoge_projectと仮定します。

手順の概要

ざっくりとした手順は次の通りになります。

  1. checkタイプの履歴テーブルを検索する
  2. バックアップを作成し、実行後の対象テーブルを実行前に置き換える

手順1:checkタイプの履歴テーブルを検索する

ローカル環境やdbt Cloudのコマンド画面で dbt lsコマンドを実行し、checkタイプの履歴テーブルのモデルを検索します。
今回のブログ記事では、lsコマンドの詳細の説明は省略します。

dbt ls --select config.materialized:snapshot,config.strategy:check,path:models --resource-type model

出力したモデルをBQで検索し、データセット名とテーブル名を特定します。

今回のケースでは、次の1つのテーブルを実行前に戻す必要があると仮定します。

  • 対象テーブルのデータセット名
    • hoge2_dataset
  • 対象テーブルのテーブル名
    • hoge_table

手順2:バックアップを作成し、実行後の対象テーブルを実行前に置き換える

Cloud Shell ターミナルで、変数を書き換えてコマンドを叩きます。

# ------------------------------
# 1. 変数の設定(適宜書き換えてください)
# ------------------------------
export PROJECT_ID="hoge_project"
export DATASET_ID="hoge2_dataset"
export TABLE_ID="hoge_table"
export BACKUP_DATASET="temp"
export TARGET_TIME="yyyy-mm-dd hh:mm:ss JST" # ← 実行前の正常だった時刻

# タイムスタンプをミリ秒に変換
export MS_TIME=$(date -d "${TARGET_TIME}" +%s%3N)

# ------------------------------
# 2. 三段階の処理を一気に行う
# ------------------------------

# ① 【実行後】のバックアップ(現状の不整合データを保存)
bq cp -f ${PROJECT_ID}:${DATASET_ID}.${TABLE_ID} ${PROJECT_ID}:${BACKUP_DATASET}.${TABLE_ID}_post_err && \

# ② 【実行前】のバックアップ(タイムトラベル機能で過去の状態を別名保存)
bq cp -f ${PROJECT_ID}:${DATASET_ID}.${TABLE_ID}@${MS_TIME} ${PROJECT_ID}:${BACKUP_DATASET}.${TABLE_ID}_pre_restore && \

# ③ 【実行前】の状態に元テーブルを復元(上書き)
bq cp -f ${PROJECT_ID}:${DATASET_ID}.${TABLE_ID}@${MS_TIME} ${PROJECT_ID}:${DATASET_ID}.${TABLE_ID}

手順2の代替手順の確認

有力な代替手順としては、ユースケース1と同様にSQL文を利用したリカバリ方法があります。

具体的には、次の2つです。

  • SQL文の中でCTAS(Create Table As Select)を利用する
  • SQL文の中でクローンを利用する

ユースケース1と同様の理由で、bq cpコマンドを使うべきだと考えています。

最後に

今回のブログ記事では、BQでタイムトラベルを利用する場合のリカバリ手順について解説しました。

私個人としても、今回記述したリカバリ手順にも改善の余地はあると思っていますし、前提条件によっては最適な手順も変わると思っています。
今回の記事をベースに、いろんな方と議論できると嬉しいです!!