
Databricks 上で Delta テーブルの基本操作を試してみた
はじめに
Delta Lake について基本的な内容ですが整理しておきたく、Databricks 上で以下の操作を試してみた内容を本記事でまとめてみます。
- タイムトラベル
- RESTORE によるロールバック
- OPTIMIZE と VACUUM
- VARIANT 型の基本
- 制約(NOT NULL・CHECK)
Delta Lake の概要
従来のデータレイク上に配置した Parquet データファイルだけでは、ACID トランザクションやスケーラブルなメタデータ処理を実現できませんでした。
Delta Lake は、レイクハウスにおけるテーブルの基盤となるストレージレイヤーを提供するオープンソースのソフトウェアで、Parquet データファイルに対してファイルベースのトランザクションログを付与することで、これらの機能を実現します。
もともと Databricks が開発したプロトコルで、現在もオープンソースプロジェクトとして開発が続けられています。既存のデータレイク上で動作し、Apache Spark の API とも互換性があります。
Databricks は クラウド型の統合されたオープンな分析プラットフォームです。Delta Lake をプラットフォーム上のすべてのテーブルのデフォルト形式として採用しており、CREATE TABLE時に形式を明示的に指定しなければ、自動的に Delta 形式でテーブルが作成されます。
Delta Lake の構成
Delta テーブルの実体は、以下の2つの要素から構成されています。
- Parquet データファイル:Delta Lake は Parquet 形式でデータを書き出す
- DeltaLog(トランザクションログ):Delta テーブルへの書き込みを行うたびに、ログファイルが
_delta_logフォルダに追加される
Reader は、読み込み時にこの_delta_logを参照して最新バージョンの状態を組み立てます。この仕組みにより、以下のようなことが可能になります。
- データスキッピング:ログに保持されたファイルごとの min/max 統計により、不要なファイルの読み込みをスキップできる
- タイムトラベル:全バージョンの履歴がログに残るため、過去のある時点の状態をクエリできる
マネージドテーブルと外部テーブルの整理
Databricks(Unity Catalog)では、データの保存先に応じて、テーブルタイプが「マネージド」と「外部(External)」の2種類に分かれます。
主な違いは以下の通りです。
| マネージドテーブル | 外部テーブル | |
|---|---|---|
| 作成方法 | LOCATIONを指定せずCREATE TABLE |
CREATE TABLE ... LOCATION 's3://...'のようにLOCATIONを指定 |
| ストレージパス | Databricks 側で自動管理 | ユーザー側で指定(事前に Unity Catalog の External Location として登録が必要) |
| テーブル削除時 | メタデータ+実データ(Parquet+_delta_log)を削除対象としてマーク |
メタデータのみ削除、実データは削除されない |
前提条件
以下の環境を使用しています。
- Databricks Free Edition
- サーバーレスワークスペース
- Databricks 上のノートブックで SQL・Python セルを実行
ランタイムは以下の通りです。
> SELECT current_version()
+---------------------------------------------------------------------------------------------------------------------------+
|current_version() |
+---------------------------------------------------------------------------------------------------------------------------+
|{19.6.x-aarch64-photon-scala2.13, NULL, 9dea94816c243d7b1977f03d2e6092ddedf725b6, 7a8881535ee395968babb208dc8c00ebd4e0b547}|
+---------------------------------------------------------------------------------------------------------------------------+
検証環境
検証用に、専用のカタログ(demo_catalog)を作成しておきます。
-- 検証環境を作成
CREATE CATALOG IF NOT EXISTS demo_catalog
COMMENT 'Delta Lake検証用カタログ';
-- デフォルトのスキーマを使用
USE SCHEMA default;
また、以下の手順で外部ロケーションを作成済みであるとします。
外部テーブルを作成
マネージドテーブルの格納パスは Unity Catalog が管理しており、直接アクセスできないため、外部テーブルを作成します。
前提条件で外部ロケーションを作成済みなので、以下のコマンドで作成済みのロケーションを確認できます。
> SHOW EXTERNAL LOCATIONS;
+-------------------------+----------------------------+-------+
|name |url |comment|
+-------------------------+----------------------------+-------+
|yasuhara-test-s3-location|s3://<バケット名>/ |NULL |
+-------------------------+----------------------------+-------+
LOCATIONに、作成済みの外部ロケーション配下のパスを指定して外部テーブルを作成します。このパス配下に、実際に Parquet ファイルや_delta_logが作成されます。
-- 外部テーブルを定義
CREATE OR REPLACE TABLE demo_catalog.default.orders_external (
order_id INT,
item_name STRING,
unit_price DOUBLE,
ordered_at DATE
)
USING DELTA
LOCATION 's3://<バケット名>/external-tables/orders_external';
テーブルに二回に分けてレコードを追加します。
INSERT INTO demo_catalog.default.orders_external VALUES
(1, 'ノートPC', 128000.00, '2026-08-01'),
(2, 'ワイヤレスマウス', 2980.00, '2026-08-01'),
(3, 'USB-Cケーブル', 980.00, '2026-08-01');
INSERT INTO demo_catalog.default.orders_external VALUES
(4, 'デスクライト', 3500.00, '2026-08-02');
この状態で S3 バケットを直接確認してみます。
aws s3 ls s3://<バケット名>/external-tables/orders_external/ --recursive
出力は以下のようになります。LOCATIONで指定したパス直下に Parquet ファイル、_delta_log/配下にトランザクションログ(JSON)が作成されていることが確認できます。
s3://<バケット>/external-tables/orders_external/
2026-09-21 22:23:52 2408 external-tables/orders_external/_delta_log/00000000000000000000.crc ← テーブル作成
2026-09-21 22:23:51 1462 external-tables/orders_external/_delta_log/00000000000000000000.json ← テーブル作成
2026-09-21 22:23:59 3114 external-tables/orders_external/_delta_log/00000000000000000001.crc ← 1回目のINSERT
2026-09-21 22:23:59 1415 external-tables/orders_external/_delta_log/00000000000000000001.json ← 1回目のINSERT
2026-09-21 22:24:03 3809 external-tables/orders_external/_delta_log/00000000000000000002.crc ← 2回目のINSERT
2026-09-21 22:24:03 1409 external-tables/orders_external/_delta_log/00000000000000000002.json ← 2回目のINSERT
2026-09-21 22:23:51 0 external-tables/orders_external/_delta_log/_staged_commits/
2026-09-21 22:23:57 1593 external-tables/orders_external/part-00000-3fce0554-c2d4-4352-a785-58be1c897320.c000.snappy.parquet ← 1回目のINSERTで作成(3件)
2026-09-21 22:24:01 1509 external-tables/orders_external/part-00000-ad75a5d0-1e7e-41b9-b221-cc1fc293a25d.c000.snappy.parquet ← 2回目のINSERTで作成(1件)
ここで、JSON(DeltaLog)と Parquet(データファイル)を見ると、以下のようになっています。
- JSON:3ファイル(
00000000000000000000〜00000000000000000002)存在。テーブル作成・1回目のINSERT・2回目のINSERTという3回のトランザクションそれぞれに対応して1つずつ作られています - Parquet:2ファイル。テーブル作成自体はデータを伴わないため Parquet ファイルは生成されず、2回のINSERTそれぞれに対して1ファイルずつ作られています
JSON ファイルの内容を少し確認してみます。
aws s3 cp s3://<バケット名>/external-tables/orders_external/_delta_log/00000000000000000001.json - | jq .
以下のように、commitInfo(タイムスタンプ、operation、operation parameters)や、追加されたファイル名、そのファイルにおける統計情報(min/max)などが記録されていることが確認できます。
{
"commitInfo": {
"timestamp": 1789997037517,
"userId": "<ユーザーID>",
"userName": "<ユーザー名>",
"operation": "WRITE",
"operationParameters": {
"mode": "Append",
"statsOnLoad": false,
"partitionBy": "[]"
},
"notebook": {
"notebookId": "2201025823614008"
},
"queryHistoryStatementId": "7f44d277-add3-476a-b306-e43a2dd9ddfe",
"clusterId": "0921-125107-tjj6uws4-v2n",
"readVersion": 0,
"isolationLevel": "WriteSerializable",
"isBlindAppend": true,
"dataChange": true,
"operationMetrics": {
"numFiles": "1",
"numOutputRows": "3",
"numOutputBytes": "1593"
},
"tags": {
"noRowsCopied": "true",
"restoresDeletedRows": "false"
},
"engineInfo": "Databricks-Runtime/19.6.x-aarch64-photon-scala2.13",
"txnId": "da38bc9d-0569-423b-8cb1-e2e8c148d09c"
}
}
{
"add": {
"path": "part-00000-3fce0554-c2d4-4352-a785-58be1c897320.c000.snappy.parquet",
"partitionValues": {},
"size": 1593,
"modificationTime": 1789997037000,
"dataChange": true,
"stats": "{\"numRecords\":3,\"minValues\":{\"order_id\":1,\"item_name\":\"USB-Cケーブル\",\"unit_price\":980.0,\"ordered_at\":\"2026-08-01\"},\"maxValues\":{\"order_id\":3,\"item_name\":\"ワイヤレスマウス\",\"unit_price\":128000.0,\"ordered_at\":\"2026-08-01\"},\"nullCount\":{\"order_id\":0,\"item_name\":0,\"unit_price\":0,\"ordered_at\":0},\"tightBounds\":true}",
"tags": {
"INSERTION_TIME": "1789997037000000",
"MIN_INSERTION_TIME": "1789997037000000",
"MAX_INSERTION_TIME": "1789997037000000",
"OPTIMIZE_TARGET_SIZE": "268435456"
}
}
}
Delta テーブルでは、各トランザクション(今回であればテーブル作成・1回目の INSERT・2回目の INSERT)が、テーブルの履歴として1つずつバージョンに記録されています。これは、先述の_delta_log配下の各 JSON ファイルにそれぞれ対応しています。このバージョン履歴は以下の SQL でも確認できます。
(
spark.sql("DESCRIBE HISTORY demo_catalog.default.orders_external")
.select("version", "timestamp", "userName", "operation")
.show(truncate=False)
)
+-------+-------------------+------------------------------+-----------------------+
|version|timestamp |userName |operation |
+-------+-------------------+------------------------------+-----------------------+
|2 |2026-09-21 13:24:03|<User> |WRITE |
|1 |2026-09-21 13:23:59|<User> |WRITE |
|0 |2026-09-21 13:23:51|<User> |CREATE OR REPLACE TABLE|
+-------+-------------------+------------------------------+-----------------------+
タイムトラベル
タイムトラベルとは、テーブルの過去の任意の時点における状態をクエリできる機能のことです。Delta テーブルでは、これまで見てきたとおりトランザクションごとにバージョンが記録され、そのバージョンに対応するデータがそのまま保持されています。これにより、任意のバージョンを指定してクエリすることができます。
バージョン指定のクエリ例は以下の通りです。各バージョンに対応するデータ内容がクエリできます。
-- バージョン1 初回のインサート後
> SELECT * FROM demo_catalog.default.orders_external VERSION AS OF 1;
+--------+----------------+----------+----------+
|order_id| item_name|unit_price|ordered_at|
+--------+----------------+----------+----------+
| 1| ノートPC| 128000.0|2026-08-01|
| 2|ワイヤレスマウス| 2980.0|2026-08-01|
| 3| USB-Cケーブル| 980.0|2026-08-01|
+--------+----------------+----------+----------+
-- バージョン2:2回目のインサート後
> SELECT * FROM demo_catalog.default.orders_external VERSION AS OF 2;
+--------+----------------+----------+----------+
|order_id| item_name|unit_price|ordered_at|
+--------+----------------+----------+----------+
| 1| ノートPC| 128000.0|2026-08-01|
| 2|ワイヤレスマウス| 2980.0|2026-08-01|
| 3| USB-Cケーブル| 980.0|2026-08-01|
| 4| デスクライト| 3500.0|2026-08-02|
+--------+----------------+----------+----------+
タイムスタンプを指定したクエリも可能です。タイムスタンプの場合は、指定した時刻以前で直近にコミットされたバージョンがクエリされます。
-- タイムスタンプを指定:バージョン0のインサート前の状態が返るタイミング
> SELECT * FROM demo_catalog.default.orders_external TIMESTAMP AS OF '2026-09-21 13:23:55';
+--------+---------+----------+----------+
|order_id|item_name|unit_price|ordered_at|
+--------+---------+----------+----------+
+--------+---------+----------+----------+
オフセット指定も可能です。current_timestamp()などの関数とintervalを組み合わせることで、相対的な指定が可能です。
※TIMESTAMP AS OF は最新バージョンのコミット時刻以前の時刻しか指定できないため、算出結果がそれより後になると、エラーで拒否されます
-- 30分前の状態を指定
SELECT * FROM demo_catalog.default.orders_external TIMESTAMP AS OF current_timestamp() - interval 30 minutes;
-- 1日前の状態を指定
SELECT * FROM demo_catalog.default.orders_external TIMESTAMP AS OF date_sub(current_date(), 1);
タイムトラベルは、監査・デバッグ・ロールバックのベースとして活用できますが、後述のVACUUMによって古い Parquet ファイルが物理削除されると、それより前の時点にはさかのぼれなくなる点に注意が必要です。
タイムトラベルできる範囲を決めるテーブルプロパティ
過去のテーブルバージョンをクエリするには、そのバージョンのログファイルとデータファイルの両方が保持されている必要があります。
これは、以下の2つのテーブルプロパティで制御されます。
delta.deletedFileRetentionDuration- 現在のテーブルバージョンで参照されなくなったデータファイル(Parquet)を VACUUM で削除するまでの閾値
- デフォルトは
interval 7 days(7日間)
delta.logRetentionDuration_delta_log配下のトランザクションログ(JSON)自体の保持期間(テーブルの履歴をどのくらい保持するかを制御)- デフォルトは
interval 30 days(30日間)
上記より、タイムトラベルにおけるデフォルトのクエリ可能期間は7日間となります。この期間は、以下のように後から変更できます。
-- テーブルプロパティを変更
ALTER TABLE demo_catalog.default.orders_external
SET TBLPROPERTIES (
'delta.deletedFileRetentionDuration' = 'interval 10 days',
'delta.logRetentionDuration' = 'interval 10 days'
);
設定値を確認:
(
spark.sql("SHOW TBLPROPERTIES demo_catalog.default.orders_external")
.filter("key LIKE '%RetentionDuration%'")
.show(truncate=False)
)
+----------------------------------+----------------+
|key |value |
+----------------------------------+----------------+
|delta.deletedFileRetentionDuration|interval 10 days|
|delta.logRetentionDuration |interval 10 days|
+----------------------------------+----------------+
RESTORE によるロールバック
Delta テーブルを以前の状態に復元するにはRESTOREコマンドを使用できます。タイムトラベルクエリと同様に、以前のバージョン番号またはタイムスタンプの指定による復元がサポートされています。
ここでorders_externalテーブルに誤った変更を加えたとして、これを変更前に復元します。
まず現在の状態を確認しておきます。
> SELECT * FROM demo_catalog.default.orders_external;
+--------+----------------+----------+----------+
|order_id| item_name|unit_price|ordered_at|
+--------+----------------+----------+----------+
| 1| ノートPC| 128000.0|2026-08-01|
| 2|ワイヤレスマウス| 2980.0|2026-08-01|
| 3| USB-Cケーブル| 980.0|2026-08-01|
| 4| デスクライト| 3500.0|2026-08-02|
+--------+----------------+----------+----------+
ここで、WHERE句を付け忘れたUPDATEを実行してしまったとします。全レコードのunit_priceが0になっています。
-- WHERE句なしのUPDATE
UPDATE demo_catalog.default.orders_external SET unit_price = 0;
-- 現在の状態を確認:全件の unit_price が 0 になっている
> SELECT * FROM demo_catalog.default.orders_external;
+--------+----------------+----------+----------+
|order_id|item_name |unit_price|ordered_at|
+--------+----------------+----------+----------+
|1 |ノートPC |0.0 |2026-08-01|
|2 |ワイヤレスマウス|0.0 |2026-08-01|
|3 |USB-Cケーブル |0.0 |2026-08-01|
|4 |デスクライト |0.0 |2026-08-02|
+--------+----------------+----------+----------+
DESCRIBE HISTORYで履歴を確認し、事故前のバージョンを特定します。
※何度かテーブルプロパティを変更しており、この変更も記録されています。
(
spark.sql("DESCRIBE HISTORY demo_catalog.default.orders_external")
.select("version", "timestamp", "userName", "operation")
.show(truncate=False)
)
+-------+-------------------+------------------------------+-----------------------+
|version|timestamp |userName |operation |
+-------+-------------------+------------------------------+-----------------------+
|7 |2026-09-22 13:06:43|<User> |UPDATE |
|6 |2026-09-22 12:54:40|<User> |SET TBLPROPERTIES |
|5 |2026-09-22 07:51:56|<User> |SET TBLPROPERTIES |
|4 |2026-09-22 07:47:18|<User> |SET TBLPROPERTIES |
|3 |2026-09-22 07:27:02|<User> |SET TBLPROPERTIES |
|2 |2026-09-21 13:24:03|<User> |WRITE |
|1 |2026-09-21 13:23:59|<User> |WRITE |
|0 |2026-09-21 13:23:51|<User> |CREATE OR REPLACE TABLE|
+-------+-------------------+------------------------------+-----------------------+
UPDATEはバージョン7で発生しているため、その直前のバージョン6にRESTOREします。
-- UPDATE前のバージョンに復元
RESTORE TABLE demo_catalog.default.orders_external TO VERSION AS OF 6;
-- 現在の状態を確認:unit_price が元の値に戻っている
> SELECT * FROM demo_catalog.default.orders_external;
+--------+----------------+----------+----------+
|order_id|item_name |unit_price|ordered_at|
+--------+----------------+----------+----------+
|1 |ノートPC |128000.0 |2026-08-01|
|2 |ワイヤレスマウス|2980.0 |2026-08-01|
|3 |USB-Cケーブル |980.0 |2026-08-01|
|4 |デスクライト |3500.0 |2026-08-02|
+--------+----------------+----------+----------+
リストアできました。ここで再度バージョンを確認すると、リストア自体も1つの新しいトランザクションとして記録されます。これにより、事故が起きたバージョンの履歴が消えるわけではなく、後から何が起きたかをタイムトラベルで確認することも可能です。
ただし、変更として記録されるため、下流のジョブ等がある場合はそこに影響を与える可能性がある点に注意します。
(
spark.sql("DESCRIBE HISTORY demo_catalog.default.orders_external")
.select("version", "timestamp", "userName", "operation")
.show(truncate=False)
)
+-------+-------------------+------------------------------+-----------------------+
|version|timestamp |userName |operation |
+-------+-------------------+------------------------------+-----------------------+
|8 |2026-09-22 13:10:09|<User> |RESTORE |
|7 |2026-09-22 13:06:43|<User> |UPDATE |
|6 |2026-09-22 12:54:40|<User> |SET TBLPROPERTIES |
|5 |2026-09-22 07:51:56|<User> |SET TBLPROPERTIES |
|4 |2026-09-22 07:47:18|<User> |SET TBLPROPERTIES |
|3 |2026-09-22 07:27:02|<User> |SET TBLPROPERTIES |
|2 |2026-09-21 13:24:03|<User> |WRITE |
|1 |2026-09-21 13:23:59|<User> |WRITE |
|0 |2026-09-21 13:23:51|<User> |CREATE OR REPLACE TABLE|
+-------+-------------------+------------------------------+-----------------------+
OPTIMIZE と VACUUM
OPTIMIZE
Delta テーブルでは、DML 操作ごとに Parquet ファイルが生成されるので、ストリーミングソースを使用する場合など、小さいバッチ書き込みが積み重なるようなケースでは、大量の小さな Parquet ファイルでテーブルが構成されるようになり、読み取り性能が悪化します。
このようなケースではOPTIMIZEにより、小さなファイル群を大きなファイルへ統合できます。このファイル統合はコンパクションとも呼ばれます。
ここでは新しくorders_optim_extテーブル(外部テーブル)を作成し、1件ずつ10回に分けてINSERTすることで、小さいファイルが積み重なった状態を再現します。
CREATE OR REPLACE TABLE demo_catalog.default.orders_optim_ext (
order_id INT,
item_name STRING,
unit_price DOUBLE,
ordered_at DATE
)
USING DELTA
LOCATION 's3://<バケット名>/external-tables/orders_optim_ext';
INSERT INTO demo_catalog.default.orders_optim_ext VALUES (1, '商品01', 100.00, '2026-08-01');
INSERT INTO demo_catalog.default.orders_optim_ext VALUES (2, '商品02', 200.00, '2026-08-01');
INSERT INTO demo_catalog.default.orders_optim_ext VALUES (3, '商品03', 300.00, '2026-08-01');
INSERT INTO demo_catalog.default.orders_optim_ext VALUES (4, '商品04', 400.00, '2026-08-01');
INSERT INTO demo_catalog.default.orders_optim_ext VALUES (5, '商品05', 500.00, '2026-08-01');
INSERT INTO demo_catalog.default.orders_optim_ext VALUES (6, '商品06', 600.00, '2026-08-01');
INSERT INTO demo_catalog.default.orders_optim_ext VALUES (7, '商品07', 700.00, '2026-08-01');
INSERT INTO demo_catalog.default.orders_optim_ext VALUES (8, '商品08', 800.00, '2026-08-01');
INSERT INTO demo_catalog.default.orders_optim_ext VALUES (9, '商品09', 900.00, '2026-08-01');
INSERT INTO demo_catalog.default.orders_optim_ext VALUES (10, '商品10', 1000.00, '2026-08-01');
DESCRIBE DETAILのnumFilesでファイル数を確認すると、INSERTのたびに Parquet ファイルが作成されていることが確認できます。
# OPTIMIZE前のファイル数を確認
(
spark.sql("DESCRIBE DETAIL demo_catalog.default.orders_optim_ext")
.select("numFiles", "sizeInBytes")
.show(truncate=False)
)
+--------+-----------+
|numFiles|sizeInBytes|
+--------+-----------+
|10 |14918 |
+--------+-----------+
この状態からOPTIMIZEを実行します。
OPTIMIZE demo_catalog.default.orders_optim_ext;
再度numFilesを確認すると、10個のファイルが1個に統合されていました。
# OPTIMIZE後のファイル数を確認
(
spark.sql("DESCRIBE DETAIL demo_catalog.default.orders_optim_ext")
.select("numFiles", "sizeInBytes")
.show(truncate=False)
)
+--------+-----------+
|numFiles|sizeInBytes|
+--------+-----------+
|1 |1650 |
+--------+-----------+
OPTIMIZEもトランザクションとして記録されるため、DESCRIBE HISTORYに新しいバージョンが追加されます。統合前の10個のファイルは削除されず、ストレージに残ったままになる点もポイントです(保持期間によっては、後述のVACUUMで不要なファイルとして物理削除する対象になります)。
(
spark.sql("DESCRIBE HISTORY demo_catalog.default.orders_optim_ext")
.select("version", "timestamp", "operation")
.show(truncate=False)
)
+-------+-------------------+-----------------------+
|version|timestamp |operation |
+-------+-------------------+-----------------------+
|11 |2026-09-22 14:22:53|OPTIMIZE |
|10 |2026-09-22 14:22:44|WRITE |
|9 |2026-09-22 14:22:40|WRITE |
|8 |2026-09-22 14:22:36|WRITE |
|7 |2026-09-22 14:22:32|WRITE |
|6 |2026-09-22 14:22:28|WRITE |
|5 |2026-09-22 14:22:25|WRITE |
|4 |2026-09-22 14:22:21|WRITE |
|3 |2026-09-22 14:22:17|WRITE |
|2 |2026-09-22 14:22:13|WRITE |
|1 |2026-09-22 14:22:09|WRITE |
|0 |2026-09-22 14:22:05|CREATE OR REPLACE TABLE|
+-------+-------------------+-----------------------+
OPTIMIZEのデフォルトでは、小さなファイルは最大 1GB までのファイルにコンパクト化されます。
VACUUM
OPTIMIZEで不要になった古いファイルは、保持期間(デフォルト7日間)を過ぎるまでストレージに残り続けます。手動で削除しない限り、このファイルはVACUUMを実行しない限り残ります。VACUUMにより、これらの不要ファイルを物理的に削除できます。未使用のデータファイルを削除することで、ストレージのコストを抑えることができます。
なお、VACUUMで削除されるのはデータファイルのみで、ログファイル(JSON)はチェックポイントが生成されるたびに、ログファイルの保持期間に基づき自動的にクリーンアップされます。
先のテーブルに対してVACUUMを試してみます。テーブル作成直後のため、各ファイルはデフォルト保持期間内で削除対象とならないため、はじめにデータファイルの保持期間を変更します。
ALTER TABLE demo_catalog.default.orders_optim_ext
SET TBLPROPERTIES ('delta.deletedFileRetentionDuration' = 'interval 0 hours');
この状態でVACUUMを実行します。
-- ドライランで削除対象を確認
> VACUUM demo_catalog.default.orders_optim_ext DRY RUN;
+-------------------------------------------------------------------------------------------------------------------------------+
|path |
+-------------------------------------------------------------------------------------------------------------------------------+
|s3://<バケット名>/external-tables/orders_optim_ext/part-00000-88838cdc-9c5c-40e4-b9c1-718ef4cc8c08.c000.snappy.parquet |
|s3://<バケット名>/external-tables/orders_optim_ext/part-00000-b4bd681b-6095-45ab-9595-cf057832a810.c000.snappy.parquet |
|s3://<バケット名>/external-tables/orders_optim_ext/part-00000-730f1992-1df1-4acd-92cd-29f6dc010203.c000.snappy.parquet |
|s3://<バケット名>/external-tables/orders_optim_ext/part-00000-590eb48f-3e98-43a8-ad02-61b9b7680402.c000.snappy.parquet |
|s3://<バケット名>/external-tables/orders_optim_ext/part-00000-d649c80f-c0be-4bb7-aae3-7a0654e504b1.c000.snappy.parquet |
|s3://<バケット名>/external-tables/orders_optim_ext/part-00000-a3a5b7d6-7f24-40e3-89e6-776968f72971.c000.snappy.parquet |
|s3://<バケット名>/external-tables/orders_optim_ext/part-00000-ee8748c8-bcdb-4637-a054-ee606bef7276.c000.snappy.parquet |
|s3://<バケット名>/external-tables/orders_optim_ext/part-00000-7506f409-14f3-4491-8678-b7772b9199be.c000.snappy.parquet |
|s3://<バケット名>/external-tables/orders_optim_ext/part-00000-4686ec47-96b3-455b-ac07-798d2f780af7.c000.snappy.parquet |
|s3://<バケット名>/external-tables/orders_optim_ext/part-00000-7a5270f7-22ab-4ba2-921d-f26198926aee.c000.snappy.parquet |
+-------------------------------------------------------------------------------------------------------------------------------+
-- VACUUMを実行
VACUUM demo_catalog.default.orders_optim_ext;
VACUUMによって古いファイルが物理削除されたため、OPTIMIZEより前のバージョンへタイムトラベルしようとすると、対応するファイルが存在せずエラーになります。VACUUM は不可逆な操作である点に注意します。
> SELECT * FROM demo_catalog.default.orders_optim_ext VERSION AS OF 10;
[DELTA_UNSUPPORTED_TIME_TRAVEL_BEYOND_DELETED_FILE_RETENTION_DURATION] Cannot time travel beyond delta.deletedFileRetentionDuration (0 HOURS) set on the table. SQLSTATE: 0AKDC
DELETE / UPDATE と削除ベクトル
DELETE・UPDATEの SQL 構文自体は一般的なものと同じですが、内部実装は Databricks Runtime 12.2 LTS 以降で導入された削除ベクトル(Deletion Vector)が適用されます。
これまでは、削除対象を含むファイルを全部読み込み、削除後のデータで新しいファイルを丸ごと書き直すコピーオンライト方式でした。現在は、ファイルを書き直す代わりに「どの行が削除されたか」を記録する削除ベクトル(Deletion Vector)という小さなメタデータファイルだけを新たに作成する方式に変わっています。
ここで、先ほど作成した外部テーブル(orders_external)に対して同じDELETEを実行し、ファイルの挙動を確認してみます。
DELETE FROM demo_catalog.default.orders_external WHERE order_id = 4;
DELETE を実行した直後に、再度aws s3 lsで確認してみます。以下では絞り込み済みですが、元の Parquet ファイルはそのまま残りつつdeletion_vector_...のような名前の削除ベクトルファイルが新規作成されています。
# deletion_vector を含むファイル名だけに絞り込む
aws s3 ls s3://<バケット名>/external-tables/orders_external/ --recursive | grep "deletion_vector"
2026-09-23 10:40:59 43 external-tables/orders_external/deletion_vector_704765c2-5163-49c4-99bf-ae86389fa62d.bin
VARIANT 型
Delta Lake では、半構造化データのクエリに、VARIANT を使用することが可能で推奨されています。
以下のテーブルを例に、VARIANT 型を含むマネージドテーブルを定義し、レコードを追加します。
CREATE OR REPLACE TABLE demo_catalog.default.shipment_events (
event_id INT,
payload VARIANT
) USING DELTA;
-- 書き込み時はPARSE_JSON()でバイナリ表現に変換する
INSERT INTO demo_catalog.default.shipment_events VALUES
(1, PARSE_JSON('{"order_id":1,"status":"shipped","carrier":"YamatoTransport"}')),
(2, PARSE_JSON('{"order_id":2,"status":"delivered","carrier":"SagawaExpress","delivered_at":"2026-08-03"}'));
読み込み時は:でフィールドにアクセスし、::で目的の型にキャストできます。
> SELECT
payload,
payload:order_id::int AS order_id,
payload:status::string AS status,
payload:carrier::string AS carrier
FROM demo_catalog.default.shipment_events;
+-----------------------------------------------------------------------------------------+--------+---------+---------------+
|payload |order_id|status |carrier |
+-----------------------------------------------------------------------------------------+--------+---------+---------------+
|{"carrier":"YamatoTransport","order_id":1,"status":"shipped"} |1 |shipped |YamatoTransport|
|{"carrier":"SagawaExpress","delivered_at":"2026-08-03","order_id":2,"status":"delivered"}|2 |delivered|SagawaExpress |
+-----------------------------------------------------------------------------------------+--------+---------+---------------+
存在しないパスを指定した場合、エラーにはならずNULLが返ります。
配列の展開
VARIANT の中に配列が含まれる場合、LATERAL VIEW EXPLODEが使えます。ここでは、配送の途中経過(tracking_events)をネストした配列として持つレコードを追加してみます。
INSERT INTO demo_catalog.default.shipment_events VALUES
(3, PARSE_JSON('{
"order_id": 3,
"status": "in_transit",
"carrier": "YamatoTransport",
"tracking_events": [
{"location": "Tokyo", "status": "picked_up", "timestamp": "2026-08-05T09:00:00"},
{"location": "Osaka", "status": "in_transit", "timestamp": "2026-08-06T14:00:00"}
]
}'));
tracking_eventsは配列で、その要素自体もさらにネストした JSON(location/status/timestampを持つオブジェクト)になっています。payload:tracking_events::array<variant>で配列としてキャストし、LATERAL VIEW EXPLODEで1要素ずつ行に展開したうえで、展開後の各要素(VARIANT)に対して再度:でフィールドを取り出します。
> SELECT
event_id,
payload:order_id::int AS order_id,
tracking_event:location::string AS location,
tracking_event:status::string AS status,
tracking_event:timestamp::string AS event_timestamp
FROM demo_catalog.default.shipment_events
LATERAL VIEW EXPLODE(payload:tracking_events::array<variant>) AS tracking_event
WHERE event_id = 3;
+--------+--------+--------+-----------+-------------------+
|event_id|order_id|location|status |event_timestamp |
+--------+--------+--------+-----------+-------------------+
|3 |3 |Tokyo |picked_up |2026-08-05T09:00:00|
|3 |3 |Osaka |in_transit |2026-08-06T14:00:00|
+--------+--------+--------+-----------+-------------------+
1件のレコード(event_id = 3)が、配列の要素数(2件)分の行に展開され、それぞれのネストされたフィールドにアクセスできていることが確認できます。
Databricks 上の Delta テーブルの制約
Delta テーブルには、構造(列名・データ型)だけでなく、値レベルのビジネスルールを強制する制約を定義できます。Databricks では代表的な制約として以下の2種類をサポートしています。
- NOT NULL: 特定の列の値を null にできないように強制
- CHECK: 指定されたブール式が各入力行に対して true である必要があることを強制
主キー、外部キー、および一意性制約(UNIQUE)は設定可能ですが、情報提供が目的で強制はされません。
ここでは以下のorders_validatedテーブルを、マネージドテーブルとして作成します。
CREATE OR REPLACE TABLE demo_catalog.default.orders_validated (
order_id INT,
item_name STRING NOT NULL,
unit_price DOUBLE,
status STRING
) USING DELTA;
-- unit_priceが0以下の行の書き込みを禁止する
ALTER TABLE demo_catalog.default.orders_validated
ADD CONSTRAINT valid_unit_price CHECK (unit_price > 0);
-- statusに定義済みの値以外が書き込まれるのを禁止する
ALTER TABLE demo_catalog.default.orders_validated
ADD CONSTRAINT valid_status CHECK (status IN ('pending', 'shipped', 'delivered', 'cancelled'));
制約に違反する書き込みを試してみるとエラーになります。
> INSERT INTO demo_catalog.default.orders_validated VALUES
(1, 'ノートPC', 128000.00, 'pending');
+-----------------+-----------------+
|num_affected_rows|num_inserted_rows|
+-----------------+-----------------+
|1 |1 |
+-----------------+-----------------+
-- 違反する書き込み
> INSERT INTO demo_catalog.default.orders_validated VALUES
(2, 'ワイヤレスマウス', -500.00, 'pending');
[DELTA_VIOLATE_CONSTRAINT_WITH_VALUES] CHECK constraint valid_unit_price (unit_price > 0) violated by row with values:
- unit_price : -500.0. SQLSTATE: 23001
制約の一覧はSHOW TBLPROPERTIESで確認できます。
SHOW TBLPROPERTIES demo_catalog.default.orders_validated;
既存データがある状態で制約を後から追加する場合は、追加前に既存の全行が制約を満たすか検証されます。
さいごに
Databricks 上で Delta Lake によるテーブルの作成・タイムトラベルなどの基本操作を試してみました。
こちらの内容がどなたかの参考になれば幸いです。






