Data Engineer Associate — 第 15

テーブルを保守して速くする — 運用コマンド・保持期間・レイアウト設計・障害復旧

読了 18確認 12更新 2026-08-09

この章で学ぶこと

  • Delta の運用コマンド 5 種を目的別に打ち分ける
  • VACUUM の 168 時間という既定値の根拠を説明する
  • data skipping が効く条件を統計値の観点から見抜く
  • パーティションと ZORDER と Liquid を選び分ける
  • 壊れた checkpoint とクラスタを手順どおりに復旧する

この章に出てくる用語

_delta_log
Delta テーブルの実体は Parquet 群と、隣の _delta_log ディレクトリです。ここに JSON のコミットログとファイルごとの min/max 統計が積まれ、タイムトラベルと data skipping を支えます。
data skipping
WHERE 句の条件に当たりようがないデータファイルを開かずに飛ばす仕組みです。_delta_log の列ごとの min/max と NULL 件数を使い、既定ではスキーマ先頭 32 列ぶんの統計しか参照できません。
OPTIMIZE
小さな Parquet ファイルを目標サイズにまとめ直す bin-packing のコマンドです。2.56 TB 未満のテーブルなら 256 MB、10 TB 超なら 1 GB が目標で、二度打っても結果が変わらない冪等な操作です。
VACUUM
未参照のデータファイルを物理削除します。保持期間の既定はテーブルプロパティ delta.deletedFileRetentionDuration の interval 1 week、つまり 168 時間です。
retentionDurationCheck
設定値より短い保持時間の VACUUM を止めます。spark.databricks.delta.retentionDurationCheck.enabled を false にすれば外せますが、並走クエリが壊れます。
Liquid Clustering
CLUSTER BY に指定した最大 4 列へデータを寄せます。書き込みには Databricks Runtime 13.3 LTS 以上が必要で、GA は 15.4 LTS 以上、PARTITIONED BY や ZORDER と併用できません。
Predictive Optimization
Unity Catalog のマネージドテーブルの OPTIMIZE・VACUUM・ANALYZE を自動実行する機能です。2024 年 11 月 11 日以降のアカウントは既定で有効で、external table は対象外です。
SHALLOW CLONE
メタデータだけを複製し、データファイルはソース側の実体を参照し続けるクローンです。作成は一瞬ですが、ソースで VACUUM を実行するとクローンの SELECT が FileNotFoundException で落ちます。

Delta の運用コマンド5種 — DESCRIBE HISTORY / DESCRIBE DETAIL / VERSION AS OF / RESTORE / OPTIMIZE の書き分け

昨日まで 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 だけと思い込むと、このコード断片を見せる形式で足をすくわれます。

DESCRIBE DETAIL — 見立てはこの 1 行から

返るのは物理メタデータ 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 TABLE`
  • CREATE 文を再構築したい → `SHOW CREATE TABLE`
  • パーティション値を列挙したい → `SHOW PARTITIONS`

DESCRIBE HISTORY — 戻す先の版番号を特定する

`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` です。

VERSION AS OF は読むだけ、RESTORE は書き戻す

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 は冪等、範囲も絞れる

`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.() を呼ぶ。

保持期間の設計 — VACUUM の 168 時間はどこから来るのか、retentionDurationCheck、logRetentionDuration

「7 日前に戻したいのに戻せない」と「使っていないストレージ代が増え続ける」は、同じダイヤルの両端です。回すのは保持期間で、Delta には性格の違うものが 2 つあります。

168 時間はどこから来るのか

`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;

2 つの保持期間を揃える

プロパティ既定値保持するもの短くすると
`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 にする必要がある。

データレイアウトで速くする — data skipping(先頭32列統計)、partitioning、ZORDER、Liquid Clustering、Predictive Optimization

SQL Warehouse のサイズを上げても、同じクエリが同じだけ遅い。原因は計算資源ではなく、読まなくていいファイルまで読んでいることです。読む量を減らす土台が data skipping で、partitioning も ZORDER も Liquid Clustering もこの土台を効かせる手段です。

土台 — ファイル単位の min/max 統計

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 以上) で統計を取り直します。

partitioning は「ディレクトリを分ける」だけ

`PARTITIONED BY` は列の値ごとに物理ディレクトリを分けます。`WHERE` にその列があればディレクトリごと読み飛ばせますが (partition pruning)、値の種類が多い列で切ると破綻します。user_id で切れば、ユーザーの数だけディレクトリと数十 KB のファイルが生まれ、`_delta_log` が肥大化してファイル一覧の取得だけで時間が溶けます。目安は目標ファイルサイズで、1 パーティションが 256 MB の数分の 1 にも届かないなら partitioning は使いません

ZORDER と Liquid Clustering

ZORDER は `OPTIMIZE` のオプションで、指定列の値が近い行を同じファイルに寄せる多次元の並べ替えです。ディレクトリを増やさずに min/max の幅を狭められるので、user_id のような値の種類が多い列に向きます。列は増やすほど 1 列あたりの効きが落ちるため、実際に `WHERE` で使う 1〜2 列に絞ります。新しい書き込みには効かず、打ち直す運用が要ります。

