
BigQuery の APPENDS / CHANGES 関数でテーブルの変更履歴を追ってみた
こんにちは!エノカワです。
BigQuery で「このテーブル、さっきまでどんな中身だったか」を確認したいとき、よく使うのが Time travel です。
FOR SYSTEM_TIME AS OF で過去のある時点のテーブル全体を参照できるので、誤って UPDATE / DELETE してしまったデータの復旧にはとても便利です。
一方で、当時の全件ではなく、その期間に変化した行だけ を追いたいこともあります。
2026 年 7 月 27 日、その用途向けの APPENDS / CHANGES 変更履歴関数 が GA となりました。
You can use the APPENDS and CHANGES change history functions to view the rows that were appended to or changed in a table during a given time range. This feature is generally available (GA).
SQL でテーブルの変更行を読む関数です。
履歴の確認にも使えますし、その結果を増分処理の材料にもできます。
テーブルが更新されたタイミングでジョブを自動起動する機能ではない点は、最初に押さえておくとよさそうです。
今回は、日本語のタスク管理データを使って、INSERT → UPDATE → DELETE のあとに両方の関数を試してみましたので、その内容をご紹介します。
APPENDS と CHANGES の違い
Time travel が「ある時点のテーブル全体」なのに対して、こちらは「指定期間に起きた変更行」です。
公式ドキュメントの内容を対比すると次のとおりです。
| 観点 | Time travel | APPENDS | CHANGES |
|---|---|---|---|
| 見えるもの | ある時点のテーブル全体 | 期間内に追加された行 | 期間内に追加・更新・削除された行 |
_CHANGE_TYPE |
なし | INSERT のみ |
INSERT / UPDATE / DELETE |
| 事前設定 | 不要 | 不要 | テーブル の enable_change_history=TRUE |
| 保持期間 | time travel 窓(標準 7 日) | 同左 | 同左 |
入力テーブルの列に加えて、次の列が付きます。
_CHANGE_TYPE… 変更の種類_CHANGE_TIMESTAMP… その変更をコミットした時刻_CHANGE_IS_FOR_UPDATE…CHANGESのみ。更新由来の DELETE ならTRUE
enable_change_history はプロジェクトやデータセットではなく、テーブル単位 の OPTIONS です。
CREATE TABLE か ALTER TABLE ... SET OPTIONS で指定します。
Time travel の保持期間はデータセット設定(2〜7 日)なので、「何日残すか」と「CHANGES 用メタデータを取るか」は別レイヤでした。
構文は次のとおりです。
TABLE キーワードは必須です。
end_timestamp は排他で、NULL にするとクエリ開始時点までが含まれます。
SELECT *
FROM APPENDS(TABLE dataset.table, start_timestamp, end_timestamp);
SELECT *
FROM CHANGES(TABLE dataset.table, start_timestamp, end_timestamp);
今回やること
小さなタスク管理テーブルを作り、次の順で操作します。
- タスクを 3 件 INSERT する
- 1 件のステータスを UPDATE する
- 1 件を DELETE する
- 同じテーブルに対して
APPENDSとCHANGESを実行し、結果を比べる
環境準備
BigQuery でデータセットを作成できる権限があれば十分です。
変更履歴の参照には bigquery.tables.getData(roles/bigquery.dataViewer など)が必要です。
検証用のデータセットを作成します。
CREATE SCHEMA IF NOT EXISTS change_history_demo;
CHANGES を使うので、テーブル作成時から enable_change_history=TRUE にしました。
後から有効化すると、有効化前の履歴が不完全になることがあるためです。
CREATE TABLE change_history_demo.tasks (
task_id INT64,
title STRING,
status STRING,
assignee STRING
)
OPTIONS(enable_change_history = TRUE);
作成後、コンソールの 詳細 タブと スキーマ / スキーマを編集 を見たのですが、enable_change_history は出てきませんでした。
有効かどうかは INFORMATION_SCHEMA.TABLES の is_change_history_enabled で確認できました。
SELECT
table_name,
is_change_history_enabled
FROM
`change_history_demo.INFORMATION_SCHEMA.TABLES`
WHERE
table_name = 'tasks';
今回の結果は次のとおりでした。
| table_name | is_change_history_enabled |
|---|---|
| tasks | YES |

APPENDS だけならこのオプションは不要です。
今回は両方を同じテーブルで試すため、最初から有効にしています。
データを投入・更新・削除する
まず 3 件のタスクを追加します。
INSERT INTO change_history_demo.tasks (task_id, title, status, assignee)
VALUES
(1, '請求書の確認', '未着手', '山田'),
(2, '週次レポート作成', '未着手', '佐藤'),
(3, '障害対応の振り返り', '進行中', '鈴木');
次に、タスク 1 のステータスを更新します。
UPDATE change_history_demo.tasks
SET status = '完了'
WHERE task_id = 1;
最後に、タスク 2 を削除します。
DELETE FROM change_history_demo.tasks
WHERE task_id = 2;
現在のテーブルは次のようになりました。
削除した「週次レポート作成」は見えず、タスク 1 は完了済みです。
SELECT * FROM change_history_demo.tasks
ORDER BY task_id;
| task_id | title | status | assignee |
|---|---|---|---|
| 1 | 請求書の確認 | 完了 | 山田 |
| 3 | 障害対応の振り返り | 進行中 | 鈴木 |

