BigQuery の スキーマ追従(schema evolution)機能の挙動を確認してみた。

BigQuery の スキーマ追従(schema evolution)機能の挙動を確認してみた。

2026.06.04

こんにちは、みかみです。

猟犬な血を引いてるせいか、小さい生物を追いかけたくてたまらないらしいうちのコ(犬)です。
数年前はショッピングモールでリスの置物を見つめたまま動かなくなりました。散歩では、白いビニール袋を見つけると突進してゆきます。最近では白い看板にも臨戦態勢になりました。
それ、生き物ではありませんよー

はじめに

BigQuery のスキーマ自動検出(autodetect)を指定すると、データ初期ロード時にあらかじめテーブルを作成する必要なく、データファイルのフォーマットやデータ型を読み取って自動でテーブルを作成することができます。

また、既存テーブルへのデータロード時に ALLOW_FIELD_ADDITION を指定すると、データファイルに新しい項目が追加された場合にロード先テーブルにもカラムを自動で追加することができ、ALLOW_FIELD_RELAXATION 指定でテーブルの REQUIRED 制約を自動で解除することが可能です。

前回、Snowflake の schema evolution 機能を確認したので、BigQuery ではどうか、確認したいと思いました。

本ブログでは、BigQuery のバッチ取り込み(bq load コマンド)を手動実行して、以下の5つのケースのスキーマ差異ファイルの取り込み時の挙動を確認します。

  • case1:カラム追加 + カラム削除
  • case2:カラム順序差異あり
  • case3:カラム(ファイルヘッダ)名差異あり
  • case4:REQUIRED カラムのファイルデータ値欠如
  • case5:データ型差異あり

やりたいこと

  • BigQuery の schema evolution 機能(schema_update_option オプションで ALLOW_FIELD_ADDITION, ALLOW_FIELD_RELAXATION 指定)の挙動を確認したい
  • BigQuery と Snowflake の schema evolution 機能に違いがあるか確認したい

前提

Google Cloud SDK(gcloud コマンド)の実行環境は準備済みであるものとします。 本エントリでは、Cloud Shell を使用しました。

また、BigQuery や GCS など各サービス操作に必要な API の有効化と権限は付与済みです。

なお、文中、Google Cloud プロジェクト ID など一部の文字は伏字に変更しています。

準備:検証用テーブルとサンプルデータファイルを準備

以下の SQL を実行して、データロード先の BigQuery のデータセットとテーブルを作成します。

bq --location=asia-northeast1 mk \
  --dataset \
  [PROJECT_ID]:schema_evolution

bq query --use_legacy_sql=false --location=asia-northeast1 '
CREATE OR REPLACE TABLE `[PROJECT_ID].schema_evolution.se_test` (
  id   INT64  NOT NULL,
  name STRING,
  age  INT64
);'

検証用のテーブルが作成できました。

$ bq show [PROJECT_ID]:schema_evolution.se_test
Table [PROJECT_ID]:schema_evolution.se_test

   Last modified             Schema             Total Rows   Total Bytes   Expiration   Time Partitioning   Clustered Fields   Total Logical Bytes   Total Physical Bytes   Labels  
 ----------------- --------------------------- ------------ ------------- ------------ ------------------- ------------------ --------------------- ---------------------- -------- 
  03 Jun 11:54:26   |- id: integer (required)   0            0                                                                 0                                                    
                    |- name: string                                                                                                                                                 
                    |- age: integer                                                                                                                                                 

続いて以下のコマンドを実行して、サンプルデータファイルを作成して GCS バケットにアップロードします。

# 初期データファイル(全ケース共通)
cat << 'EOF' > initial.csv
id,name,age
1,Alice,25
2,Bob,30
EOF

# case1:カラム追加(email を id と name の間に挿入)+ age 削除
cat << 'EOF' > case1_evolution.csv
id,email,name
3,charlie@example.com,Charlie
EOF

# case2:カラム順序変更
cat << 'EOF' > case2_evolution.csv
age,name,id
35,Charlie,3
EOF

# case3:カラム名変更(name → full_name)
cat << 'EOF' > case3_evolution.csv
id,full_name,age
3,Charlie,35
EOF