OPTIMIZE sales ZORDER BY (customer_id);

その運用ごと置き換えるのが Liquid Clustering です。版数の要件は 3 段に分かれており、ここを 1 つにまとめると誤ります。

  • クラスタリング済みテーブルへ書き込むには Databricks Runtime 13.3 LTS 以上 (推奨は最新 LTS)
  • Delta テーブルでのGA は Databricks Runtime 15.4 LTS 以上
  • `CLUSTER BY AUTO` は DBR 15.4 LTS 以上に加え、Unity Catalog のマネージドテーブルであることと Predictive Optimization が有効であることが必須
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 に任せる境目

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 つに当たるときだけです。

Deletion Vectors — 消したのに容量が減らない

`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 つである。

壊れたときの復旧と罠 — checkpoint 破損、INIT_SCRIPT_FAILURE、ライブラリ衝突、SHALLOW CLONE + VACUUM

ここまでは速くする話でした。最後は壊れたときの話です。厄介なのは、正しい手順を知らないと復旧作業そのものが二次被害を生む点にあります。試験で繰り返し問われる壊れ方は 4 つに絞れます。共通しているのは、症状が出ている場所と原因のある場所がずれていることです。ログを先に読み、どの層の話かを確定させてから手を動かします。

checkpoint が壊れてストリームが起動しない

Structured Streaming の `checkpointLocation` には、どこまで読んだかを持つ offsets、確定したマイクロバッチを記録する commits、集計の途中状態を持つ state、クエリ ID を持つ metadata が入ります。1 つのクエリに 1 つのディレクトリを割り当てるのが原則で、複数のクエリで共有してはいけません。

破損時にやってはいけないのは、ディレクトリを丸ごと消して起動し直すことです。ソースの先頭から全件を読み直すため、シンク側に大量の重複が生まれます。新しい checkpoint パスを指定するのも結果は同じです。破損が一部のファイルに限られるなら、最後に成功したコミットを残したまま壊れたファイルだけを取り除き、同じ checkpoint で再起動するのが重複を最小にする手順です。なお `failOnDataLoss = false` はソース側でデータが消えていても止まらないようにするフラグで、checkpoint の破損には効きません。

ジョブ側の設定も揃えます。同じ checkpoint を 2 つの run が同時に触ると壊れるため、Lakeflow Jobs では最大同時実行数を 1 にし、失敗時は再試行間隔を徐々に延ばす指数バックオフで再試行させます。

INIT_SCRIPT_FAILURE でクラスタが上がらない

クラスタスコープの 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 とは層が違います。

SHALLOW CLONE を作った本番で VACUUM を打つ

`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 は で失敗しうる。

この章のまとめ

  1. 調べる 3 種と直す 2 種を分け、見立ててから打つ
  2. VACUUM の既定 168 時間は走行中クエリを守る安全マージン
  3. 統計は先頭 32 列のみ。フィルタ列は前方に置く
  4. Liquid Clustering はキー最大 4 列、GA は DBR 15.4 LTS 以上
  5. SHALLOW CLONE のソースで VACUUM を打つと参照先が消える

この端末にだけ保存されます(登録不要)

この章の根拠

最終確認 2026-08-09 / 対応バージョン DEA 2026-05-04 改訂版

コース全体

  1. Databricks とレイクハウスの全体像 — なぜ必要か、何がどこに属し、どの計算資源で動くか
  2. データはどこに、どんな形で保存されるか — テーブルの実体と Delta Lake の内部
  3. 計算資源を作り込み、コードを書く場所を決める — クラスタ設定とノートブック/ローカル IDE
  4. Spark はどう動き、SQL で何をどこまで書けるか
  5. PySpark でデータを加工し、遅いコードを見抜く
  6. 流れ込むデータを受け止める — Structured Streaming と Auto Loader のファイル検出
  7. 取り込み時にスキーマをどう扱うか — Auto Loader のスキーマ進化と COPY INTO
  8. ファイル以外からも取り込む — read_files・Lakeflow Connect・JDBC と API
  9. 生データを使える形に育てる — メダリオン設計と宣言的パイプライン
  10. 処理を1つのジョブに束ねる — タスク種別と依存関係の設計
  11. ジョブを動かし、失敗から立て直す — トリガー・リトライ・パラメータ
  12. 書いたものを安全に本番へ届ける — Git 連携とデプロイの自動化
  13. 誰に何を見せるか — Unity Catalog のアクセス制御とアイデンティティ統制
  14. 誰が何をしたか、いくらかかったか、どこで詰まったかを見る
  15. テーブルを保守して速くする — 運用コマンド・保持期間・レイアウト設計・障害復旧
  16. DEA 直前仕上げ — 方式選定・出題範囲対応表・数値総まとめ・引っかけの型