Unity Catalogのマネージドテーブルを持っている人向けで、とくにrow_commit_versionで増分処理を組んでいるなら影響が出ます。
2026-07-29から、既存のテーブルにrow trackingとCheckpoint V2が順に自動で有効になり始めました(7月のリリースノート)。
他のエンジンが読めなくなることはありません。
ただしrow_commit_versionを「その行がいつ書かれたか」として使うつもりなら、自動で有効化されたテーブルではその読み方が成立しません。
有効化より前に書かれた行が全部同じ版に揃うので、差分を取る処理が1回だけテーブル全体を返します。
row trackingを、UPDATEをまたいで同じ行を追う目的にしか使っていないなら、そこは影響を受けないので安心して読み飛ばしてください。
それでもrow_idがどこに書かれているかは、あとで増分処理を組むときに効いてきます。
業務ではデータ分析基盤としてDatabricksを使っていて、Unity Catalogもその一部です。
自動有効化の対象はUnity Catalogのマネージドテーブルなので、これは他人の環境の話ではないと思って読み始めました。
Databricksの環境は手元では動かせないので、同じ操作をOSS側で当てました。
row trackingもv2CheckpointもDelta Lakeのプロトコル側の機能なので、OSSのSparkとDelta Lakeがあれば同じ形のテーブルを作れます。
検証環境は次のとおりです。
OS : Ubuntu 24.04.4 LTS (WSL2, kernel 6.6.87.2-microsoft-standard-WSL2)
JDK : openjdk 21.0.12 (Temurin)
Spark : 4.1.3
Delta : io.delta:delta-spark_2.13:4.3.1
DuckDB : v1.5.5 (delta 拡張)
Rust : rustc 1.95.0 / delta_kernel 0.26.0 / deltalake 0.32.4
Sparkは4.2.0を先に入れたのですが、そちらではCREATE TABLE ... USING deltaがjava.lang.NoSuchMethodError: CatalogStorageFormat.copy(...)で落ちました。
delta-spark 4.3.1のPOMを見るとspark-sql_2.13:4.1.0をprovidedで要求していて、4.1系向けのビルドしか出ていません。
Databricks Runtime 19はSpark 4.2.0ベースを謳っているので、マネージド側とOSS側でSparkの世代が1つずれています。
4.1.3を別に入れて先に進みましたが、この回り道はPOMを先に開いていれば要りませんでした。
追試するなら、delta-sparkのPOMのprovidedを見ると、対応するSparkが4.1系だと分かります。
誰のテーブルに、いつ来るのか
自動有効化には条件があって、持っているテーブルに一斉に来るわけではありません。
Databricksのブログによると、テーブルへのアクセスを100日ぶん観測して、その間に触った全クライアントがその機能に対応していると確認できたときにだけ有効化する、という作りになっています(Automatic Upgrades)。
100日という長さは、月次バッチや四半期のレポートのように間隔の空いたジョブを取りこぼさないために選んだ、と説明されています。
| 条件 | 内容 |
|---|---|
| クライアント | 外部クライアントが触っているテーブルには当てない |
| 観測期間 | 直近100日のアクセスを見る |
| 活動 | 30日以上アクセスの無いテーブルは飛ばす |
ここで順番が決まります。
Spark以外のエンジンから同じテーブルを読んでいるなら、そのテーブルは今のところ条件から外れていて、先に来るのはDatabricksの中だけで完結しているテーブルです。
ブログは外部クライアントの対応を確認できるようになったら含めると書いているので、来ないわけではなく、対応が揃った時点で同じことが起きます。
自動有効化が当てる機能もrow trackingとCheckpoint V2の2つだけではなく、Liquid Clustering、Deletion Vectors、Column Mapping、Parquet V2、Catalog Commitsを合わせた7つが挙がっています。
この記事で4つのエンジンに読ませたのはそのうちの2つで、残りは確かめていません。
とくにDeletion Vectorsはリーダ側の対応が要る機能なので、ここでの「読めた」を7機能ぶんに広げないでください。
ALTER TABLE 1回が、3つのコミットに割れる
まず、row trackingもCheckpoint V2も無い普通のテーブルを作ります。
2行を別々のコミットで入れて、そのあとに自動有効化と同じ設定を当てました。
-- row tracking も Checkpoint V2 も無い、アップグレード前のテーブル
CREATE TABLE old1 (id INT, name STRING) USING delta LOCATION '/tmp/dbx-bench/old1';
INSERT INTO old1 VALUES (1,'a'); -- コミット1
INSERT INTO old1 VALUES (2,'b'); -- コミット2
-- Databricks が既存テーブルに当てているのと同じ2つの設定を後から入れる
ALTER TABLE old1 SET TBLPROPERTIES ('delta.enableRowTracking'='true','delta.checkpointPolicy'='v2');
ALTER TABLEを投げたときにSparkが出したのはこの1行だけでした。
Upgraded table at file:/tmp/dbx-bench/old1 to Protocol(1,7,None,[appendOnly,domainMetadata,invariants,rowTracking]).
minReaderVersionが1のままです。
Checkpoint V2はリーダ側の機能なので、これだと読む側のバージョンが上がっていないことになります。
私はこのメッセージだけを見て「リーダ版は上がらないのか」と一度書きかけました。
これは誤読で、Sparkが出しているのはアップグレードが終わる前の値でした。
同じログを見る機会があれば、この1行で判断せずに_delta_logを開いてみてください。
中身はもっと素直でした。
00000000000000000000.json : op=CREATE TABLE | PROTOCOL r=1 w=2 readerFeatures=None
00000000000000000001.json : op=WRITE | add baseRowId=None defaultRowCommitVersion=None
00000000000000000002.json : op=WRITE | add baseRowId=None defaultRowCommitVersion=None
00000000000000000003.json : op=UPGRADE PROTOCOL | PROTOCOL r=1 w=7 readerFeatures=None
00000000000000000004.json : op=ROW TRACKING BACKFILL | add baseRowId=0 defaultRowCommitVersion=4
| add baseRowId=1 defaultRowCommitVersion=4
00000000000000000005.json : op=SET TBLPROPERTIES | PROTOCOL r=3 w=7 readerFeatures=['v2Checkpoint']
ALTER TABLE1回が3つのコミットに割れていました。
UPGRADE PROTOCOLでライタ側の機能を足し、ROW TRACKING BACKFILLで既存ファイルに行のIDを配り、最後のSET TBLPROPERTIESでリーダ版が3に上がってv2Checkpointが付きます。
Sparkが出したProtocol(1,7,...)は3つのうち真ん中より前、つまりコミット3の時点のものだったので、あの1行だけでは判断できませんでした。
2つの設定を1文で指定したのに、内部では順番が決まった3段階の手順になっている。
図の下段が実データのファイルです。
コミット4で行のIDが配られていますが、parquetのファイル名も更新時刻も変わっていません。
4つのエンジンで読む
このアップグレードしたテーブルを、Spark以外の3つのエンジンで読みました。
DuckDBのdelta拡張、Rustのdelta_kernel、同じくRustのdelta-rsです。
最後の2つは実装が別なので、片方が読めてもう片方が落ちる可能性があります。
# DuckDB 1.5.5 の delta 拡張でスキャンする
duckdb -c "INSTALL delta; LOAD delta; SELECT * FROM delta_scan('file:///tmp/dbx-bench/old1') ORDER BY id;"
| エンジン | 読めたか | 返ってきた列 |
|---|---|---|
| Spark 4.1.3 + Delta 4.3.1 | 読めた |
id name + _metadata.row_id _metadata.row_commit_version
|
| DuckDB 1.5.5(delta拡張) | 読めた |
id name
|
| Rust delta_kernel 0.26.0 | 読めた |
id name
|
| Rust delta-rs 0.32.4 | 読めた |
id name
|
4つとも読めました。
プロトコルのリーダ版が3に上がっても、v2Checkpointに対応していれば読み取り自体は通ります。
delta-rsはmin_reader_version: 3 / reader_features: [V2Checkpoint]と正しく解釈したうえでデータを返していました。
行のIDが出てくるのはSparkだけで、残りの3つはidとnameしか返しません。
Rustの2つを書くところで一度止まりました。
delta_kernel 0.26.0でdelta_kernel::engine::default::DefaultEngineを使おうとすると、could not find 'default' in 'engine'でビルドが通りません。
0.25.0でdefault engineがdelta_kernel_default_engineという別のクレートに分かれていて、engine::defaultのパスは消えていました。
コンストラクタもDefaultEngine::try_new(...)からDefaultEngine::builder(store).build()に変わっています。
さらにobject_storeを自分のCargo.tomlに書いたら、依存グラフに2つのバージョンが入って型が合わなくなり、delta_kernel::object_storeの再エクスポートを使うのが正解でした。
0.25から上げて動かなくなったコードを抱えているなら、engine::defaultのパスを先に、object_storeの二重依存を次に見ると早いです。
最終的に通ったのがこれです。
use std::sync::Arc;
use delta_kernel::Snapshot;
use delta_kernel_default_engine::DefaultEngine;
fn main() {
let path = std::env::args().nth(1).unwrap(); // テーブルのディレクトリを引数で受ける
let abs = std::fs::canonicalize(&path).unwrap(); // 相対パスを絶対パスに直す
let url = url::Url::from_directory_path(&abs).unwrap(); // kernel は URL しか受け取らない
// object_store は delta_kernel の再エクスポートを使う(別クレートから取ると型が合わない)
let store = Arc::new(delta_kernel::object_store::local::LocalFileSystem::new());
let engine = DefaultEngine::builder(store).build(); // 0.26 系はビルダ経由で組む
match Snapshot::builder_for(url.as_str()).build(&engine) { // 最新バージョンのスナップショットを取る
Ok(s) => {
println!("OK version={}", s.version()); // 読めたコミット番号
let cols = s.schema().fields() // テーブルスキーマの列を並べる
.map(|f| format!("{}:{:?}", f.name(), f.data_type()))
.collect::<Vec<_>>().join(", ");
println!("schema={cols}"); // row id 列があるかをここで見る
}
Err(e) => println!("ERR {e}"), // 読めなかった場合の理由
}
}
出力はOK version=5とschema=id:Primitive(Integer), name:Primitive(String)でした。
行のIDに当たる列はスキーマに現れません。
row_idはどこにあるのか
Sparkは_metadata.row_idで行ごとの番号を返しますが、DuckDBもdelta_kernelもdelta-rsもその列を知りません。
Sparkだけが余分な情報を持っているはずもないので、番号の出どころがどこかにあります。
最初にデータファイルそのものを見ました。
F=$(ls /tmp/dbx-bench/old1/*.parquet | head -1) # アップグレード前から在るデータファイルを1本取る
duckdb -c "SELECT name, type FROM parquet_schema('$F');" # そのファイルの物理スキーマを出す
name type
spark_schema NULL
id INT32
name BYTE_ARRAY
idとnameの2列だけです。
テーブル設定には_row-id-col-<uuid>という列名が予約されているのですが、実ファイルにその列はありません。
ここまでで、行のIDはデータの中に無いことが確定しました。
では、Sparkが返している0や1という番号は誰が決めたのか。
答えはログのaddアクションにあって、コミット4の中身をそのまま出すとこうなります。
{"add":{"path":"part-00000-e1f1a7b2-....snappy.parquet","size":675,
"modificationTime":1785540961292,"baseRowId":0,"defaultRowCommitVersion":4}}
{"add":{"path":"part-00000-c48eff7e-....snappy.parquet","size":675,
"modificationTime":1785540958744,"baseRowId":1,"defaultRowCommitVersion":4}}
baseRowIdがファイル単位で0と1です。
行のIDは、そのファイルのbaseRowIdにファイル内の行位置を足したもので決まります(Delta のPROTOCOL.mdの定義)。
今回は1ファイルにつき1行なので、baseRowIdがそのまま行のIDになりました。
面白い並びになっていて、baseRowId=0が付いたのはコミット2で書かれたファイル、baseRowId=1が付いたのはコミット1のファイルです。
その結果、Sparkから見るとid=2の行がrow_id=0、id=1の行がrow_id=1になります。
id name row_id row_commit_version
1 a 1 4
2 b 0 4
行のIDは書かれた順とも主キーの順とも無関係でした。
配られたときのファイルの並び順で決まるだけの番号です。
なのでrow_idの大小に意味を持たせないほうが安全です。
挿入順のつもりでORDER BYに使うと、エラーにはならないまま違う順序が返ってきます。
Checkpoint V2でも同じ情報の持ち方でした。
row trackingを最初から有効にして作ったテーブルで、_last_checkpointとサイドカーを開くとこうなっています。
_last_checkpoint : {"version":2,"size":7,"numOfAddFiles":2,
"v2Checkpoint":{"path":"...checkpoint.<uuid>.json", ...,
"sidecarFiles":[{"path":"...checkpoint.0000000001.0000000001.<uuid>.parquet"}]}}
_last_checkpointはJSONで、protocolやmetaDataをnonFileActionsに直接埋め、ファイル一覧だけをサイドカーのparquetに逃がします。
そのサイドカーをDuckDBのread_parquetで開くと、addが普通の列として読めます。
path baseRowId defaultRowCommitVersion size
part-00000-4aaa6aee-a820-4 0 1 675
part-00000-854b4483-e85e-4 1 2 675
チェックポイントには行の起点が入っていて、データのほうには入っていません。
DuckDBもdelta_kernelもdelta-rsも、このbaseRowIdを読み飛ばしてファイルの中身だけを返しています。
ROW TRACKING BACKFILLが書き換えたもの
3つに割れたコミットの真ん中、ROW TRACKING BACKFILLが何をしたのかを見ます。
アップグレード前のコミット1と、backfillのコミット4を並べるとこうなりました。
| path | size | modificationTime | baseRowId | |
|---|---|---|---|---|
| コミット1(WRITE) | part-00000-c48eff7e-... |
675 | 1785540958744 | なし |
| コミット4(BACKFILL) | part-00000-c48eff7e-... |
675 | 1785540958744 | 1 |
パスもサイズもmodificationTimeも同じで、removeアクションは1件も出ていません。
ログがそう言っているだけでは落ち着かないので、ファイルの側も確認しました。
md5sum /tmp/dbx-bench/old1/*.parquet # 中身が変わっていないかを見る
stat -c '%n %y' /tmp/dbx-bench/old1/*.parquet # 更新時刻が動いていないかを見る
3ea1aabe8d5600554cc24a5c1e95dc56 part-00000-c48eff7e-...-c000.snappy.parquet
b2594285f8b30d5268aa81ac28884aa1 part-00000-e1f1a7b2-...-c000.snappy.parquet
part-00000-c48eff7e-...-c000.snappy.parquet 2026-08-01 08:35:58.744753094 +0900
part-00000-e1f1a7b2-...-c000.snappy.parquet 2026-08-01 08:36:01.292752057 +0900
更新時刻は最初に書き込まれたときのままでした。
backfillは同じaddアクションをbaseRowId付きで出し直しただけで、実データには触っていません。
既存テーブルへの自動有効化がデータの再書き込みを伴わないのは、この構造による。
ここは2行と8行のテーブルで確かめた話です。
動いているのはログの側だけなので行数が増えても同じはずですが、何億行あるテーブルで試したわけではありません。
引っかかったのはここから先です。
backfillが付けたdefaultRowCommitVersionは2つのファイルとも4で、コミット4というのはbackfill自身のバージョンでした。
もとの2行はコミット1とコミット2という別々のタイミングで書かれているのに、どちらも4になります。
2行では並びが見えないので、8行を8回に分けて書いたテーブルでも同じことをしました。
コミット1からコミット8まで1行ずつ入れると、コミット9でUPGRADE PROTOCOL、コミット10でROW TRACKING BACKFILLが走ります。
id name row_id row_commit_version もとのコミット
1 a 3 10 1
2 b 1 10 2
3 c 4 10 3
4 d 5 10 4
5 e 0 10 5
6 f 7 10 6
7 g 6 10 7
8 h 2 10 8
8つの別々のコミットで書かれた8行が、row_commit_versionは全部10、つまりbackfillのコミット番号に揃いました。
row_idも3,1,4,5,0,7,6,2で、挿入順とは無関係です。
別のテーブルでも繰り返して、結果が毎回同じになることは確認しました。
row_commit_versionは「その行が最後に変更されたコミット」なので、backfillで全行が触られたと解釈すれば仕様どおりではあります。
それでも、データファイルのバイト列は1バイトも動いていないのに、版だけが進んでいます。
これが効くのは増分処理です。
row_commit_version > <前回処理した版>で差分を取る作りにしていると、自動有効化が入った直後の1回だけ、テーブルの全行が「変更された行」として出てきます。
そして、それより前の履歴はもう区別できません。
有効化より前にコミット1で書かれた行とコミット2で書かれた行を、あとから見分ける手段はテーブルの中には残っていません。
5万ファイルでは、ALTER TABLE 1回が5コミットになる
データを書き直さないなら、残るコストはログを書く時間だけです。
ファイル数だけを変えたテーブルを作って、同じALTER TABLEを当てました。
spark.sql.files.maxRecordsPerFile=1で1ファイル1行に固定しているので、行数がそのままファイル数になります。
| ファイル数 | backfillのコミット数 | backfillが書いたJSON | 1ファイルあたり | ALTER TABLEの所要時間 |
|---|---|---|---|---|
| 100 | 1 | 36,711バイト | 367バイト | 1.67 / 1.18 / 1.12秒 |
| 1,000 | 1 | 367,912バイト | 368バイト | 1.55 / 1.24 / 1.07秒 |
| 10,000 | 1 | 3,724,913バイト | 372バイト | 1.88 / 1.42 / 1.28秒 |
| 50,000 | 3 | 18,875,842バイト | 378バイト | 3.73秒 |
所要時間はファイル数が100倍になってもほとんど動きません。
増えるのはログのバイト数で、addアクション1件あたり約370バイトがファイル数ぶん並ぶだけです。
1ファイル1行という極端な作りなので、10,000ファイルのテーブルでは_delta_logが7.06MB、parquetの合計が6.95MBと逆転していました。
実際のテーブルはファイルがもっと大きいのでこの比率はそのまま当てはまりませんが、backfillが触る量がファイル数だけで決まることは変わりません。
行数もデータ量も関係しない。
50,000ファイルで所要時間が伸びたのは、backfillが1つのコミットに収まらなかったからでした。
00000000000000000002.json : op=UPGRADE PROTOCOL | 594 バイト
00000000000000000003.json : op=ROW TRACKING BACKFILL | 8298768 バイト add 22000件
00000000000000000004.json : op=ROW TRACKING BACKFILL | 8310337 バイト add 22000件
00000000000000000005.json : op=ROW TRACKING BACKFILL | 2266737 バイト add 6000件
00000000000000000006.json : op=SET TBLPROPERTIES | 1213 バイト
22,000という数がきれいすぎたので、境界の前後でもう一度作りました。
21,999ファイルのテーブルはbackfillが1コミットで済みましたが、22,001ファイルなら22,000件と1件の2コミットに割れます。
Delta 4.3.1のbackfillは22,000ファイルごとに区切られていて、最初に見た3コミットという並びは、1回で全部のファイルを配り切れる小ささだったからそうなっていただけでした。
UPDATEを流すと、そこで実体化する
row_commit_versionが全部同じ値になったテーブルに、1行だけUPDATEを当てました。
UPDATE upd SET name = 'a2' WHERE id = 1; -- 有効化を通したテーブルの1行だけを書き換える
### UPDATE 前 ### UPDATE 後
id name row_id rcv id name row_id rcv
1 a 1 4 1 a2 1 6
2 b 0 4 2 b 0 4
行のIDは1のまま保たれていて、row_commit_versionは触った行だけが6に進みました。
row trackingが行の同一性を保つというのは、この動きのことです。
このUPDATEが書いた新しいファイルを見ると、サイズが675バイトから1486バイトに増えています。
物理スキーマを開くと理由が分かりました。
name type
id INT32
name BYTE_ARRAY
_row-id-col-b3e462ea-0077-49ab-ad5e-53d3c3598f83 INT64
_row-commit-version-col-9f201e13-dc77-400a-9497-2a9ad07efb7a INT64
テーブル設定で予約されていた_row-id-col-<uuid>が、ここで実際の列として現れました。
書き直された行はbaseRowId+位置では表せなくなるので、データの側に持たせるしかありません。
アップグレード前から在るもう1本のファイルはidとnameの2列のままです。
つまり同じテーブルの中に、行のIDをログから計算するファイルと、データに持っているファイルが混ざります。
Spark以外のエンジンがrow trackingに対応するなら、この2通りの両方を扱わないと辻褄が合いません。
_row-id-col-で始まる列を素直に返してしまうと、UPDATEされたファイルの行だけIDが見える、という中途半端な状態になります。
エンジンを書く側にはやっかいなはずです。
同じテーブルの中で行のIDの持ち方が2通りに割れていて、しかもどちらなのかはファイルを開くまで分かりません。
DuckDBもRustの2つもまだrow_idを返してこないわけですが、これは単なる未実装というより、順番として妥当な後回しに見えます。
手元のテーブルを調べて、増分処理を組み直す
自分のテーブルに有効化が来たかどうかは、履歴にROW TRACKING BACKFILLのコミットがあるかで分かります。
h = spark.sql("DESCRIBE HISTORY main.default.events") # 調べたいテーブル
h.filter("operation = 'ROW TRACKING BACKFILL'") \
.select("version", "timestamp").show(truncate=False) # 返ってきた version が境界
返ってきたバージョンが境界で、それより前に書かれた行のrow_commit_versionは全部その値に揃っています。
1行も返ってこないなら、そのテーブルにはまだ来ていません。
どれくらい出るのかを、4行のテーブルで見ました。
1行ずつ4つのコミットで入れ、有効化の直前のバージョン4を「ここまで処理した」として控えてから、自動有効化と同じALTER TABLEを当てています。
row_commit_version > 4 で拾える行: 4 / 全 4 行
有効化の前に入れた4行が、そのまま「変更された行」として出てきます。
同じ範囲をChange Data Feedで読むと、結果が変わります。
cdf = (spark.read.format("delta")
.option("readChangeFeed", "true")
.option("startingVersion", watermark + 1) # 前回処理したところの次から
.option("endingVersion", now) # 今のバージョンまで
.table("main.default.events"))
startingVersion=5 endingVersion=7 で返ってきた行数: 0
有効化が作った3つのコミットを、CDFは変更として返しませんでした。
データファイルが1つも増減していないので、返すものが無いというだけです。
そのあとに1行UPDATEを流すと、その1行ぶんだけ返ってきます。
| id|name| _change_type|_commit_version|
| 1| a| update_preimage| 8|
| 1| a2|update_postimage| 8|
ただしCDFを読んでいるあいだは、行のIDが見えなくなります。
[UNRESOLVED_COLUMN.WITH_SUGGESTION] A column, variable, or function parameter with
name `_metadata`.`row_id` cannot be resolved.
やりたいことによって、使うものが分かれます。
| やりたいこと | 使うもの |
|---|---|
| UPDATEをまたいで同じ行を追う |
_metadata.row_id。有効化の影響を受けない |
| 前回からの変更行を拾う | Change Data Feed。読んでいるあいだrow_idは見えない |
| 増分の起点を持つ | 処理したversionをテーブルの外に保存する |
3つ目が一番素朴な作りですが、有効化を通っても壊れなかったのはこれでした。
有効化を戻すと、半分だけ戻る
組み直すより止めたほうが早いのでは、とも思ったので、戻す用のテーブルを1つ作ってfalseに当ててみました。
delta.enableRowTrackingをfalseにすると、_metadata.row_idはすぐ読めなくなります。
[FIELD_NOT_FOUND] No such struct field `row_id` in `file_path`, `file_name`,
`file_size`, `file_block_start`, `file_block_length`, `file_modification_time`.
テーブルの側は戻っていませんでした。
有効化前 : minReaderVersion=1 minWriterVersion=2
tableFeatures=[appendOnly, invariants]
有効化後 : minReaderVersion=3 minWriterVersion=7
tableFeatures=[appendOnly, domainMetadata, invariants, rowTracking, v2Checkpoint]
false に戻す : minReaderVersion=3 minWriterVersion=7
tableFeatures=[appendOnly, domainMetadata, invariants, rowTracking, v2Checkpoint]
ドキュメントにも、無効化してもテーブル機能は消えずプロトコルのバージョンも下がらないと書いてあります(row trackingのドキュメント)。
falseのままにすると、行のIDは使えないのにリーダ版だけ3で止まります。
機能ごと外すならDROP FEATUREが要ります。
ALTER TABLE revert DROP FEATURE rowTracking; -- テーブル機能ごと外す
ALTER TABLE revert DROP FEATURE v2Checkpoint;
2つとも通って、minReaderVersionは1に戻りました。
ただし元通りにはならず、checkpointProtectionという別のテーブル機能が増えてminWriterVersionは7のまま残ります。
DROP FEATURE 後 : minReaderVersion=1 minWriterVersion=7
tableFeatures=[appendOnly, checkpointProtection, domainMetadata, invariants]
Auto Upgradesの側は、一度自分で無効化した機能をあとから再有効化しないと書いています。
戻すかどうかを決められるのは実質1回きりで、決めたあとは自分で管理する側に回ることになります。
所感
一番損なのは、row_commit_versionを「行が書かれた時刻の代わり」として増分処理の起点に使うことです。
自動有効化を通ったテーブルでは、それより前に書かれた行がすべて同じ版に揃います。
増分の起点が要るなら、処理したversionを外に持つ形に変えるのが確実で、そうしておけば有効化のコミットは差分として出てきません。
逆に、行の同一性そのもの(UPDATEをまたいで同じ行を追う)を使いたいだけなら、この変更の影響は受けません。
次に損なのが、慌ててdelta.enableRowTrackingをfalseにすることです。
行のIDは読めなくなるのに、リーダ版は3のまま残ります。
Spark以外から読んでいる場合は、当面何もしなくて構いません。
今回の4エンジンはどれも読めましたし、読み取りが止まる要素は見つかりませんでした。
気にするとすれば、プロトコルのminReaderVersionが1から3に上がることと、その変更がSET TBLPROPERTIESのコミットで来ること。
自作のリーダーでプロトコルのバージョンを固定で検査しているなら、そこだけ確認しておくと安全です。
測れていないことも書いておきます。
実際のDatabricks上で自動有効化が走るところは見ていないので、OSSで手で当てたのと同じ順序でコミットが並ぶかまでは確認できていません。
ファイル数を振った測定も1ファイル1行の作りなので、1ファイルが数百MBあるテーブルで同じ時間に収まるかは別に測る必要があります。
自分の環境に降りてきたあとでDESCRIBE HISTORYを眺めるのが、たぶん一番早い答え合わせになります。
調べていていちばん腑に落ちたのは、ROW TRACKING BACKFILLという専用のコミットが独立して置かれていたところでした。
行のIDを後から配るという操作をプロトコルのアップグレードとも設定の書き換えとも別のコミットに分けているので、md5を突き合わせるだけで「データには触っていない」と言い切れます。
既存のテーブルに後から機能を足すのは間違えたら被害が大きい操作ですが、何をしたかがログに1行残る形に割ってあるぶん、あとから追う側はずいぶん楽でした。
そこは良い設計だと思います。




