はじめに
連載「Genie Codeと学ぶDatabricks」の第3回です。
- 入門編: 作らせて読む学習法
- Unity Catalog編: 3階層名前空間を体で覚える
- Delta Lake編: わざと事故を起こして、タイムトラベルで戻す (本記事)
前回、DESCRIBE EXTENDEDの出力で「Provider: delta」という行を見つけて、「そういう形式で保存されているらしい」で素通りしました。今回はその正体を確かめます。追記し、わざと誤ったデータを混入させ、タイムトラベルで調査して、元に戻す。Delta Lakeの説明でよく見る「ACIDトランザクション」や「タイムトラベル」という言葉を、機能一覧の項目としてではなく、事故対応の実体験として覚えるのが今回のゴールです。
環境はDatabricks Free Editionで、前回作った workspace.tpch_learning.orders_1998 (668,101件) をそのまま実験台にします。まだ作っていない方は、前回の記事の2本目のプロンプトを流せば1分で用意できます。
Delta Lakeとは何者か
Delta Lakeは、レイクハウスの基盤となるストレージレイヤーです。実体はParquetというカラムナ形式のデータファイルなのですが、それに加えてトランザクションログを持っているのが本質です。テーブルへのすべての変更 (作成、追記、上書き、削除) がログにバージョンとして記録され、これによってACIDトランザクションと、過去の任意のバージョンへのアクセス (タイムトラベル) が可能になります。
前回、カタログエクスプローラーの詳細タブに delta.lastUpdateVersion=0 のようなプロパティがずらりと並んでいたのを覚えているでしょうか。あれがトランザクションログの存在の痕跡です。「ファイルではなくテーブルで考えてよい」のは、この仕組みが下で支えてくれているからです。
説明はこのくらいにして、確かめに行きます。
まず、履歴を見る
最初のステップは、何かを変更することではなく、いま履歴がどうなっているかを見ることです。フルページ型のGenie Codeに依頼します。
workspace.tpch_learning.orders_1998テーブルのDelta Lakeとしての履歴を
確認するノートブックを作成してください。
- DESCRIBE HISTORYで変更履歴を表示する
- 各セルの前に、結果が何を意味するかをコメントで説明する
- テーブル名は3階層で指定する
興味深いことに、生成されたノートブックは依頼した3項目を超える6セル構成でした。DESCRIBE HISTORYに加えて、DESCRIBE DETAILで現在のサイズやファイル数を見るセル、VERSION AS OFの使用例、果ては操作タイプの分布をPlotlyで可視化するセルまで付いてきます。しかも履歴の各カラム (version、operation、readVersion、isBlindAppendなど) の意味が日本語コメントで説明されている。頼んだ以上のものが返ってくるのは学習素材としてはお得ですが、裏を返せば「依頼していないコード」も実行することになるわけで、実行前に全セルに目を通すという原則はここでも効いてきます。
実行すると、履歴には1行だけ入っているはずです。バージョン0として、前回実行した操作がoperation列に「CREATE OR REPLACE TABLE AS SELECT」として、実行者やタイムスタンプ、操作パラメータとともに記録されています。自分では履歴を記録する操作を何もしていないのに、テーブルへの書き込みが最初から全部記録されている。これがトランザクションログです。前回のリネージと同じ構図で、Delta Lakeでも「記録は勝手に取られている」が基本になります。
わざと事故を起こす
次に、実務で一番よくある事故を再現します。「対象を間違えて、関係ないデータを追記してしまった」というやつです。
workspace.tpch_learning.orders_1998テーブルに対する誤操作を
再現するノートブックを作成してください。
- samples.tpch.ordersから1997年の注文を抽出し、INSERT INTOで
orders_1998に追記する (誤操作のシミュレーションです)
- 追記後の総件数と、o_orderdateの年ごとの件数を表示する
- DESCRIBE HISTORYで履歴がどう増えたかを確認する
- テーブル名はすべて3階層で指定する
orders_1998という名前のテーブルに1997年のデータを流し込むわけです。名前と中身が食い違う、嫌な状態を意図的に作ります。生成されたノートブックは、冒頭のMarkdownセルに「これはデモ目的のシミュレーションです」という注意書きまで付けた律儀なものでした。
ここで予定外の見どころがありました。Genie Codeが最初に生成した状態確認クエリ (総件数と年別件数をUNION ALLでまとめるもの) が実行エラーになったのです。するとGenie Codeはエラーメッセージを読んで問題点を3つに整理し、修正して再実行、それでもORDER BYの型エラーが出ると、さらにアプローチを変えてもう一度修正し、最終的に完走させました。第1回で「エラーが出たら自分で診断して修正するところまで自律的に回る」と書きましたが、その挙動を目の前で見たのは初めてです。人間は途中で一度も手を出していません。
肝心の実行結果です。1997年分として1,137,325件が追記され、総件数は668,101件から1,805,426件になりました。年別件数の表示が分かりやすく事故を物語っています。
| order_year | order_count | earliest_date | latest_date |
|---|---|---|---|
| 1997 | 1,137,325 | 1997-01-01 | 1997-12-31 |
| 1998 | 668,101 | 1998-01-01 | 1998-08-02 |
1997年は丸1年分あるのに対して、1998年は8月2日まで。第1回でグラフの末尾が急落して見えた「部分月問題」の物証がこんなところで取れました。
そしてDESCRIBE HISTORYには、バージョン1として operation: WRITE が積まれています。operationParametersを見ると mode: Append、さらに isBlindAppend: true、つまり既存データには一切触れない純粋な追記だったことまで記録されています。事故の内容が、誰が・いつ・どのモードで、まで残っている。ここで重要なのは、事故の後でも慌てる必要がないと分かっていることです。バージョン0という「事故前の状態」が、履歴の中にそのまま残っています。
タイムトラベルで調査して、戻す
復旧に入ります。ポイントは、いきなり戻すのではなく、まず戻り先を確認することです。
workspace.tpch_learning.orders_1998テーブルを、タイムトラベルで
調査して復旧するノートブックを作成してください。
- 現在の件数と、VERSION AS OF 0を指定した時点の件数を比較する
- RESTORE TABLEでバージョン0に戻す
- 復旧後の件数を確認し、DESCRIBE HISTORYでRESTOREが
どう記録されたかを確認する
- テーブル名はすべて3階層で指定する
生成されたコードの中で主役になるのは、この2つの構文です。
-- 過去のバージョンを「見る」
SELECT COUNT(*) FROM workspace.tpch_learning.orders_1998 VERSION AS OF 0;
-- 過去のバージョンに「戻す」
RESTORE TABLE workspace.tpch_learning.orders_1998 TO VERSION AS OF 0;
VERSION AS OFはテーブルを変更せずに過去を覗く読み取り専用の構文なので、何度実行しても安全です。まずこれで「バージョン0は本当に668,101件か」を確認してから、RESTOREで戻す。事故対応の鉄則である「調査と復旧を分ける」が、そのまま構文のレベルで分かれています。
生成されたノートブックは今回も仕様以上で、現在とバージョン0の年別件数をPlotlyの棒グラフで並べる比較セルまで付いてきました。実行するとサマリはこうなります。現在の総件数1,805,426件、バージョン0の総件数668,101件、差分1,137,325件。差分が先ほど誤追記した件数と厳密に一致することを確認できた時点で、戻してよいという確信が持てます。
もう一つ細かいことですが、RESTOREを実行するセルには「注意: これは実際にテーブルを変更します」というコメントをGenie Code自身が付けていました。読み取りと書き込みの区別がプロンプトで頼まなくても意識されているのは、安心材料です。
RESTOREを実行すると、件数は668,101件に戻り、年別件数からも1997年の行が消えます。そしてDESCRIBE HISTORYを見ると、RESTORE自体がバージョン2として積まれていることが分かります。最終的な履歴はこの3行です。
| version | operation |
|---|---|
| 2 | RESTORE |
| 1 | WRITE |
| 0 | CREATE OR REPLACE TABLE AS SELECT |
作って、事故って、戻した。今回の物語がそのまま履歴に残っています。RESTOREのoperationParametersには {'version': '0'} と復元先まで記録されていました。履歴が「巻き戻された」のではなく、「バージョン0の状態に戻すという操作」が履歴の先頭に追加された。つまり誤追記があった事実も履歴には残り続けます。監査の観点では、これは望ましい挙動です。
CREATE OR REPLACEの正体
ここまで来ると、前回積み残した伏線が回収できます。前回、Genie Codeが生成した CREATE OR REPLACE TABLE について「同名のテーブルが既にあれば黙って置き換える。REPLACEされた側は本当に消えるのか」と書きました。
答えはもう見えています。REPLACEも履歴に積まれる操作の一つです。置き換えられた側の状態は前のバージョンとして残り、VERSION AS OFで参照でき、RESTOREで戻せます。「黙って置き換える」の実態は「黙って新しいバージョンを積む」でした。
とはいえ、これは「REPLACEしても安全」という意味ではありません。後述するように過去バージョンには保持期限があり、下流のパイプラインは最新バージョンを読んで動いてしまいます。復旧の道が用意されていることと、事故を起こしてよいことは別です。AIが書いた書き込み系のコードを実行前に確認する、という前回からの原則は変わりません。ただ、万一のときに「戻れる場所がある」と知っているかどうかで、事故対応の落ち着きはまったく違います。
ハマりどころ: 「いつでも戻れる」わけではない
タイムトラベルには保持期限があります。デフォルトでは、テーブル履歴の保持は30日 (logRetentionDuration)、タイムトラベルに必要な過去データファイルの保持は7日 (deletedFileRetentionDuration) で、VACUUMが実行されると期限切れのファイルは物理削除されます。つまりタイムトラベルは事故直後の復旧手段であって、長期バックアップではありません。公式ドキュメントにも、履歴を長期バックアップとして使うことは推奨しないと明記されています。
もう1つはAIとの付き合い方です。Genie Codeに「さっきの追記を元に戻して」と曖昧に頼むこともできてしまいますが、どのバージョンに戻すかの判断をAIに委ねるのは危険です。DESCRIBE HISTORYを自分の目で見て、戻り先のバージョン番号を自分で指定する。今回の仕様プロンプトで「VERSION AS OF 0」と明示したのはそのためです。破壊的操作の最終判断は人間が持つ、という連載の原則はタイムトラベルにも適用されます。
まとめ
orders_1998でわざと事故を起こして分かったことをまとめます。
- Deltaテーブルへの書き込みは、何も設定しなくてもすべてバージョンとして記録される。DESCRIBE HISTORYが履歴の入口
- VERSION AS OFは読み取り専用で安全に過去を覗ける。RESTOREは戻す操作。調査と復旧を構文レベルで分けられる
- RESTORE自体も新しいバージョンとして積まれる。誤操作の事実も履歴に残り続けるので、監査に強い
- 前回の伏線を回収: CREATE OR REPLACEは「黙って消す」のではなく「黙って新しいバージョンを積む」。ただし事故を許容してよい理由にはならない
- タイムトラベルの保持期限はデフォルトで7日 (データファイル) / 30日 (履歴)。長期バックアップの代わりにはならない
- 戻り先のバージョン番号は、AIに任せず自分でDESCRIBE HISTORYを見て指定する
一番の収穫は、「上書き事故が怖い」の解像度が上がったことでした。第1回からずっと「書き込みmodeは自分の目で確認」と繰り返してきましたが、それは事故が復旧不能だからではなく、復旧には条件 (期限内であること、戻り先を正しく特定できること) があるからです。仕組みを知った上でなお慎重に、が正しい距離感だと思います。
次回はLakeflow編です。ここまで手で組んできた「読み込んで、変換して、保存する」という流れを、Lakeflow Designerでノーコードのパイプラインとして組み、その裏で生成されるSDP (Spark Declarative Pipelines) のコードを読み解きます。






