Data Engineer Associate — 第 15 章
この章で学ぶこと
この章に出てくる用語
昨日まで 3 秒で返っていたダッシュボードが、今朝は 40 秒かかる。あるいは朝のバッチが誤った条件で `UPDATE` を流し、テーブルの中身が壊れた。どちらも Delta テーブルの運用ですが、打つべきコマンドは違います。遠回りが起きるのは、調べるコマンドを飛ばして重いコマンドを打つときです。試験でも「復元したい」という問題文に `VACUUM` や `OPTIMIZE` が復元手段のように並びます。役割で 3 つに分けておけば取り違えません。
| コマンド | 役割 | 書き換えるか | Python API |
|---|---|---|---|
| `DESCRIBE DETAIL` | 調べる | いいえ | `dt.detail()` |
| `DESCRIBE HISTORY` | 調べる | いいえ | `dt.history()` |
| `VERSION AS OF` | 調べる | いいえ | `option("versionAsOf", 5)` |
| `RESTORE TABLE` | 戻す | はい (新コミット) | `dt.restoreToVersion(43)` |
| `OPTIMIZE` | 並べ直す | はい (行は不変) | `dt.optimize().executeCompaction()` |
Python の `dt` は `from delta.tables import DeltaTable` のうえで `dt = DeltaTable.forPath(spark, '/delta/sales')` と作ります。SQL だけと思い込むと、このコード断片を見せる形式で足をすくわれます。
返るのは物理メタデータ 1 行で、列は format、id、name、description、location、createdAt、lastModified、partitionColumns、clusteringColumns、numFiles、sizeInBytes、properties、minReaderVersion、minWriterVersion、tableFeatures、statistics の 16 個です。スキーマは返りません。
DESCRIBE DETAIL sales;
DESCRIBE DETAIL delta.`/data/events/`;見るのは 1 か所、sizeInBytes を numFiles で割った平均ファイルサイズです。自動調整される目標サイズは、テーブルが 2.56 TB 未満なら 256 MB、2.56 TB から 10 TB では 256 MB から 1 GB へ線形に増え、10 TB 超では 1 GB です。平均が数 MB なら小ファイルが溜まっており `OPTIMIZE` の出番、目標付近なら遅さの原因はレイアウトではありません。
`DESCRIBE HISTORY sales` は `_delta_log` のコミットを新しい順に返します。version、timestamp、userName、operation、operationParameters、operationMetrics、isBlindAppend など 14 列です。operation は WRITE、MERGE、DELETE、OPTIMIZE などで、operationMetrics には numFiles や numOutputRows が入ります。遡れる長さはテーブルプロパティ `delta.logRetentionDuration` が決め、既定は `interval 30 days` です。
SELECT count(*) FROM sales VERSION AS OF 43;
SELECT * FROM sales TIMESTAMP AS OF '2026-08-01 09:00:00';
SELECT * FROM sales@v43;3 つとも読み取り専用で、テーブル本体は 1 バイトも変わりません。`@v43` はバージョン番号の短縮表記です。PySpark なら `spark.read.format("delta").option("versionAsOf", 5).load(path)` と書きます。中身を確かめてから `RESTORE` に進みます。
RESTORE TABLE sales TO VERSION AS OF 43;`RESTORE` は過去の状態を新しいコミットとして積み直す操作です。最新が 45 なら結果は 46 になり、43 には戻りません。実行には MODIFY 権限が要ります。このコミットは dataChange = true で書かれるため、同じテーブルを読むストリーミングクエリは復元分を新規データとして読み直し、下流に重複が出ます。
`OPTIMIZE` は小さいファイルを目標サイズにまとめ直す bin-packing で、行の中身は変えません。bin-packing は冪等で、同じデータに 2 回走らせても 2 回目は何もしないため、定期実行に組み込んでも害はありません。`WHERE` にパーティション述語を書けば直近の書き込み分だけを対象にできます。
OPTIMIZE sales WHERE event_date >= '2026-08-01';構文より打つ順番です。`DESCRIBE DETAIL` で平均サイズを見て小さければ `OPTIMIZE`、operationMetrics で numFiles の減りを確認、保持期間を決めて `VACUUM`、事故のときだけ `VERSION AS OF` で見てから `RESTORE`。この順番が運用の型です。
確認 — 穴あけ 3 問
0 / 3
空欄を押すと選択肢が出ます。間違えても減点はありません。
テーブル本体を過去の状態へ物理的に巻き戻すコマンドは である。
DESCRIBE DETAIL が返す sizeInBytes を で割ると、そのテーブルの平均ファイルサイズが分かる。
dt = DeltaTable.forPath(spark, '/delta/sales') のあと、パス・サイズ・ファイル数を 1 行で得るには dt.() を呼ぶ。
「7 日前に戻したいのに戻せない」と「使っていないストレージ代が増え続ける」は、同じダイヤルの両端です。回すのは保持期間で、Delta には性格の違うものが 2 つあります。
`VACUUM sales;` が消すのは、最新のスナップショットから参照されなくなり、かつ参照されなくなってからしきい値より古くなったデータファイルです。このしきい値はテーブルプロパティ `delta.deletedFileRetentionDuration` が決め、既定は `interval 1 week`、つまり 168 時間です。
168 という数字の根拠は性能ではなく安全です。長時間のバッチやストリーミングクエリは、開始時点のスナップショットを参照し続けます。その途中で参照中のファイルが消えるとクエリは FileNotFoundException で落ちます。168 時間は走行中の読み取りを壊さないための安全マージンです。
意味を取り違えないでください。`RETAIN 12 HOURS` は「12 時間より新しい未参照ファイルを守る」指定であって、「12 時間以内のファイルを消す」指定ではありません。向きが逆の選択肢は頻出です。
SET spark.databricks.delta.retentionDurationCheck.enabled = false;
VACUUM sales RETAIN 1 HOURS;`spark.databricks.delta.retentionDurationCheck.enabled` は既定 true の安全装置で、テーブルの設定値を下回る保持時間を指定すると VACUUM をエラーで止めます。既定の 7 日のまま `RETAIN 12 HOURS` を打つと失敗するのはこれが理由です。外した状態で `RETAIN 0 HOURS` を打つと最新版に属さない全ファイルが即座に消え、並走クエリもタイムトラベルも壊れます。
もっとも、Spark 設定を外すのは今や推奨手段ではありません。サーバーレスコンピュートはこの設定に対応しておらず、Databricks Runtime 18.0 以降では `VACUUM` が保持時間の引数を無視します (0 時間のときだけ例外)。現行の SQL リファレンスの構文も `VACUUM table_name { { FULL | LITE } | DRY RUN }` で、`RETAIN` 句は載っていません。保持期間はテーブルプロパティで指定するのが正解です。
ALTER TABLE sales SET TBLPROPERTIES (
'delta.deletedFileRetentionDuration' = 'interval 30 days',
'delta.logRetentionDuration' = 'interval 30 days');
VACUUM sales DRY RUN;| プロパティ | 既定値 | 保持するもの | 短くすると |
|---|---|---|---|
| `delta.logRetentionDuration` | `interval 30 days` | コミット履歴 | DESCRIBE HISTORY に出ない |
| `delta.deletedFileRetentionDuration` | `interval 1 week` | 未参照のデータファイル | VACUUM に消され戻せない |
タイムトラベルは履歴と実ファイルの両方が揃って初めて成立します。既定のままだと履歴は 30 日見えるのに実データは 7 日で消えるため、8 日前は履歴に出るのに読めません。片方だけ延ばしても無意味で、公式にも `logRetentionDuration` は `deletedFileRetentionDuration` 以上と明記されています。なお `VACUUM` は `_` や `.` で始まるディレクトリを無視するため、`_delta_log` 自体は消しません。何時間にするかは、下流の再処理範囲、監査要件、最長バッチの実行時間の最大値で決めます。
確認 — 穴あけ 3 問
0 / 3
空欄を押すと選択肢が出ます。間違えても減点はありません。
VACUUM を引数なしで実行したとき、削除対象になるのは 時間より古い未参照ファイルである。
VACUUM my_table RETAIN 12 HOURS は、 未参照ファイルを物理削除する指定である。
既定の 7 日を下回る retention を指定して VACUUM を通すには を false にする必要がある。
SQL Warehouse のサイズを上げても、同じクエリが同じだけ遅い。原因は計算資源ではなく、読まなくていいファイルまで読んでいることです。読む量を減らす土台が data skipping で、partitioning も ZORDER も Liquid Clustering もこの土台を効かせる手段です。
Delta は書き込みのたびに、データファイルごとの統計を `_delta_log` の JSON に記録します。中身は列ごとの最小値・最大値・NULL 件数・レコード件数です。クエリの `WHERE` 句とこの範囲を突き合わせ、条件に当たりようがないファイルは開かずに飛ばします。これが data skipping です。
重要なのは対象範囲です。統計はスキーマの先頭 32 列にしか取られません。テーブルプロパティ `delta.dataSkippingNumIndexedCols` の既定値が 32 だからです。40 列あるテーブルの 38 列目でフィルタしても統計が無いので効きません。長い文字列は収集時に切り詰められるため、本文のような列を前方に置くのも損です。
ALTER TABLE sales SET TBLPROPERTIES ('delta.dataSkippingNumIndexedCols' = '64');
ALTER TABLE sales SET TBLPROPERTIES ('delta.dataSkippingStatsColumns' = 'order_date,customer_id');
ANALYZE TABLE sales COMPUTE DELTA STATISTICS;`delta.dataSkippingStatsColumns` は列名を直接指定でき、`delta.dataSkippingNumIndexedCols` より優先されます。使えるのは Databricks Runtime 13.3 LTS 以上です。どちらも設定後に書かれたデータにしか効かないので、既存データには `ANALYZE TABLE ... COMPUTE DELTA STATISTICS` (DBR 14.3 LTS 以上) で統計を取り直します。
`PARTITIONED BY` は列の値ごとに物理ディレクトリを分けます。`WHERE` にその列があればディレクトリごと読み飛ばせますが (partition pruning)、値の種類が多い列で切ると破綻します。user_id で切れば、ユーザーの数だけディレクトリと数十 KB のファイルが生まれ、`_delta_log` が肥大化してファイル一覧の取得だけで時間が溶けます。目安は目標ファイルサイズで、1 パーティションが 256 MB の数分の 1 にも届かないなら partitioning は使いません。
ZORDER は `OPTIMIZE` のオプションで、指定列の値が近い行を同じファイルに寄せる多次元の並べ替えです。ディレクトリを増やさずに min/max の幅を狭められるので、user_id のような値の種類が多い列に向きます。列は増やすほど 1 列あたりの効きが落ちるため、実際に `WHERE` で使う 1〜2 列に絞ります。新しい書き込みには効かず、打ち直す運用が要ります。
OPTIMIZE sales ZORDER BY (customer_id);その運用ごと置き換えるのが Liquid Clustering です。版数の要件は 3 段に分かれており、ここを 1 つにまとめると誤ります。
CREATE TABLE sales (order_id BIGINT, order_date DATE, customer_id BIGINT)
CLUSTER BY (order_date, customer_id);
ALTER TABLE sales CLUSTER BY (customer_id);
OPTIMIZE sales FULL;制約は 2 つです。クラスタリングキーは最大 4 列まで。そして partitioning とも ZORDER とも併用できません。既存の `PARTITIONED BY` テーブルをその場で Liquid に変える `ALTER TABLE` は無いので、`CLUSTER BY` 付きの新テーブルを作り CTAS (CREATE TABLE AS SELECT) でコピーし、名前を入れ替えます。既存データも並べ直したいときだけ、DBR 16.4 LTS 以上で `OPTIMIZE ... FULL` を使います。
| 手段 | 枝刈りの粒度 | 向くカーディナリティ | キーの後変更 |
|---|---|---|---|
| `PARTITIONED BY` | ディレクトリ単位 | 低い (年月、国コード) | 実質不可 (全件書き直し) |
| `ZORDER BY` | ファイル単位 | 高い (user_id など) | `OPTIMIZE` のたびに指定 |
| `CLUSTER BY` | ファイル単位 | 高低どちらでも | `ALTER TABLE` でいつでも |
ディレクトリ単位の枝刈りを partition pruning、統計値によるファイル単位の枝刈りを file pruning (= data skipping) と呼び分けます。両者は重ねて効き、file pruning は ZORDER が無くても既定で動きます。
Predictive Optimization は、Unity Catalog のマネージドテーブルに対して `OPTIMIZE`・`VACUUM`・`ANALYZE` の 3 つを自動実行します。2024 年 11 月 11 日以降に作成されたアカウントでは既定で有効です。
ALTER CATALOG main ENABLE PREDICTIVE OPTIMIZATION;
ALTER SCHEMA main.ops DISABLE PREDICTIVE OPTIMIZATION;任せられない範囲もはっきりしています。external table は対象外、ZORDER は自動実行されません。`VACUUM` の保持期間は `delta.deletedFileRetentionDuration` に従うので、7 日より長く残したいなら先にプロパティを変えます。自分で定期実行を書くのはこの 3 つに当たるときだけです。
`DELETE` のたびに Parquet ファイル全体を書き直すのが重い、という悩みに効くのが Deletion Vectors です。`delta.enableDeletionVectors` を true にすると、削除された行の位置をビットマップとして別ファイルに書き、Parquet 本体は残します。読み取り時に差し引かれるので結果は正しく、書き込み量が減ります。実体を消すのは後回しで、`OPTIMIZE` か `REORG TABLE sales APPLY (PURGE)` が書き直し、保持期間を過ぎてから `VACUUM` で消えます。REORG の直後に VACUUM を打っても保持期間内なので減りません。これが「削除したのに料金が下がらない」の正体です。
確認 — 穴あけ 3 問
0 / 3
空欄を押すと選択肢が出ます。間違えても減点はありません。
delta.dataSkippingNumIndexedCols の既定値は で、統計はスキーマ先頭のその列数分にしか収集されない。
Liquid Clustering を使うテーブルへ書き込むには、Databricks Runtime 以上が必要である。
Predictive Optimization が自動実行するのは OPTIMIZE・VACUUM・ の 3 つである。
ここまでは速くする話でした。最後は壊れたときの話です。厄介なのは、正しい手順を知らないと復旧作業そのものが二次被害を生む点にあります。試験で繰り返し問われる壊れ方は 4 つに絞れます。共通しているのは、症状が出ている場所と原因のある場所がずれていることです。ログを先に読み、どの層の話かを確定させてから手を動かします。
Structured Streaming の `checkpointLocation` には、どこまで読んだかを持つ offsets、確定したマイクロバッチを記録する commits、集計の途中状態を持つ state、クエリ ID を持つ metadata が入ります。1 つのクエリに 1 つのディレクトリを割り当てるのが原則で、複数のクエリで共有してはいけません。
破損時にやってはいけないのは、ディレクトリを丸ごと消して起動し直すことです。ソースの先頭から全件を読み直すため、シンク側に大量の重複が生まれます。新しい checkpoint パスを指定するのも結果は同じです。破損が一部のファイルに限られるなら、最後に成功したコミットを残したまま壊れたファイルだけを取り除き、同じ checkpoint で再起動するのが重複を最小にする手順です。なお `failOnDataLoss = false` はソース側でデータが消えていても止まらないようにするフラグで、checkpoint の破損には効きません。
ジョブ側の設定も揃えます。同じ checkpoint を 2 つの run が同時に触ると壊れるため、Lakeflow Jobs では最大同時実行数を 1 にし、失敗時は再試行間隔を徐々に延ばす指数バックオフで再試行させます。
クラスタスコープの init script が 0 以外の終了コードで終わると、Databricks はクラスタの起動を打ち切り `INIT_SCRIPT_FAILURE` 状態にします。自動リトライもスキップもなく、複数登録していれば最初の失敗で止まります。init script は環境構築の前提なので、失敗を無視した起動はしない設計です。
調査は stderr を読むところから始めます。cluster log delivery を設定していれば `/<cluster-log-path>/<cluster-id>/init_scripts` に、設定していなければ `/databricks/init_scripts` に出力され、ファイル名は `<timestamp>_<log-id>_<init-script-name>.sh.stderr.log` の形式です。手元で `bash -x` にかけて再現させるところまでが定石です。
同じパッケージがクラスタにも入っていて、ノートブックでも `%pip install` した場合、ノートブックスコープが優先されます。クラスタに pandas 2.0 が入っていても、ノートブックで `%pip install pandas==2.2` を実行すれば、そのノートブックは 2.2 を使います。Databricks はノートブックごとに隔離された Python 環境を作るため、同じクラスタの別ノートブックは 2.0 のまま動き続けます。効果はセッション限りなので、ジョブで使うなら毎回インストールされる形にしておきます。これは実行時の話で、起動時に失敗する INIT_SCRIPT_FAILURE とは層が違います。
`CREATE TABLE dev SHALLOW CLONE prod;` はメタデータだけを複製し、データファイルはソース側の実体を参照し続けます。作成が一瞬で済む代わりに、ソースで `VACUUM` を実行すると参照先が物理削除され、クローン側の `SELECT` が `FileNotFoundException` で落ちます。復旧は `CREATE OR REPLACE TABLE dev SHALLOW CLONE prod;` でクローンを作り直すことです。頻発するなら、実体までコピーする `DEEP CLONE` に切り替えます。クローン側への書き込みがソースに伝播することはなく、スキーマが自動同期されることもありません。
4 つを層で並べ直すと、調べる場所が一意に決まります。checkpoint はストリーミングの進捗、INIT_SCRIPT_FAILURE はクラスタの起動、ライブラリ衝突はノートブックの実行時、clone と Deletion Vectors はテーブルのファイル管理。障害を見たらまずどの層かを決め、その層のログだけを読むと切り分けが速く終わります。
確認 — 穴あけ 3 問
0 / 3
空欄を押すと選択肢が出ます。間違えても減点はありません。
cluster-scoped init script が 0 以外の終了コードで終わると、クラスタは起動を打ち切られ になる。
クラスタに pandas 2.0 が入った状態でノートブックが %pip install pandas==2.2 を実行すると、そのノートブックは を使う。
SHALLOW CLONE の作成元テーブルで VACUUM を実行すると、クローン側の SELECT は で失敗しうる。
この章のまとめ
この端末にだけ保存されます(登録不要)
この章の根拠
最終確認 2026-08-09 / 対応バージョン DEA 2026-05-04 改訂版
コース全体