# case4:REQUIRED カラムの欠落(id カラムなし)
cat << 'EOF' > case4_evolution.csv
name,age
Charlie,35
EOF

# case5:データ型変更(age に数値として解釈できない文字列)
cat << 'EOF' > case5_evolution.csv
id,name,age
3,Charlie,thirty-five
EOF

# 検証データファイルアップロード
gcloud storage cp initial.csv         gs://test-mikami/schema_evolution/bq/
gcloud storage cp case1_evolution.csv gs://test-mikami/schema_evolution/bq/
gcloud storage cp case2_evolution.csv gs://test-mikami/schema_evolution/bq/
gcloud storage cp case3_evolution.csv gs://test-mikami/schema_evolution/bq/
gcloud storage cp case4_evolution.csv gs://test-mikami/schema_evolution/bq/
gcloud storage cp case5_evolution.csv gs://test-mikami/schema_evolution/bq/

GCS にアップロードした初期データファイルをテーブルにロードします。

bq load \
  --location=asia-northeast1 \
  --source_format=CSV \
  --skip_leading_rows=1 \
  [PROJECT_ID]:schema_evolution.se_test \
  gs://test-mikami/schema_evolution/bq/initial.csv

無事、初期データがロードできました。

$ bq query --use_legacy_sql=false --location=asia-northeast1 \
  'SELECT * FROM `[PROJECT_ID].schema_evolution.se_test`'
+----+-------+-----+
| id | name  | age |
+----+-------+-----+
|  1 | Alice |  25 |
|  2 | Bob   |  30 |
+----+-------+-----+

case1:カラム追加 + カラム削除

以下のファイルデータを、先程初期データをロードしたテーブルに取り込みます。

$ gcloud storage cp gs://test-mikami/schema_evolution/bq/case1_evolution.csv -
Copying gs://test-mikami/schema_evolution/bq/case1_evolution.csv to file://-
id,email,name
3,charlie@example.com,Charlie

email が追加され、age がないデータファイルです。

以下のコマンドを実行して、データをテーブルにロードします。

bq load \
  --location=asia-northeast1 \
  --source_format=CSV \
  --skip_leading_rows=1 \
  --schema_update_option=ALLOW_FIELD_ADDITION \
  --schema_update_option=ALLOW_FIELD_RELAXATION \
  --autodetect \
  [PROJECT_ID]:schema_evolution.se_test \
  gs://test-mikami/schema_evolution/bq/case1_evolution.csv

テーブルデータを確認してみます。

$ bq query --use_legacy_sql=false --location=asia-northeast1 \
  'SELECT * FROM `[PROJECT_ID].schema_evolution.se_test`'
+----+---------------------+---------+------+
| id |        email        |  name   | age  |
+----+---------------------+---------+------+
|  3 | charlie@example.com | Charlie | NULL |
|  1 | NULL                | Alice   |   25 |
|  2 | NULL                | Bob     |   30 |
+----+---------------------+---------+------+

期待通り、email カラムがテーブルに追加され、データファイルになかった age カラムの値は null で、正常にロードできました。

case2:カラム順序差異あり

次に、以下のデータファイルを取り込みます。

$ gcloud storage cp gs://test-mikami/schema_evolution/bq/case2_evolution.csv -
Copying gs://test-mikami/schema_evolution/bq/case2_evolution.csv to file://-
age,name,id
35,Charlie,3

テーブル定義は id,name,age のカラム順ですが、データファイルは項目の並び順が逆になっています。

以下のコマンドでロードします。

bq load \
  --location=asia-northeast1 \
  --source_format=CSV \
  --skip_leading_rows=1 \
  --schema_update_option=ALLOW_FIELD_ADDITION \
  --schema_update_option=ALLOW_FIELD_RELAXATION \
  --autodetect \
  [PROJECT_ID]:schema_evolution.se_test \
  gs://test-mikami/schema_evolution/bq/case2_evolution.csv

テーブルデータを確認します。

$ bq query --use_legacy_sql=false --location=asia-northeast1 \
  'SELECT * FROM `[PROJECT_ID].schema_evolution.se_test`'
