背景
現在、Databricks をキャッチアップ中で、Databricks で Lakehouse を実現するための中核技術である Delta Lake について調べています。その調査・勉強した内容を記事に起こすぞ!の第二弾です。
この記事を読む前に、第一弾の記事「Databricks - Delta Lake のデータフォーマットを理解する」を読んでおくと、より理解しやすいと思います。
Delta Lake のトランザクションログとは何か
第一弾の記事にも記載した通り、Delta Lake のトランザクションログ は Databricks が Lakehouse を実現するうえで極めて重要な役割を担っています。
このトランザクションログは、
- Delta Lake の ACIDトランザクション保証
- タイムトラベル(過去の状態の再現)
- データの信頼性と一貫性
といった機能を支える「変更履歴の台帳」です。
新しい Delta Lake テーブルを作成すると、最初のトランザクションログが _delta_log フォルダ内に生成されます。以降、そのテーブルに対して行われたすべての操作(書き込み・更新・削除など)がこのログに追記されていきます。
Delta Lake で作成したテーブルを操作してトランザクションログの変化を観察する
Delta Lakeで作成したテーブルに対して、アクションを行ったとき、トランザクションログはどのようにそのアクションを記録するのでしょうか。今回は、第一弾の記事で作成した「フルーツ」テーブルに対して更新操作を行い、その操作がどのようにアクションログに記録されたかを観察してみます。
フルーツの Delta Lake テーブルを使って、下記の手順で観察します。
- Dockerで検証環境を準備する
- ダミーデータを作成してDelta形式で保存する
- 商品の「いちご」を「プレミアムいちご」に変更する処理を実行する
- トランザクションログを観察する
1. Dockerで検証環境を準備する (別記事参照)
第一弾の記事の「Dockerで検証環境を準備する」の手順を参照してください。
2.ダミーデータを作成してDelta形式で保存する(別記事参照)
第一弾の記事の「ダミーデータを作成してDelta形式で保存する」の手順を参照してください。
作成直後の _delta_log の構成は以下のようになります。
opt/spark/work-dir/fruitsDelta
├─ _delta_log
| └─ 00000000000000000000.json <--トランザクションログ
├─ part-00000-55ed8456-afe3-4571-b7ba-0f50d3188477-c000.snappy.parquet
└─ part-00000-e91fa813-fcba-4bfc-bb1f-2e0c70edd091-c000.snappy.parquet
3. 商品の「いちご」を「プレミアムいちご」に変更する処理を実行する
from delta.tables import DeltaTable
from pyspark.sql.functions import lit
delta_table = DeltaTable.forPath(spark, "/opt/spark/work-dir/fruitsDelta")
delta_table.update( condition="`商品`='いちご'", set={"商品": lit("プレミアムいちご")})
- memo
- Spark SQLは日本語の列名をサポートしていないので、列名をバッククォートで囲む必要があります
実行結果
商品の「いちご」が「プレミアムいちご」に更新されていることが確認できます
>>> spark.read.format("delta").load("/opt/spark/work-dir/fruitsDelta").show()
+----------------+----+-------+
| 商品|個数| 収穫日|
+----------------+----+-------+
| みかん| 20|2025-11|
| プレミアムいちご| 50|2025-12|
| りんご| 3|2025-09|
| バナナ| 10|2025-10|
+----------------+----+-------+
4. トランザクションログを観察する
docker に入りファイルを確認すると、新しくpart-00001と冒頭についたファイルが追加されています。また、トランザクションログが格納された_delta_logフォルダの中身を確認すると、新しいトランザクションファイルが追加されたことを確認できます。
opt/spark/work-dir/fruitsDelta
├─ _delta_log
| ├─ 00000000000000000000.json
| └─ 00000000000000000001.json <--新しいトランザクションログ
├─ part-00000-55ed8456-afe3-4571-b7ba-0f50d3188477-c000.snappy.parquet
├─ part-00000-e91fa813-fcba-4bfc-bb1f-2e0c70edd091-c000.snappy.parquet
└─ part-00001-458536c0-be22-4a54-baf1-d5fd6463a76b-c000.snappy.parquet <-更新されたデータ本体のファイル
新しく追加されたトランザクションログのjsonファイル(00000000000000000001.json)の中身を観察してみましょう。commitInfo, add, remove の3つのエントリが確認できます。
{
"commitInfo":{
"timestamp":1763002526921,
"operation":"UPDATE",
"operationParameters":{
"predicate":"[\"(商品#2016 = いちご)\"]"},
"readVersion":0,
"isolationLevel":"Serializable",
"isBlindAppend":false,
"operationMetrics":{
"numRemovedFiles":"1",
"numRemovedBytes":"997",
"numCopiedRows":"1",
"numDeletionVectorsAdded":"0",
"numDeletionVectorsRemoved":"0",
"numAddedChangeFiles":"0",
"executionTimeMs":"2138",
"numDeletionVectorsUpdated":"0",
"scanTimeMs":"2053",
"numAddedFiles":"1",
"numUpdatedRows":"1",
"numAddedBytes":"1047",
"rewriteTimeMs":"84"
},
"engineInfo":"Apache-Spark/3.5.1 Delta-Lake/3.1.0",
"txnId":"387802de-1217-4ff9-ba38-39e55e8e3bda"
}
}
{
"add":{
"path":"part-00000-55ed8456-afe3-4571-b7ba-0f50d3188477-c000.snappy.parquet",
"partitionValues":{},
"size":1047,
"modificationTime":1763002526914,
"dataChange":true,
"stats":"{
\"numRecords\":2,
\"minValues\":{
\"商品\":\"みかん\",
\"個数\":20,
\"収穫日\":\"2025-11\"
},
\"maxValues\":{
\"商品\":\"プレミアムいちご\",
\"個数\":50,
\"収穫日\":\"2025-12\"
},
\"nullCount\":{
\"商品\":0,
\"個数\":0,
\"収穫日\":0
}
}"
}
}
{
"remove":{
"path":"part-00001-458536c0-be22-4a54-baf1-d5fd6463a76b-c000.snappy.parquet",
"deletionTimestamp":1763002526918,
"dataChange":true,
"extendedFileMetadata":true,
"partitionValues":{},
"size":997
}
}
commitInfoを観察
まず初めに、commitInfoを観察します。
Delta Lake テーブルを新規作成したときのトランザクションログと異なり、commitInfoのoperationMetricsに記録されている項目が増えています。今回は更新(UPDATE)操作が行われたため、operationMetrics に新しい項目が追加されています。
{
"commitInfo":{
"timestamp":1763002526921,
"operation":"UPDATE",
"operationParameters":{
"predicate":"[\"(商品#2016 = いちご)\"]"},
"readVersion":0,
"isolationLevel":"Serializable",
"isBlindAppend":false,
"operationMetrics":{
"numRemovedFiles":"1",
"numRemovedBytes":"997",
"numCopiedRows":"1",
"numDeletionVectorsAdded":"0",
"numDeletionVectorsRemoved":"0",
"numAddedChangeFiles":"0",
"executionTimeMs":"2138",
"numDeletionVectorsUpdated":"0",
"scanTimeMs":"2053",
"numAddedFiles":"1",
"numUpdatedRows":"1",
"numAddedBytes":"1047",
"rewriteTimeMs":"84"
},
"engineInfo":"Apache-Spark/3.5.1 Delta-Lake/3.1.0",
"txnId":"387802de-1217-4ff9-ba38-39e55e8e3bda"
}
}
| テーブルを新規作成したとき | 今回のトランザクションログ | 各メトリクスの説明 | 例 |
|---|---|---|---|
| numFiles | - | Parquetのファイル数 | |
| numOutputRows | - | テーブルに登録された行数 | |
| numOutputBytes | - | テーブルに登録されたデータの合計サイズ | |
| - | numRemovedFiles | 削除された Parquet ファイルの数。 | 「いちご」を含む1つのファイルが削除された |
| - | numRemovedBytes | 削除されたファイルの合計サイズ | 997 bytes |
| - | numCopiedRows | 更新処理中に「変更が不要だったが同じファイル内にあったため再コピーされた行」数。UPDATE や MERGE 処理で一部の行のみが変更されたときに、残りの行が「再コピー」としてカウントされます。 | 1 ※更新ファイル内に他に1行あり、それも再書き込みされた |
| - | numDeletionVectorsAdded | 削除ベクトルの概念)物理的にファイルを削除せず、論理的に削除をマークした場合に増える値。この機能は Delta Lake 3.0+ の Deletion Vectors(DV) で利用されます。 | 0 ※今回はファイルを再書き込みしたので使用なし |
| - | numDeletionVectorsRemoved | 削除ベクトルが削除された数。Deletion Vectors 方式で削除されたマークを消した場合にカウントされる。 | 0 |
| - | numAddedChangeFiles | CDC(Change Data Capture)用に生成された「変更ログファイル」の数。CDCを有効にしている場合にのみ増加します。 | 無効 |
| - | executionTimeMs | トランザクション全体の実行時間 | 2138 ms |
| - | numDeletionVectorsUpdated | Deletion Vector(DV)に更新操作が行われた場合にカウントされます。物理ファイルの再生成なしで更新が行われた場合に使用。 | 0 |
| - | scanTimeMs | スキャンにかかった時間 | 2053 ms ※ほとんどの時間がスキャンに費やされた |
| - | numAddedFiles | 新しく生成された Parquet ファイルの数。更新されたデータを含む新ファイルとして保存されます。 | 1 |
| - | numUpdatedRows | 実際に変更された行数 | 「いちご」の値を含んだ1行のみが更新対象 |
| - | numAddedBytes | 新たに生成されたファイルの合計サイズ | 1047 bytes |
| - | rewriteTimeMs | 再書き込みにかかった時間 | 84 ms |
addを観察
ここに記載されているのは、更新後に生成された新しいParquetファイル のメタデータです。dataChange の項目を確認すると true が立っており、実際にデータ変更があったことを示しています。
{
"add":{
"path":"part-00000-55ed8456-afe3-4571-b7ba-0f50d3188477-c000.snappy.parquet",
"partitionValues":{},
"size":1047,
"modificationTime":1763002526914,
"dataChange":true,
"stats":"{
\"numRecords\":2,
\"minValues\":{
\"商品\":\"みかん\",
\"個数\":20,
\"収穫日\":\"2025-11\"
},
\"maxValues\":{
\"商品\":\"プレミアムいちご\",
\"個数\":50,
\"収穫日\":\"2025-12\"
},
\"nullCount\":{
\"商品\":0,
\"個数\":0,
\"収穫日\":0
}
}"
}
}
ここで、このパートには「みかん」と「プレミアムいちご」のデータの情報しかなく、「バナナ」と「りんご」のデータが記載されていないことに疑問を持つかもしれません。実は、今回のデータは2つのparquetファイルに分かれています。
つまり、今回の商品「いちご」→「プレミアムいちご」に更新をしたい場合、「みかん」「いちご」のデータが入ったParquetファイルのみを更新すれば良いというわけです。「りんご」と「バナナ」は別の Parquet ファイルに格納されているため、更新対象外でした。
これにより、トランザクションログには1つのparquetファイルに対するアクションのログのみが記載されています。
removeを観察
このパートはremove処理について記載されていますね。対象となったParquetファイルはpart-00001-458536c0<省略>というファイルです。これは、今回更新処理をしたときに新しく追加されたParquetファイルでした。
{
"remove":{
"path":"part-00001-458536c0-be22-4a54-baf1-d5fd6463a76b-c000.snappy.parquet",
"deletionTimestamp":1763002526918,
"dataChange":true,
"extendedFileMetadata":true,
"partitionValues":{},
"size":997
}
}
新しく追加されたこのParquetファイルには何が記載されているのでしょうか。
実は、このファイルには 「いちご」 が含まれていた旧データが格納されています。すなわち、「プレミアムいちご」へ変更する前のデータです。Delta Lake は、 元々のデータが格納された Parquetファイルを更新する代わりに、更新前のデータをバックアップして新しいParquetファイルを作成し追加しています。
このファイルはタイムトラベル機能を実現するために削除自体はされず、保持されます。
removeと記載されていますが、新しく追加された Parquet ファイルを削除したという意味ではないので注意してください。このファイルを最新バージョンのテーブル情報としては扱わない処理をしたということです。
まとめ
今回の記事では、Delta Lake のトランザクションログがどのように更新を記録しているかを実際に観察してみました。
新処理を行うと、Delta Lake は既存データを直接上書きせず、古いデータを無効化(remove)し、新しいデータを追加(add) することで整合性を保っています。
この仕組みにより、ACID トランザクションやタイムトラベルなどの高信頼なデータ管理が実現されているようです。第三弾の記事では、このログを使った バージョン管理やタイムトラベルの仕組みをしてみようかなと思います。
11/17 追記
参考
- 「delta lake徹底入門」 – 2025/5/28 Bennie Haelen (原著), Dan Davis (原著), 長谷川 亮 (翻訳), 倉光 怜 (翻訳), 竹下 俊一郎 (翻訳)
- Diving Into Delta Lake: Unpacking The Transaction Log: https://www.databricks.com/blog/2019/08/21/diving-into-delta-lake-unpacking-the-transaction-log.html