ここからが本題です。
同じ操作のあと、APPENDS と CHANGES でどう見えるかを比べます。
APPENDS を試す
追加履歴だけを見ます。
テーブル作成直後なので、time travel 窓内の全件を取る意図で NULL, NULL を指定しました。
SELECT
task_id,
title,
status,
assignee,
_CHANGE_TYPE AS change_type,
_CHANGE_TIMESTAMP AS change_time
FROM APPENDS(TABLE change_history_demo.tasks, NULL, NULL)
ORDER BY change_time, task_id;
結果は次のとおりでした。
_CHANGE_TYPE はすべて INSERT です。
| task_id | title | status | assignee | change_type | change_time |
|---|---|---|---|---|---|
| 1 | 請求書の確認 | 未着手 | 山田 | INSERT | 2026-08-28 09:21:22.345000 UTC |
| 2 | 週次レポート作成 | 未着手 | 佐藤 | INSERT | 2026-08-28 09:21:22.345000 UTC |
| 3 | 障害対応の振り返り | 進行中 | 鈴木 | INSERT | 2026-08-28 09:21:22.345000 UTC |

やってみて分かったのは、次の 2 点です。
- タスク 1 の
statusは INSERT 当時の「未着手」 のままです。その後の UPDATE はAPPENDSに出ませんでした - すでに DELETE したタスク 2 も、当時 INSERT された行として残っていました
「期間内に何が追加されたか」は取れますが、更新後の値や削除そのものは見えませんでした。
CHANGES を試す
同じテーブルに対して CHANGES を実行します。
SELECT
task_id,
title,
status,
assignee,
_CHANGE_TYPE AS change_type,
_CHANGE_TIMESTAMP AS change_time,
_CHANGE_IS_FOR_UPDATE AS change_is_for_update
FROM CHANGES(TABLE change_history_demo.tasks, NULL, NULL)
ORDER BY change_time, task_id, change_type;
結果は次のとおりでした。
UPDATE は「旧行の DELETE + 新行の UPDATE」として 2 行出てきました。
| task_id | title | status | assignee | change_type | change_time | change_is_for_update |
|---|---|---|---|---|---|---|
| 1 | 請求書の確認 | 未着手 | 山田 | INSERT | 2026-08-28 09:21:22.345000 UTC | false |
| 2 | 週次レポート作成 | 未着手 | 佐藤 | INSERT | 2026-08-28 09:21:22.345000 UTC | false |
| 3 | 障害対応の振り返り | 進行中 | 鈴木 | INSERT | 2026-08-28 09:21:22.345000 UTC | false |
| 1 | 請求書の確認 | 未着手 | 山田 | DELETE | 2026-08-28 09:22:24.295000 UTC | true |
| 1 | 請求書の確認 | 完了 | 山田 | UPDATE | 2026-08-28 09:22:24.295000 UTC | false |
| 2 | 週次レポート作成 | 未着手 | 佐藤 | DELETE | 2026-08-28 09:25:07.016000 UTC | false |

行を更新すると、次の 2 レコードが並びます。
- 更新前の行 …
_CHANGE_TYPE = DELETEかつ_CHANGE_IS_FOR_UPDATE = TRUE - 更新後の行 …
_CHANGE_TYPE = UPDATE
本物の DELETE(タスク 2)は _CHANGE_IS_FOR_UPDATE = FALSE でした。
「消えたように見える行」が、本当の削除なのか UPDATE の片割れなのかは、このフラグで区別できました。
使い分けの指針
両方を同じテーブルで動かしてみて、自分なりに整理した使い分けは次のとおりです。
- 追加された行だけを拾いたい(ログの取り込み、追記型テーブルの増分同期)→
APPENDS。事前設定なしで、既存テーブルにもそのまま使える - 更新・削除まで追いたい(マスタ系の増分同期、「いつどの行が変わったか」の調査)→
CHANGES。ただしテーブル単位の事前設定が必要 - ある時点の全体を見たい(誤操作からの復旧、当時の件数の確認)→ 従来どおり time travel
CHANGES は後から有効化しても、有効化前の履歴はさかのぼれません。
更新・削除も追う可能性があるテーブルは、作成時点で enable_change_history=TRUE にしておくほうが安全だと感じました。
まとめ
以上、GA になった BigQuery の変更履歴関数 APPENDS / CHANGES を、小さなタスク管理テーブルで試してみました。
実際に動かしてみると、Time travel との違いは「全体を見るか、変わった行を見るか」で整理でき、思っていたよりシンプルでした。
APPENDS は追加された行だけが、INSERT 当時の値のまま残ります。
CHANGES は更新前後の行が並んで返ってくるので、「いつどの行がどう変わったか」を SQL だけで追えるのが便利だと感じました。
一方で、CHANGES はテーブル単位で enable_change_history=TRUE にしておく必要があり、後からではさかのぼれません。
指定できる期間は最大 1 日で、保持も time travel 窓(標準 7 日)に依存します。
関数呼び出しは指定期間の書き込みを処理するため、コンピュート課金も念頭に置いておくとよさそうです。
テーブルの変更を行単位で追いたい方は、ぜひ触ってみてはいかがでしょうか?