+----+---------------------+---------+------+
| id |        email        |  name   | age  |
+----+---------------------+---------+------+
|  3 | charlie@example.com | Charlie | NULL |
|  1 | NULL                | Alice   |   25 |
|  2 | NULL                | Bob     |   30 |
|  3 | NULL                | Charlie |   35 |
+----+---------------------+---------+------+

データファイルの項目順に差異があっても、正常にロードできました。

case3:カラム(ファイルヘッダ)名差異あり

続いて、テーブルのカラム名とデータファイルのヘッダの名前に差分があるファイルです。テーブルのカラム名は name ですが、データファイルのヘッダでは full_name になっています。

$ gcloud storage cp gs://test-mikami/schema_evolution/bq/case3_evolution.csv -
Copying gs://test-mikami/schema_evolution/bq/case3_evolution.csv to file://-
id,full_name,age
3,Charlie,35
bq load \
  --location=asia-northeast1 \
  --source_format=CSV \
  --skip_leading_rows=1 \
  --schema_update_option=ALLOW_FIELD_ADDITION \
  --schema_update_option=ALLOW_FIELD_RELAXATION \
  --autodetect \
  [PROJECT_ID]:schema_evolution.se_test \
  gs://test-mikami/schema_evolution/bq/case3_evolution.csv

正常にロードできたようなので、テーブルデータを確認します。

$ bq query --use_legacy_sql=false --location=asia-northeast1 \
  'SELECT * FROM `[PROJECT_ID].schema_evolution.se_test`'
+----+-----------+------+---------------------+---------+
| id | full_name | age  |        email        |  name   |
+----+-----------+------+---------------------+---------+
|  3 | NULL      |   35 | NULL                | Charlie |
|  1 | NULL      |   25 | NULL                | Alice   |
|  2 | NULL      |   30 | NULL                | Bob     |
|  3 | NULL      | NULL | charlie@example.com | Charlie |
|  3 | Charlie   |   35 | NULL                | NULL    |
+----+-----------+------+---------------------+---------+

full_name カラムがテーブルに追加され、エラーなくファイルデータがロードできました。

case4:REQUIRED カラムのファイルデータ値欠如

テーブルで REQUIREDNOT NULL)指定されている id カラム項目が存在しないデータファイルです。

$ gcloud storage cp gs://test-mikami/schema_evolution/bq/case4_evolution.csv -
Copying gs://test-mikami/schema_evolution/bq/case4_evolution.csv to file://-
name,age
Charlie,35
bq load \
  --location=asia-northeast1 \
  --source_format=CSV \
  --skip_leading_rows=1 \
  --schema_update_option=ALLOW_FIELD_ADDITION \
  --schema_update_option=ALLOW_FIELD_RELAXATION \
  --autodetect \
  [PROJECT_ID]:schema_evolution.se_test \
  gs://test-mikami/schema_evolution/bq/case4_evolution.csv

テーブルデータを確認します。

$ bq query --use_legacy_sql=false --location=asia-northeast1 \
  'SELECT * FROM `[PROJECT_ID].schema_evolution.se_test`'
+------+-----------+------+---------------------+---------+
|  id  | full_name | age  |        email        |  name   |
+------+-----------+------+---------------------+---------+
| NULL | NULL      |   35 | NULL                | Charlie |
|    3 | NULL      | NULL | charlie@example.com | Charlie |
|    3 | NULL      |   35 | NULL                | Charlie |
|    1 | NULL      |   25 | NULL                | Alice   |
|    2 | NULL      |   30 | NULL                | Bob     |
|    3 | Charlie   |   35 | NULL                | NULL    |
+------+-----------+------+---------------------+---------+

テーブル定義も確認します。

$ bq show [PROJECT_ID]:schema_evolution.se_test
Table [PROJECT_ID]:schema_evolution.se_test

   Last modified           Schema          Total Rows   Total Bytes   Expiration   Time Partitioning   Clustered Fields   Total Logical Bytes   Total Physical Bytes   Labels  
 ----------------- ---------------------- ------------ ------------- ------------ ------------------- ------------------ --------------------- ---------------------- -------- 
  03 Jun 12:46:27   |- id: integer         6            149                                                               149                   6751                           
                    |- full_name: string                                                                                                                                       
                    |- age: integer                                                                                                                                            
                    |- email: string                                                                                                                                           
                    |- name: string                                                                                                                                            

