この記事は BrainPad Advent Calendar 2020 2日目の記事になります
こんにちは@nissy0409240です
BrainPadでエンジニアをしています
最近は@BrainPadProductの中の人もしています!是非フォローをよろしくお願いします!
さて、2020年もあと一ヶ月を切りましたが
皆様いかがお過ごしでしょうか
今年は新型コロナの影響もあり移動について考えさせられた1年だったように思います
ふと移動に思いを巡らすと移動というもの一つとっても様々なものがあります
観光・異動・引越し・データ移行...
本エントリーではBig Queryのテーブルデータを移行した際に調べたこと
またどのように実施したかをお話出来ればと思います
背景
昨今、こちらのエントリーのようにデータ基盤として
GCPのBigQueryを採用する事例は珍しいものではないと感じます
しかし、データ基盤は構築して終わりではありません
運用・保守対応が発生します
テーブルやデータセット、カラムの追加は
イメージ出来る方も多いと思いますが
テーブルデータの移行をしなくてはいけない時もあります
しかも移行だけではなく簡単なデータの変換も必要な状況です
そんな訳で今回やりたいことはこの二つになります
- テーブルデータをエクスポートして別のテーブルにインポートしたい
- テーブル名は同じにする
- レコード数はテーブルによって異なる(多いものはめちゃ多い...)
- エクスポートしたデータの中身を変換したい
- ひとつのカラムを機械的に全件変換する
方法検討
やると決まったら方法の検討です
ドキュメントを読み漁り
以下の2パターンに絞りました
方法 | メリット | デメリット |
---|---|---|
エクスポート/変換/ロード | エクスポート自体の料金はかからない | スクリプトをまとめて実行するのでシェルの作成や変換処理の実装で多少の手間はかかる |
データセットコピー/UPDATEステートメント実行 | コマンド二つで作業が完了する | データセットのコピーはβ版。UPDATE文実行がフルスキャンになってしまうため料金が跳ね上がる可能性がある |
やはりUPDATEが全件フルスキャンとなりコストがかかる可能性やβ版の機能よりは枯れた技術を使う方がいいという考えから今回は「エクスポート/変換/ロード」を採用しました
スクリプト作成
ここまで決まったらスクリプト作成です
リファレンスを参考にスクリプトを作成していきます
一行一行解説していくのもくどいので処理ごとにコメントを書きつつ
出来上がったスクリプトを下記に記します
(URLやパス、テーブル名などは擬似的なものに書き換えています)
#!/bin/bash
BEFORE_DATA_SET=$1
AFTER_DATA_SET=$2
# 実行環境に一時ディレクトリ作成
mkdir -p tmp/
while true ;do
for TARGET_TABLE in hoge huga piyo;do
# 移行元テーブルからエクスポート
bq extract --compression GZIP --destination_format NEWLINE_DELIMITED_JSON \
--print_header=false \
"${BEFORE_DATA_SET}.${TARGET_TABLE}" \
gs://aexample-bucket/path/to/export_${TARGET_TABLE}.json.gz
# GCSからエクスポートしたファイルを取得
gsutil cp gs://example-bucket/path/to/${TARGET_TABLE}.json.gz tmp/
gunzip tmp/${TARGET_TABLE}.json.gz
# sedで置換
sed -i "s/\"example_key\":\"before\"/\"example_key\":\"after\"/g" tmp/${TARGET_TABLE}.json
# GCSへアップロード
gsutil cp tmp/${TARGET_TABLE}.json gs://example-bucket/path/to/load/
# 一時ディレクトリのファイル削除
rm tmp/${TARGET_TABLE}.json
# 移行先テーブルへロード
bq load --source_format=NEWLINE_DELIMITED_JSON "${AFTER_DATA_SET}.${TARGET_TABLE}" gs://example-bucket/path/to/load/${TARGET_TABLE}.json >/dev/null 2>/dev/null
# GCSのファイルを削除
gsutil rm gs://aexample-bucket/path/to/export_${TARGET_TABLE}.json.gz
gsutil rm gs://example-bucket/path/to/load/${TARGET_TABLE}.json
done
done
exit
実行結果
実際にテスト用データセットで実行してみると無事、指定したデータセット間で同じ名前のテーブルが作成され置換も成功し、データもロードされていました
件数も想定通り!
めでたしめでたし...とはなりません!
パーティション分割テーブル
Big Queryではパーティション分割テーブルを作成することが出来ます
パーティショニングすることでパフォーマンスの向上とコスト削減を行うことが可能です
パーティション分割テーブルでは_PARTITIONTIME
や_PARTITIONDATE
擬似列が存在し実行時にはこちらを指定することでテーブルに読み込まれた日付を指定することが可能となります
先ほどのスクリプトではこちらを考慮出来ておらず過去の_PARTITIONTIME
を指定した際に何も抽出されず、
実行した時点の_PARTITIONTIME
を指定したところ全件出力されしまいました
そこでエクスポートする際、パーティションデコレータを指定してエクスポートすることにしました
先ほどのスクリプトのエクスポート部分をパーティションデコレータを付与する形式に書き換えて上げます
# 移行元テーブルからエクスポート(修正前)
bq extract --compression GZIP --destination_format NEWLINE_DELIMITED_JSON \
--print_header=false \
"${DATA_SET}.${TARGET_TABLE}" \
gs://aexample-bucket/path/to/export_${TARGET_TABLE}.json.gz
# 移行元テーブルからエクスポート(修正後)
bq extract --compression GZIP --destination_format NEWLINE_DELIMITED_JSON \
--print_header=false \
"${DATA_SET}.${TARGET_TABLE}\$$20201201" \
gs://aexample-bucket/path/to/export_${TARGET_TABLE}_$20201201.json.gz
しかし、上記に書いたような_PARTITIONTIMEのハードコーディングは実際のテーブルでは1日分のテスト以外役に立ちません
スクリプト全体を修正します
#!/bin/bash
BEFORE_DATA_SET=$1
AFTER_DATA_SET=$2
#
# 日付
#
FROM_DATE=$3
TO_DATE=$4
TARGET_DATE=$FROM_DATE
# 実行環境に一時ディレクトリ作成
mkdir -p tmp/
while true ;do
for TARGET_TABLE in hoge huga piyo;do
# 移行元テーブルからエクスポート
bq extract --compression GZIP --destination_format NEWLINE_DELIMITED_JSON \
--print_header=false \
"${BEFORE_DATA_SET}.${TARGET_TABLE}\$${TARGET_DATE}" \
gs://aexample-bucket/path/to/export_${TARGET_TABLE}_${TARGET_DATE}.json.gz
# GCSからエクスポートしたファイルを取得
gsutil cp gs://example-bucket/path/to/${TARGET_TABLE}_${TARGET_DATE}.json.gz tmp/
gunzip tmp/${TARGET_TABLE}_${TARGET_DATE}.json.gz
# sedで置換
sed -i "s/\"example_key\":\"before\"/\"example_key\":\"after\"/g" tmp/${TARGET_TABLE}_${TARGET_DATE}.json
# GCSへアップロード
gsutil cp tmp/${TARGET_TABLE}_${TARGET_DATE}.json gs://example-bucket/path/to/load/
# 一時ディレクトリのファイル削除
rm tmp/${TARGET_TABLE}_${TARGET_DATE}.json
# 移行先テーブルへロード
bq load --source_format=NEWLINE_DELIMITED_JSON "${AFTER_DATA_SET}.${TARGET_TABLE}\$${TARGET_DATE}" gs://example-bucket/path/to/load/${TARGET_TABLE}_${TARGET_DATE}.json >/dev/null 2>/dev/null
# GCSのファイルを削除
gsutil rm gs://aexample-bucket/path/to/export_${TARGET_TABLE}_${TARGET_DATE}.json.gz
gsutil rm gs://example-bucket/path/to/load/${TARGET_TABLE}_${TARGET_DATE}.json
done
if [ "$TARGET_DATE" = "$TO_DATE" ] ; then
# 一時ディレクトリ削除
rm -Rf tmp/
break
fi
TARGET_DATE=$(date -d "$TARGET_DATE 1day" "+%Y%m%d")
done
exit
こちらを実行し、無事過去の_PARTITIONTIME
もそのままに移行することが出来ました
めでたしめでたし!
最後までお付き合い頂きありがとうございました
参考
https://cloud.google.com/bigquery/docs/exporting-data?hl=ja#bq
https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-json
https://cloud.google.com/bigquery/docs/exporting-data?hl=ja#pricing
https://cloud.google.com/bigquery/docs/copying-datasets?hl=ja
https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax?hl=ja#update_statement
https://cloud.google.com/bigquery/pricing/?hl=ja#dml
https://cloud.google.com/bigquery/docs/creating-partitioned-tables?hl=ja
https://cloud.google.com/bigquery/docs/best-practices-costs?hl=ja#partition_data_by_date
https://cloud.google.com/bigquery/docs/managing-partitioned-table-data#browsing_partitioned_table_data