背景
現在、Databricks をキャッチアップ中です。
Databricks で Lakehouse を実現するための中核技術である Delta Lake について調べたので、内容を整理してみました。
Delta Lake とは何か
- CSV、Avro、Iceberg などと同じく「データフォーマット」の1つ
- Parquet / Iceberg と同様に、列指向(Column Layout) のフォーマット
行指向(Row Layout) との明確な違いを説明できる?
列指向のいいところ
- 列指向では各項目の値が1行に格納されているため、「商品」という項目にアクセスしたいとき、効率よくアクセスができる。一方で、行指向だと「商品」の項目だけを取り出したい時、「個数」「収穫日」などのお呼びでない項目をスキャンする必要がある。
Delta Lake のデータフォーマットを観察する
列指向のファイルはどうやってデータを保管・管理しているのでしょうか。実際に Parquet/ Delta Lakeファイルを作り観察をします。ステップとしては下記の3ステップです
- Docker で検証環境を準備
- ダミーデータを作成して Delta 形式で保存
- Delta Lake ファイルを観察
1. Docker で検証環境を準備
下記のdockerfile をビルドして実行
FROM apache/spark:3.5.1-python3
USER root
RUN pip install --no-cache-dir delta-spark==3.1.0
RUN mkdir -p /home/spark/.ivy2/cache && chmod -R 777 /home/spark/.ivy2
USER spark
ENTRYPOINT ["pyspark", \
"--packages", "io.delta:delta-spark_2.12:3.1.0", \
"--conf", "spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension", \
"--conf", "spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog"]
上記を実行すると下記のようにSpark環境が立ち上がります
Python 3.8.10 (default, Nov 22 2023, 10:22:35)
[GCC 9.4.0] on linux
・・・省略・・・
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/__ / .__/\_,_/_/ /_/\_\ version 3.5.1
/_/
Using Python version 3.8.10 (default, Nov 22 2023 10:22:35)
Spark context Web UI available at http://177f0d912b20:4040
Spark context available as 'sc' (master = local[*], app id = local-1762921563213).
SparkSession available as 'spark'.
2. ダミーデータを作成して Delta 形式で保存
せっかくなので先ほど図示したダミーデータを作ります。
from pyspark.sql import SparkSession
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, DateType
from datetime import date
# Delta対応のSparkSessionを作成する
spark = SparkSession.builder \
.appName("CreateDeltaTable") \
.config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \
.config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") \
.getOrCreate()
# スキーマを定義する
schema = StructType([
StructField("商品", StringType(), True),
StructField("個数", IntegerType(), True),
StructField("収穫日", StringType(), True) # 今回は "2025-09" 形式なので StringType が適切
])
# データを作成する
data = [
("りんご", 3, "2025-09"),
("バナナ", 10, "2025-10"),
("みかん", 20, "2025-11"),
("いちご", 50, "2025-12")
]
# DataFrameに変換
df = spark.createDataFrame(data, schema=schema)
生成したダミーデータを見てみます。図示したデータが表示されていますね。
>>> df.show()
+------+----+-------+
| 商品|個数| 収穫日|
+------+----+-------+
|りんご| 3|2025-09|
|バナナ| 10|2025-10|
|みかん| 20|2025-11|
|いちご| 50|2025-12|
+------+----+-------+
Delta Lake 形式で生成したダミーデータを保存します。
df.write.format("delta").mode("overwrite").save("/opt/spark/work-dir/fruitsDelta")
3. Delta Lake ファイルを観察
ダミーデータを作成したら、dockerfile で作ったコンテナを起動して、docker内に入り/opt/spark/work-dir/fruitsDeltaにファイルが作成されたことを確認します。
作成されたファイル・フォルダの一覧の確認
/opt/spark/work-dir$ ls -l /opt/spark/work-dir/fruitsDelta
実行結果
drwxr-xr-x 2 spark spark 4096 Nov 12 05:39 _delta_log
-rw-r--r-- 1 spark spark 997 Nov 12 05:39 part-00000-e91fa813-fcba-4bfc-bb1f-2e0c70edd091-c000.snappy.parquet
-rw-r--r-- 1 spark spark 997 Nov 12 05:39 part-00001-458536c0-be22-4a54-baf1-d5fd6463a76b-c000.snappy.parquet
結果を確認すると、データ本体である parquet ファイルに加えて、_delta_logというフォルダができています。
このフォルダ内に作成されたファイルこそ、Delta Lake の最大の特徴です。
Delta Lake では実行した変更が1つのトランザクションとして、_delta_logフォルダ内にjsonファイルで記録されます(1つのトランザクションのコミットにつき、1つのjsonファイルが生成されます)。
/opt/spark/work-dir/fruitsDelta/_delta_log$ ls
>> 00000000000000000000.json
00000000000000000000.jsonの中身を確認すると下記の記述がされています。
{
"commitInfo":{
"timestamp":1762925971531,
"operation":"WRITE",
"operationParameters":{
"mode":"Overwrite",
"partitionBy":"[]"
},
"isolationLevel":"Serializable",
"isBlindAppend":false,
"operationMetrics":{
"numFiles":"2",
"numOutputRows":"4",
"numOutputBytes":"1994"
},
"engineInfo":"Apache-Spark/3.5.1 Delta-Lake/3.1.0",
"txnId":"1aa3e726-9efe-4f33-8379-b193c4076b33"
}
}
{
"metaData":{
"id":"1ceb205b-42fb-4f6b-a031-a3e26868c1e5",
"format":{
"provider":"parquet",
"options":{}
},
"schemaString":"{
\"type\":\"struct\",
\"fields\":[
{\"name\":\"商品\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},
{\"name\":\"個数\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},
{\"name\":\"収穫日\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}
]}",
"partitionColumns":[],
"configuration":{},
"createdTime":1762925971021
}
}
{
"protocol":{
"minReaderVersion":1,
"minWriterVersion":2
}
}
{
"add":{
"path":"part-00000-e91fa813-fcba-4bfc-bb1f-2e0c70edd091-c000.snappy.parquet",
"partitionValues":{},
"size":997,
"modificationTime":1762925971406,
"dataChange":true,
"stats":"{
\"numRecords\":2,
\"minValues\":{\"商品\":\"りんご\",\"個数\":3,\"収穫日\":\"2025-09\"},
\"maxValues\":{\"商品\":\"バナナ\",\"個数\":10,\"収穫日\":\"2025-10\"},
\"nullCount\":{\"商品\":0,\"個数\":0,\"収穫日\":0}
}"
}
}
{
"add":{
"path":"part-00001-458536c0-be22-4a54-baf1-d5fd6463a76b-c000.snappy.parquet",
"partitionValues":{},
"size":997,"modificationTime":1762925971407,
"dataChange":true,
"stats":"{
\"numRecords\":2,
\"minValues\":{\"商品\":\"いちご\",\"個数\":20,\"収穫日\":\"2025-11\"},
\"maxValues\":{\"商品\":\"みかん\",\"個数\":50,\"収穫日\":\"2025-12\"},
\"nullCount\":{\"商品\":0,\"個数\":0,\"収穫日\":0}}"
}
}
以下、細かく中身を観察してみましょう。
commitInfo:
commitInfoの記述には実行されたトランザクションの説明が記載されています。この情報により、タイムトラベルを実現したり、監査証跡を可能にしています。
- timestamp: トランザクションが完了した時刻(コミットが完了した時刻)
⭐️ Delta Lake はこのtimestampを使ってタイムトラベルをするときの時点を指定します
-
operation : 実行された操作の種類
例) 種類としては write, append, delete, update, merge(upsert) があるが、今回は書き込みの実行なので write と記載されている -
operationParameters: どのモードで書き込まれたかを記録
例)2の手順で実行したモードはOverwriteなので、overwrite と記載されている - isolationLevel: 同時実行制御レベル
⭐️ Delta Lake はトランザクションを Serializable(一貫性が最も高い) レベルで扱う
-
isBlindAppend:書き込み時に既存データを参照・変更している稼働かを示す
- true → 既存データを読み取らずに、単に新しいファイルを追加した場合(例:mode(append))
- false → 書き込み時に既存データを参照・変更している場合 (append以外)
例)2ではmode("overwrite")で実行したためfalseと記載されている
-
operationMetrics:実行結果のメトリクス情報
例) paquetのファイル数:2、行数:4(2で作成したデータ数と一致)、書き込みバイト数:1994と記載されている -
engineInfo: 実行エンジン情報
例) どのSpark・Deltaバージョンで実行されたかが記載されている - txnId: トランザクションID(UUID形式)
{
"commitInfo":{
"timestamp":1762925971531,
"operation":"WRITE",
"operationParameters":{
"mode":"Overwrite",
"partitionBy":"[]"
},
"isolationLevel":"Serializable",
"isBlindAppend":false,
"operationMetrics":{
"numFiles":"2",
"numOutputRows":"4",
"numOutputBytes":"1994"
},
"engineInfo":"Apache-Spark/3.5.1 Delta-Lake/3.1.0",
"txnId":"1aa3e726-9efe-4f33-8379-b193c4076b33"
}
}
metaData:
metaData にはスキーマ、各項目のフォーマットなどの情報が記載されています。SQLでいうDDLに当たります。
{
"metaData":{
"id":"1ceb205b-42fb-4f6b-a031-a3e26868c1e5",
"format":{
"provider":"parquet",
"options":{}
},
"schemaString":"{
\"type\":\"struct\",
\"fields\":[
{\"name\":\"商品\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}},
{\"name\":\"個数\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}},
{\"name\":\"収穫日\",\"type\":\"string\",\"nullable\":true,\"metadata\":{}}
]}",
"partitionColumns":[],
"configuration":{},
"createdTime":1762925971021
}
}
protocol:
互換性に関する情報が記載されています。この情報を見ることで、Delta Lake がどの Spark / Delta Lake のバージョンでRead, Write ができるかがわかります。これはバージョン間の互換性の管理に役立ちます。また、古すぎるバージョンを誤って使用しないようにします(安全性の担保)。また、protocolを更新(バージョンの引き上げ)をすることで、新しい機能を有効化することもできるようです(検証して、近いうち記事に起こしたいと思います)。
{
"protocol":{
"minReaderVersion":1,
"minWriterVersion":2
}
}
add:
追加されたparquetファイルのメタデータを記録したものです。この1つ1つのparquetファイルの情報があることで、Delta Lake はロールバックや、タイムトラベル、複雑な操作を実行することができます。
{
"add":{
"path":"part-00000-e91fa813-fcba-4bfc-bb1f-2e0c70edd091-c000.snappy.parquet",
"partitionValues":{},
"size":997,
"modificationTime":1762925971406,
"dataChange":true,
"stats":"{
\"numRecords\":2,
\"minValues\":{\"商品\":\"りんご\",\"個数\":3,\"収穫日\":\"2025-09\"},
\"maxValues\":{\"商品\":\"バナナ\",\"個数\":10,\"収穫日\":\"2025-10\"},
\"nullCount\":{\"商品\":0,\"個数\":0,\"収穫日\":0}
}"
}
}
{
"add":{
"path":"part-00001-458536c0-be22-4a54-baf1-d5fd6463a76b-c000.snappy.parquet",
"partitionValues":{},
"size":997,"modificationTime":1762925971407,
"dataChange":true,
"stats":"{
\"numRecords\":2,
\"minValues\":{\"商品\":\"いちご\",\"個数\":20,\"収穫日\":\"2025-11\"},
\"maxValues\":{\"商品\":\"みかん\",\"個数\":50,\"収穫日\":\"2025-12\"},
\"nullCount\":{\"商品\":0,\"個数\":0,\"収穫日\":0}}"
}
}
まとめ
Databricks を勉強する中で、Data Lake House を実現する方法として、Delta Lake がキーワードになっていたので、手を動かしてキャッチアップしてみました (そもそもData Lake Houseとはなんぞやについてはいつか)。
Delta Lake は Parquet + トランザクションログ(_delta_log) により、信頼性の高いデータ管理を実現しているようです。_delta_logを保持することにより、タイムトラベルや、ロールバック、監査証跡を可能とし、スキーマの更新(公式ドキュメントに記載されている「Schema Evolusion」)を実現しているのですね。Databricks の概念理解に一歩近づけました。
2025/11/13 追記:
第二弾の記事「Delta Lake のトランザクションログ」を書きましたので、参考になれば幸いです。
参考
- 「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