id カラムの required が削除され、ファイルデータが正常にロードできました。

case5:データ型差異あり

最後に、テーブル定義とファイルデータでデータ型の差異があるファイルです。

$ gcloud storage cp gs://test-mikami/schema_evolution/bq/case5_evolution.csv -
Copying gs://test-mikami/schema_evolution/bq/case5_evolution.csv to file://-
id,name,age
3,Charlie,thirty-five

テーブルの age カラムは integer で定義されているので、文字列はロードできないはずです。

bq load \
  --location=asia-northeast1 \
  --source_format=CSV \
  --skip_leading_rows=1 \
  --schema_update_option=ALLOW_FIELD_ADDITION \
  --schema_update_option=ALLOW_FIELD_RELAXATION \
  --autodetect \
  [PROJECT_ID]:schema_evolution.se_test \
  gs://test-mikami/schema_evolution/bq/case5_evolution.csv

やはり、データ型が異なるファイルはロードエラーが発生しました。

$ bq load \
  --location=asia-northeast1 \
  --source_format=CSV \
  --skip_leading_rows=1 \
  --schema_update_option=ALLOW_FIELD_ADDITION \
  --schema_update_option=ALLOW_FIELD_RELAXATION \
  --autodetect \
  [PROJECT_ID]:schema_evolution.se_test \
  gs://test-mikami/schema_evolution/bq/case5_evolution.csv
Waiting on bqjob_r636fda207af8f428_0000019e8d892cc4_1 ... (0s) Current status: DONE   
BigQuery error in load operation: Error processing job '[PROJECT_ID]:bqjob_r636fda207af8f428_0000019e8d892cc4_1': Provided Schema does not match Table cm-da-mikami-
yuki-258308:schema_evolution.se_test. Field age has changed type from INTEGER to STRING
Failure details:
- It looks like you are appending to an existing table with
autodetect enabled. Disabling autodetect may resolve this.

Parquet ファイルでは autodetect 指定不要か確認

CSV ファイルを --autodetect 指定なしでロードしようとしたところ、ALLOW_FIELD_ADDITION を指定していても、以下のエラーが発生しました。

$ bq load \
  --location=asia-northeast1 \
  --source_format=CSV \
  --skip_leading_rows=1 \
  --schema_update_option=ALLOW_FIELD_ADDITION \
  --schema_update_option=ALLOW_FIELD_RELAXATION \
  [PROJECT_ID]:schema_evolution.se_test \
  gs://test-mikami/schema_evolution/bq/case1_evolution.csv
Waiting on bqjob_r2faf78a1d0689aaa_0000019e8d777d5d_1 ... (0s) Current status: DONE   
BigQuery error in load operation: Error processing job '[PROJECT_ID]:bqjob_r2faf78a1d0689aaa_0000019e8d777d5d_1': Error while reading data, error message: CSV processing
encountered too many errors, giving up. Rows: 1; errors: 1; max bad: 0; error percent: 0
Failure details:
- gs://test-mikami/schema_evolution/bq/case1_evolution.csv: Error
while reading data, error message: Unable to parse; line_number: 2
byte_offset_to_start_of_line: 14 column_index: 2 column_name: "age"
column_type: INT64 value: "Charlie" File: gs://test-
mikami/schema_evolution/bq/case1_evolution.csv

公式ドキュメントには以下の記載がありました。

If the data you're appending is in CSV or newline-delimited JSON format, specify the --autodetect flag to use schema auto-detection or supply the schema in a JSON schema file. The added columns can be automatically inferred from Avro or Datastore export files.

Because schemas can be automatically inferred from Avro data you don't need to use the --autodetect flag.

では、Avro 同様にスキーマ情報を含む Parquet ファイルも、autodetect 指定なしでスキーマ追従できるのか確認してみます。

準備:Parquet ファイル作成&データロード先テーブル初期化

