なぜ LOAD DATA INTO は失敗するのに、Python の Load Job は成功するのか
背景
Google Cloud Storage(GCS)に置いた CSV を BigQuery にロードしたいとき、
BigQuery UI、bq load コマンドだと普通に成功するのに、
SQL の LOAD DATA INTO を使うと突然こんなエラーが出ることになった。
Invalid schema update. Field xxxxx has changed type from STRING to INTEGER
もしくは、パーティション付きテーブルに対してこう。
Cannot replace a table with a different partitioning spec.
「え、UI からは成功したのに、SQL だとエラー?」
そんな体験をした人、結構いるはずです。
現場で発生した問題例
以前、Load 処理を SQL で再現しようとした際、
TRUNCATE → INSERT INTO の手順で全量入れ替えを行っていました。
一見同じ「上書き」に見えますが、
TRUNCATE は実際にテーブルのデータを削除してから INSERT を実行するため、
その間に別のワークフローやビューが同じテーブルを参照すると、一時的に空データを返す状態 になります。
この「空白期間」が原因で、一部の処理でデータ欠損 が発生しました。
bq load / Load Job API の WRITE_TRUNCATE はこの問題を解決します。
BigQuery が内部的に一時テーブルを使い、ロード完了後に メタデータレベルで一括スワップ するため、
ロード中にテーブルが空になることはありません。
SQL レイヤーの制約
LOAD DATA INTO は BigQuery の SQL エンジン が直接動かす命令です。
つまり、クエリエンジンの中で “完全にスキーマが一致していること” が前提。
特徴
| 機能 | 挙動 |
|---|---|
| スキーマ自動推定 | ❌ なし(既存テーブルに完全一致が必要) |
| 型不一致 | ❌ エラーになる |
| 分区・クラスタリング | ❌ OVERWRITE は DROP + CREATE 扱いで失敗しやすい |
| スキーマ変更 | ❌ 許されない |
| 処理目的 | データクエリ向け(厳格・安全) |
BigQuery は「SQL はデータ定義を壊さない」という設計思想を持っています。
だから、CSV の列が INT に見えた瞬間、「STRING 型の列を勝手に変えようとしてる!」と怒られるわけです。
API レイヤー(Load Job)の挙動
一方で、bq load コマンド、Python クライアントなどは
BigQuery Load Job API を呼び出しています。
こちらは「データをロードするための専用エンジン」。
特徴
| 機能 | 挙動 |
|---|---|
| スキーマ自動推定 (autodetect=True) | ✅ あり |
| 型不一致 | ✅ 自動変換 or スキップ |
| 分区・クラスタリング | ✅ 保持したまま上書き可 |
| 書き込みモード (WRITE_TRUNCATE, WRITE_APPEND) | ✅ 柔軟に設定可能 |
| 処理目的 | データ投入・ETL 向け(柔軟・現実的) |
つまり テーブルを削除せず、中身だけ空にして再ロード。
分区情報もクラスタリングもそのまま残ります。
挙動比較まとめ
| 項目 | SQL (LOAD DATA INTO) | API (bq load, Python) |
|---|---|---|
| スキーマ自動推定 | × | ○ |
| 型不一致許容 | × | ○ |
| 分区上書き | × | ○ |
| クラスタリング保持 | × | ○ |
| 実行層 | Query Engine | Load Job API |
| 主な用途 | クエリ・定義操作 | データロード・ETL |
Python でのロード例
from google.cloud import bigquery
client = bigquery.Client(project="xxxxxxx")
job_config = bigquery.LoadJobConfig(
source_format=bigquery.SourceFormat.CSV,
field_delimiter=",",
skip_leading_rows=1,
quote_character='"',
allow_quoted_newlines=False,
max_bad_records=0,
ignore_unknown_values=False,
autodetect=True,
write_disposition="WRITE_TRUNCATE", # 「上書き」モード
)
uri = "gs://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx*.csv"
table_id = "xxxxxxx.xxxxxxx.xxxxxxxx"
load_job = client.load_table_from_uri(uri, table_id, job_config=job_config)
load_job.result()
print(f"Loaded {load_job.output_rows} rows to {table_id}.")
これで、スキーマの自動検出もされ、分区設定も壊れません。
まとめ
- LOAD DATA INTO は SQL 層。スキーマ厳格・安全重視。
- bq load / Python は API 層。ETL 向け・柔軟性重視。
- 同じ「ロード」でも目的と制約が全く違う。
- データパイプラインで安定してロードしたいなら、API(Python / CLI)を使うのが正解。