- DML 各文(INSERT / UPDATE / DELETE / MERGE / TRUNCATE)の構文と使い分け、バルク変更としての課金・同時実行の考え方を理解する
- MERGE 文で CDC・upsert パターンを実装できる
- タイムトラベル(
FOR SYSTEM_TIME AS OF)と fail-safe の違いを理解し、誤操作から復旧できる - テーブルスナップショットとテーブルクローンを使い分け、バックアップ・開発環境複製に応用できる
- 変更履歴(
APPENDS/CHANGES)でテーブルの変更を追跡できる
前提: 課金プロジェクト必須(概算 1 円未満。数 KB 規模のテーブルのみを扱うため、オンデマンドの無料枠「月 1 TiB のクエリ処理まで無料」に確実に収まります)。サンドボックスでは DML 文自体が実行できないため、この章だけは課金を有効化したプロジェクトが必要です。CTAS や基本的な SELECT 文は 第 4 章 を前提とします。
DML とバルク変更のコスト
BigQuery の DML(データ操作言語)は INSERT / UPDATE / DELETE / MERGE / TRUNCATE TABLE の 5 種類です。いずれも GA(一般提供)で、GoogleSQL 方言で書きます。TRUNCATE TABLE は全行を削除しつつスキーマ・説明・ラベルなどのメタデータは保持するので、全件削除だけが目的なら DELETE より効率的です。
| 文 | 用途 | 課金の考え方 |
|---|---|---|
INSERT | 新規行の追加 | スキャン対象(SELECT 部分やリテラル評価分)に課金 |
UPDATE | 既存行の更新(FROM 句で他テーブルと結合可) | WHERE 条件・結合対象のスキャン分に課金 |
DELETE | 条件に合う行の削除(WHERE 必須) | WHERE 条件のスキャン分に課金 |
MERGE | INSERT・UPDATE・DELETE をアトミックに統合 | ソース・ターゲット双方のスキャン分に課金 |
TRUNCATE TABLE | 全行削除(メタデータは保持) | 他 DML よりコストが小さいことが多い |
DML はいずれもスキャンしたバイト数に応じて課金される点で SELECT クエリと同じです。パーティション化されたテーブルに対する UPDATE / DELETE は、パーティションキーを WHERE 句に含めれば対象パーティションだけを書き換えるので、スキャン量を大きく抑えられます(パーティショニングの詳細は 第 6 章)。
同時実行数にも文の種類で違いがあります。INSERT は 24 時間で先着 1,500 ステートメントまで即座に実行され、それ以降は同時 10・キュー最大 100 まで受け付けます。一方 UPDATE / DELETE / MERGE は「ミューテーティング DML」として扱われ、同一テーブルに対して同時実行できるのは最大 2 件、キューは最大 20 件までです(2026-08 時点)。
MERGE による Upsert
MERGE はソーステーブルとターゲットテーブルを突き合わせ、行ごとに次の 3 パターンへ振り分けて 1 つのアトミックな操作として実行します。CDC(変更データキャプチャ)を反映する際の基本形です。
MERGE [INTO] target_name [[AS] alias]
USING source_name [[AS] alias]
ON merge_condition
WHEN MATCHED [AND search_condition] THEN
UPDATE SET ... | DELETE
WHEN NOT MATCHED [BY TARGET] [AND search_condition] THEN
INSERT (...) VALUES (...)
WHEN NOT MATCHED BY SOURCE [AND search_condition] THEN
UPDATE SET ... | DELETE
WHEN NOT MATCHED BY TARGET(ソースにしかない行)では列名を省略すると、ターゲットテーブルの全列が序数の昇順で対象になります。ただし取り込み時刻パーティションテーブルが対象の場合は列名の明示が必須です。重要な制約として、WHEN MATCHED 句で UPDATE を行う場合、複数のソース行が同一のターゲット行にマッチしているとランタイムエラーになります。ステージングテーブル側のキー重複が典型的な原因です。
マルチステートメントトランザクションとの関係
BEGIN TRANSACTION / COMMIT TRANSACTION / ROLLBACK TRANSACTION を使うと、複数の DML をまとめてアトミックに実行できます(スナップショット分離)。トランザクション内では CURRENT_TIMESTAMP() はトランザクション開始時刻を返し、変更できるテーブルは最大 100、パーティション変更は最大 100,000 までという上限があります。永続オブジェクトを作成・削除する DDL はトランザクション内では使えません。単一クエリで完結させる場合はこの構文で十分ですが、複数クエリにまたがる一連の処理は次章以降で扱うスクリプトやセッションで組み立てます。
タイムトラベルと fail-safe
タイムトラベルは、テーブルの過去の状態を FOR SYSTEM_TIME AS OF 句で読み出せる仕組みです。既定の保持期間は 7 日間で、データセット単位(または ALTER SCHEMA によるプロジェクト既定値)で 2 日から 7 日の範囲に設定できます(2026-08 時点)。
-- タイムトラベル保持期間を変更する(48〜168 の 24 の倍数で指定)
ALTER SCHEMA `PROJECT_ID.handson_ch09`
SET OPTIONS (max_time_travel_hours = 48);
-- 過去の状態を読み出す
SELECT *
FROM `PROJECT_ID.handson_ch09.orders`
FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 10 MINUTE);
タイムトラベル期間が終了すると、続けて fail-safe 期間が始まります。fail-safe は削除・変更されたデータをさらに 7 日間、BigQuery 内部に保持する仕組みですが、この 7 日間は固定でユーザーが設定を変更することはできず、FOR SYSTEM_TIME AS OF でユーザーが直接読み出すこともできません(データ復旧には Google Cloud のサポートが必要です)。物理ストレージ課金モデルではタイムトラベルと fail-safe 用のストレージにも課金されますが、論理ストレージモデルでは基本料金に含まれます。
スナップショット・クローン・変更履歴
テーブルスナップショットとテーブルクローンは、どちらも「ある時点のテーブルのコピー」を作る機能ですが、書き込み可否と課金モデルが異なります。
| 観点 | テーブルスナップショット | テーブルクローン |
|---|---|---|
| 書き込み | 読み取り専用 | 書き込み可能(標準テーブルと同様に操作可) |
| 課金 | 差分課金。基本テーブルと異なるバイト分のみ課金され、複数スナップショットがある場合は最古のものがユニークなデータ分の費用を負担 | 作成時点ではストレージ費用ゼロ。変更・追加した差分のみ課金 |
| 用途 | 特定時点の凍結コピー(バックアップ) | 書き換えて試せる複製(開発・検証環境) |
| 制約 | 基本テーブルと同一リージョン・同一組織のみ、ビュー不可 | 同上。カラム型ストレージのため小さな変更でもストレージ増加が大きくなりうる |
-- スナップショットを作る(過去の時点を指定することも可能)
CREATE SNAPSHOT TABLE `PROJECT_ID.handson_ch09.prices_snapshot`
CLONE `PROJECT_ID.handson_ch09.prices`
OPTIONS (expiration_timestamp = TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 1 DAY));
-- クローンを作る
CREATE TABLE `PROJECT_ID.handson_ch09.prices_clone`
CLONE `PROJECT_ID.handson_ch09.prices`;
-- スナップショットから標準テーブルへ復元する(上書きする場合は CREATE OR REPLACE)
CREATE OR REPLACE TABLE `PROJECT_ID.handson_ch09.prices`
CLONE `PROJECT_ID.handson_ch09.prices_snapshot`;
変更履歴を追う(APPENDS / CHANGES)
APPENDS(TABLE table, start_timestamp, end_timestamp) は追加された行だけを、CHANGES(TABLE table, start_timestamp, end_timestamp) は追加・更新・削除のすべてを返すテーブル値関数です。APPENDS は常に使えますが、CHANGES を使うにはテーブル作成時(または後から)に enable_change_history = TRUE を設定する必要があり、この設定はメタデータ保存コストの増加を伴います。どちらも戻り値に _CHANGE_TYPE(INSERT / UPDATE / DELETE)と _CHANGE_TIMESTAMP 列が付与され、CHANGES はさらに更新由来の削除かどうかを示す _CHANGE_IS_FOR_UPDATE 列を持ちます。なお CHANGES は 1 回の呼び出しで指定できる期間(start_timestamp〜end_timestamp)が最大 1 日という上限があるため、長い期間を追うには呼び出しを日単位に分割します。
CREATE TABLE `PROJECT_ID.handson_ch09.events` (event_id INT64, status STRING)
OPTIONS (enable_change_history = true);
SELECT event_id, status, _CHANGE_TYPE, _CHANGE_TIMESTAMP
FROM CHANGES(TABLE `PROJECT_ID.handson_ch09.events`, NULL, CURRENT_TIMESTAMP())
ORDER BY _CHANGE_TIMESTAMP;
演習用データセット handson_ch09(ロケーション US)と 5 行だけのテーブルを作り、条件を書き間違えた DELETE の事故を FOR SYSTEM_TIME AS OF で復旧します。課金プロジェクト必須(数 KB 規模・オンデマンド無料枠内)。所要時間の目安は 10 分です。
- データセットとテーブルを作る。
PROJECT_IDは自分のプロジェクト ID に置き換えてください。
# bq CLI でデータセットを作成
bq mk --location=US --dataset PROJECT_ID:handson_ch09
CREATE TABLE `PROJECT_ID.handson_ch09.orders` AS
SELECT * FROM UNNEST([
STRUCT(1 AS order_id, 'JP' AS region, 120 AS amount),
STRUCT(2, 'JP', 340),
STRUCT(3, 'US', 500),
STRUCT(4, 'JP', 90),
STRUCT(5, 'US', 220)
]);
- 「order_id = 4 のテスト行だけ消すつもりが、region で書いてしまった」という典型的な事故を再現する。
-- 本来 WHERE order_id = 4 のつもりが、region で条件を書いてしまった
DELETE FROM `PROJECT_ID.handson_ch09.orders`
WHERE region = 'JP';
SELECT COUNT(*) FROM `PROJECT_ID.handson_ch09.orders` を実行すると 5 行のはずが 2 行に減っています(JP の 3 行が消えた)。
- 削除前の状態をタイムトラベルで確認する。削除を実行してからの経過時間に合わせて
INTERVALの分数を調整してください。
SELECT *
FROM `PROJECT_ID.handson_ch09.orders`
FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 5 MINUTE)
ORDER BY order_id;
5 行すべて(削除された JP の 3 行を含む)が返れば、この時点はまだ削除前の状態です。
- 消えた行だけを復旧する。
INSERT INTO `PROJECT_ID.handson_ch09.orders`
SELECT *
FROM `PROJECT_ID.handson_ch09.orders`
FOR SYSTEM_TIME AS OF TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 5 MINUTE)
WHERE region = 'JP';
再度 COUNT(*) を実行し、5 行に戻っていることを確認します。
後片付け:
bq rm -f -t PROJECT_ID:handson_ch09.orders
ターゲットテーブルとステージングテーブルを作り、MERGE で「既存 SKU は更新・新規 SKU は追加」する CDC の基本形を体験します。課金プロジェクト必須(数 KB 規模)。所要時間の目安は 8 分です。
- ターゲットとステージングのテーブルを作る。
CREATE TABLE `PROJECT_ID.handson_ch09.inventory` AS
SELECT * FROM UNNEST([
STRUCT('sku-001' AS sku, 100 AS stock, 'active' AS status),
STRUCT('sku-002', 50, 'active'),
STRUCT('sku-003', 0, 'discontinued')
]);
CREATE TABLE `PROJECT_ID.handson_ch09.inventory_staging` AS
SELECT * FROM UNNEST([
STRUCT('sku-002' AS sku, 30 AS stock, 'active' AS status),
STRUCT('sku-004', 75, 'active')
]);
- MERGE で反映する。
sku-002は在庫更新、sku-004は新規追加になります。
MERGE `PROJECT_ID.handson_ch09.inventory` T
USING `PROJECT_ID.handson_ch09.inventory_staging` S
ON T.sku = S.sku
WHEN MATCHED THEN
UPDATE SET stock = S.stock, status = S.status
WHEN NOT MATCHED BY TARGET THEN
INSERT (sku, stock, status) VALUES (S.sku, S.stock, S.status);
SELECT * FROM `PROJECT_ID.handson_ch09.inventory` ORDER BY sku の結果は次のようになります。
| sku | stock | status |
|---|---|---|
| sku-001 | 100 | active(変更なし) |
| sku-002 | 30 | active(UPDATE で更新) |
| sku-003 | 0 | discontinued(変更なし) |
| sku-004 | 75 | active(INSERT で新規追加) |
後片付け:
bq rm -f -t PROJECT_ID:handson_ch09.inventory
bq rm -f -t PROJECT_ID:handson_ch09.inventory_staging
小さい価格テーブルからスナップショットとクローンを作り、ベーステーブルへの一括 UPDATE がそれぞれにどう影響するかを確認します。課金プロジェクト必須(数 KB 規模)。所要時間の目安は 10 分です。
- ベーステーブルを作り、リスクのある一括更新をする前にスナップショットとクローンを両方作っておく。
CREATE TABLE `PROJECT_ID.handson_ch09.prices` AS
SELECT * FROM UNNEST([
STRUCT('A' AS item, 1000 AS price),
STRUCT('B', 2000)
]);
CREATE SNAPSHOT TABLE `PROJECT_ID.handson_ch09.prices_snapshot`
CLONE `PROJECT_ID.handson_ch09.prices`
OPTIONS (expiration_timestamp = TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 1 DAY));
CREATE TABLE `PROJECT_ID.handson_ch09.prices_clone`
CLONE `PROJECT_ID.handson_ch09.prices`;
- 「全商品の価格を 2 倍にするつもりが、条件を外してしまった」事故を再現する。
UPDATE `PROJECT_ID.handson_ch09.prices`
SET price = price * 2
WHERE TRUE;
ベーステーブルは A=2000, B=4000 になりますが、prices_snapshot は作成時点の A=1000, B=2000 のまま固定されています(読み取り専用)。prices_clone も同じく A=1000, B=2000 のままです(ベースとは独立しているため、ベース側の変更は伝播しません)。
- クローンは書き込み可能なので、クローン側だけ個別に更新できることを確認する。
UPDATE `PROJECT_ID.handson_ch09.prices_clone`
SET price = price + 100
WHERE item = 'A';
クローンは A=1100 になりますが、ベーステーブルの A=2000 には一切影響しません。
- スナップショットから復元して、誤った一括更新をなかったことにする。
CREATE OR REPLACE TABLE `PROJECT_ID.handson_ch09.prices`
CLONE `PROJECT_ID.handson_ch09.prices_snapshot`;
ベーステーブルが A=1000, B=2000 に戻ります。この章のハンズオンで作ったリソースは以上のため、データセットごと削除します。
後片付け:
bq rm -r -f -d PROJECT_ID:handson_ch09
つまずきポイント
WHERE 句の条件を書き間違えると、この章のハンズオンのように意図しない範囲が変更・削除されます。本番前提のテーブルでは必ず dry run(第 7 章)で影響範囲を見積もり、小さいデータで一度試してから流してください。
max_time_travel_hours を最小の 48(2 日)に設定するとストレージコストは下がりますが、事故に気づくのが 3 日後だと FOR SYSTEM_TIME AS OF では手が届きません。fail-safe はユーザーがクエリで読める領域ではなく、サポートに依頼する最終手段です。
WHEN MATCHED 句で UPDATE を行う MERGE は、ソース側に同じキーの行が複数あるとランタイムエラーになります。ステージングテーブルを作る ETL 側で主キーの重複がないことを事前に保証してください。
APPENDS と CHANGES はどちらもマルチステートメントトランザクションの中からは呼び出せません。
理解度チェック
Q1. タイムトラベルと fail-safe の違いは?
タイムトラベルは既定 7 日(2〜7 日で設定可能)の窓で、FOR SYSTEM_TIME AS OF によりユーザーが自分で過去の状態を読み出せます。fail-safe はタイムトラベル終了後にさらに 7 日間(固定・変更不可)データを内部保持する仕組みで、ユーザーが直接クエリで読み出すことはできず、復旧にはサポートへの問い合わせが必要です。
Q2. テーブルスナップショットとテーブルクローンの主な違いは?
スナップショットは読み取り専用で、差分課金(最古のスナップショットがユニークなデータ分のストレージ費用を負担)です。クローンは書き込み可能で、作成時点ではストレージ費用がゼロ、その後の変更・追加分だけが課金されます。
Q3. MERGE 文でランタイムエラーになる典型的な原因は?
WHEN MATCHED 句で UPDATE(または DELETE)を行う場合に、複数のソース行が同一のターゲット行にマッチしていることです。ステージングテーブル側のキー重複が典型的な原因です。
Q4.(シナリオ)夜間バッチの UPDATE を間違え、対象テーブルの大量の行の status 列を誤った値で上書きしてしまった。データセットのタイムトラベル保持期間は既定の 7 日のままで、事故に気づいたのは実行の 10 分後である。今すぐ取るべき行動は?
A. fail-safe に直接クエリを投げて復旧する B. FOR SYSTEM_TIME AS OF で誤った UPDATE 前の状態を読み出し、正しい値に戻す C. スナップショットを事前に作っていないので復旧は不可能とあきらめる D. Google Cloud サポートにチケットを起票し、7 日間の応答を待つ
B。 事故の 10 分後ならタイムトラベル窓(7 日間)の中なので、FOR SYSTEM_TIME AS OF で UPDATE 前の状態を読み出し、正しい値を作り直せます。A は誤り: fail-safe はユーザーが直接クエリで読める領域ではありません。C は誤り: スナップショットがなくてもタイムトラベルだけで復旧できます。D は誤り: タイムトラベル窓内で自分で即座に復旧できるのに、7 日待つ必要はありません。
Q5.(シナリオ)本番データセットの検証環境を素早く複製し、複製後は検証環境側だけで自由に UPDATE・DELETE を試したい。複製直後のストレージ費用は極力抑えたい場合、最も適した機能はどれか?
A. テーブルスナップショット B. テーブルクローン C. CREATE TABLE AS SELECT による物理コピー D. bq cp によるテーブルコピージョブ
B。 テーブルクローンは書き込み可能で、作成時点ではストレージ費用がかからず、変更した差分だけに課金されるため、要件(書き込み可能・低コストな複製)に合致します。A は誤り: スナップショットは読み取り専用なので UPDATE・DELETE ができません。C・D は誤り: どちらも複製した瞬間から全データ分のストレージ費用が発生する物理コピーです。
まとめと次章へ
- DML はバルク変更向けで、INSERT は緩やかだが UPDATE・DELETE・MERGE は同時 2 件までしか走らない
- MERGE は WHEN MATCHED / NOT MATCHED BY TARGET / NOT MATCHED BY SOURCE で 1 行ずつ INSERT・UPDATE・DELETE に振り分ける upsert の基本形
- タイムトラベル(既定 7 日、2〜7 日で設定可)は自分で読み出せる窓、fail-safe(固定 7 日)はサポート経由の最終手段
- スナップショットは読み取り専用の凍結コピー、クローンは書き込み可能で独立したコピー。差分課金の考え方は同じ
APPENDS(常時利用可)とCHANGES(enable_change_historyが必要)でテーブルの変更履歴を追える
次章では、BigLake・Iceberg やフェデレーションを使って BigQuery の外側にあるデータを扱う方法を学びます(第 10 章)。