以下を実行して、ロードする Parquet ファイルを作成して GCS にアップロードします。

# Parquet ファイル作成スクリプト
cat << 'EOF' > create_case1_parquet.py
import pyarrow as pa
import pyarrow.parquet as pq

schema = pa.schema([
    pa.field('id',    pa.int64(),  nullable=False),
    pa.field('email', pa.string(), nullable=True),
    pa.field('name',  pa.string(), nullable=True),
])

table = pa.table({
    'id':    [3],
    'email': ['charlie@example.com'],
    'name':  ['Charlie'],
}, schema=schema)

pq.write_table(table, 'case1_evolution.parquet')
print('case1_evolution.parquet を作成しました')
EOF

# Parquet ファイル作成スクリプトを実行
python3 create_case1_parquet.py

# GCS にアップロード
gcloud storage cp case1_evolution.parquet gs://test-mikami/schema_evolution/bq/

ロード先テーブルを初期化します。

bq query --use_legacy_sql=false --location=asia-northeast1 '
CREATE OR REPLACE TABLE `[PROJECT_ID].schema_evolution.se_test` (
  id INT64 NOT NULL, name STRING, age INT64
);'

bq load \
  --location=asia-northeast1 \
  --source_format=CSV \
  --skip_leading_rows=1 \
  [PROJECT_ID]:schema_evolution.se_test \
  gs://test-mikami/schema_evolution/bq/initial.csv

テーブル定義と初期データを確認しておきます。

$ bq show [PROJECT_ID]:schema_evolution.se_test
Table [PROJECT_ID]:schema_evolution.se_test

   Last modified             Schema             Total Rows   Total Bytes   Expiration   Time Partitioning   Clustered Fields   Total Logical Bytes   Total Physical Bytes   Labels  
 ----------------- --------------------------- ------------ ------------- ------------ ------------------- ------------------ --------------------- ---------------------- -------- 
  03 Jun 14:30:24   |- id: integer (required)   2            44                                                                44                    5898                           
                    |- name: string                                                                                                                                                 
                    |- age: integer                                                                                                                                                 

$ bq query --use_legacy_sql=false --location=asia-northeast1 \
  'SELECT * FROM `[PROJECT_ID].schema_evolution.se_test`'
+----+-------+-----+
| id | name  | age |
+----+-------+-----+
|  1 | Alice |  25 |
|  2 | Bob   |  30 |
+----+-------+-----+

以下のコマンドを実行して、Parquet ファイルを --autodetectALLOW_FIELD_RELAXATION も指定せずにロードします。

bq load \
  --location=asia-northeast1 \
  --source_format=PARQUET \
  --schema_update_option=ALLOW_FIELD_ADDITION \
  [PROJECT_ID]:schema_evolution.se_test \
  gs://test-mikami/schema_evolution/bq/case1_evolution.parquet

正常に実行できたようなので、テーブルデータを確認します。

$ bq query --use_legacy_sql=false --location=asia-northeast1 \
  'SELECT * FROM `[PROJECT_ID].schema_evolution.se_test`'
+----+---------------------+---------+------+
| id |        email        |  name   | age  |
+----+---------------------+---------+------+
|  1 | NULL                | Alice   |   25 |
|  2 | NULL                | Bob     |   30 |
|  3 | charlie@example.com | Charlie | NULL |
+----+---------------------+---------+------+

スキーマ情報を持つ Parquet ファイルでは、autodetect 指定無しでスキーマ追従できることが確認できました。

まとめ(所感)

Snowflake でテーブルに ENABLE_SCHEMA_EVOLUTION を指定した場合同様、BigQuery でも bq load コマンドの schema_update_option オプションで ALLOW_FIELD_ADDITIONALLOW_FIELD_RELAXATION を指定することで、スキーマ追従できました。
スキーマ追従機能を利用する際は、意図しないフォーマット不正がスキーマ変更として自動的に取り込まれてしまうリスクもあるため、データ品質チェックと合わせて運用する必要があると思います。
とはいえ、データソースの仕様変更に対して手動メンテナンス不要なスキーマ追従は、運用コストを削減する非常に便利な機能だと思いました。

参考

この記事をシェアする

関連記事