Databricks にて Hadoop Catalog の Apache Iceberg をさわってみた


## 概要

本記事では、Databricks 上で Hadoop Catalog を利用して Apache Iceberg（以下、Iceberg）を扱う基本的な手順を紹介します。ただし、ここで紹介する内容は Databricks 上で Iceberg を推奨することを目的としたものではありません。

なお、Iceberg ではテーブルを管理するための**カタログ**が必須となりますが、今回の検証ではストレージだけで完結する **Hadoop Catalog** を用いています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f6949a05-c8e8-4071-8dd1-f8aa14b605fb.png)

> 引用元: [Configuration - Apache Iceberg™](https://iceberg.apache.org/docs/latest/spark-configuration/#catalog-configuration)

いくつかの操作は Databricks の仕様上エラーとなりましたが、そのまま掲載しています。Apache Iceberg は複数のサービスで動作することを想定していますが、実際にはサービスベンダーが機能をサポート・保証しているかを確認する必要があります。Spark ベースのプラットフォームであっても必ずしも動作が保証されるわけではないので、利用する際には必ず検証を行うことをおすすめします。

## クラスターの作成

### クラスターの設定について

Databricks 上で Apache Iceberg を動かすには、以下のような設定を行ったクラスターを用意する必要があります。

- Databricks Runtime のバージョンを 12.2LTS 以前にすること
- アクセスモードを分離なし共有（Uniyt Catalog 利用不可のモード」）にすること
- Java 8 に対応したライブラリ（Iceberg 1.6.1 以前のライブラリ）を利用すること
- Spark 構成に Iceberg 関連の設定を実施

これらの設定にたどり着くまでに、以下のようなエラーに遭遇しました（参考リンク参照）:

- [Databricks 上で Hadoop Catalog により Apache Iceberg を実行する際の Failed to find data source: ICEBERG への対応方法 #Spark - Qiita](https://qiita.com/manabian/items/60327ea87fc3e018dcef)
- [Databricks 上で Hadoop Catalog により Apache Iceberg を実行する際の Multiple sources found for ICEBERG #Spark - Qiita](https://qiita.com/manabian/items/4c2c78c7db77f704e5ab)
- [Databricks 上で Hadoop Catalog により Apache Iceberg を実行する際の Java のバージョンによるエラーへの対応方法 #Spark - Qiita](https://qiita.com/manabian/items/35a288804d4695565338)

### クラスター構築時の設定

#### 1. Databricks Runtime にて 12.2 を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/4324f0d6-d98e-4397-9650-1f02ded050fd.png)

#### 2. アクセスモードを「分離なし共有」に設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/3791dd9b-c781-48f0-b17c-4441f70589dc.png)

#### 3. Spark 構成に Iceberg 関連の設定を追加

`spark.sql.catalog.spark_catalog.warehouse` に指定したディレクトリに、テーブルのデータが書き込まれます。

```text
spark.sql.catalog.spark_catalog = org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.spark_catalog.type = hadoop
spark.sql.catalog.spark_catalog.warehouse = dbfs:/Iceberg
spark.sql.extensions = org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/e7d59ec4-1de0-47d5-94a9-8a744b404179.png)

#### 4. Iceberg 関連ライブラリ (`org.apache.iceberg:iceberg-spark-runtime-3.3_2.13:1.6.1`) をインストール

クラスターの「ライブラリ」タブで「新規インストール」を選択し、「ライブラリをインストール」ウィンドウから「Maven」を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/5f496302-c98d-4289-a5fe-281c20951883.png)

`org.apache.iceberg iceberg-spark-runtime` と入力すると、バージョン一覧が表示されます。ここでは `org.apache.iceberg:iceberg-spark-runtime-3.3_2.13` の `1.6.1` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/224f7b07-876d-4396-b416-255a2d0679cd.png)

`org.apache.iceberg:iceberg-spark-runtime-3.3_2.13:1.6.1` であることを確認して「インストール」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/d73020ea-2005-4692-9d6e-70f0bdf478e5.png)


## Apache Iceberg の基本的な操作

### 1. Namespace の作成

#### CREATE NAMESPACE 文の実行

Iceberg 用のデータベース（Namespace）を作成します。

```sql
-- データベース(Namespace)を作成する
CREATE NAMESPACE IF NOT EXISTS iceberg_demo;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/2b2bca5e-29c8-4d6c-98ba-6f13e22d938e.png)

#### `iceberg_demo` ディレクトリが作成されたことを確認

以下の Python コードを実行すると、Spark の設定から warehouse ディレクトリのパスを取得し、その中に作成された Namespace 用ディレクトリをリストで確認できます。`dbfs:/Iceberg` 配下に `iceberg_demo` ディレクトリができていれば OK です。

```python
%python
wh_dir = spark.conf.get("spark.sql.catalog.spark_catalog.warehouse")
display(dbutils.fs.ls(wh_dir))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/55f440a3-78fb-4964-b825-27e5b75ca9df.png)



### 2. テーブルの作成

#### CREATE TABLE 文の実行

先ほど作成した Namespace 内に `first_table` を作成します。

```sql
CREATE OR REPLACE TABLE iceberg_demo.first_table(
    id INT,
    name STRING,
    value INT
)
USING iceberg;
```

#### `first_table` ディレクトリが作成されたことを確認

Namespace 内にテーブル用ディレクトリが追加されているか、以下のコードで確認します。

```python
%python
wh_dir = spark.conf.get("spark.sql.catalog.spark_catalog.warehouse")
namspace_name = "iceberg_demo"
table_data_dir = f"{wh_dir}/{namspace_name}"
display(dbutils.fs.ls(table_data_dir))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/04fd7206-d252-4c1e-953b-ce3bfe656c25.png)



### 3. データの書き込み

#### INSERT 文の実行

`first_table` にデータを挿入します。

```sql
INSERT INTO
  iceberg_demo.first_table (id, name, value)
VALUES (1, 'sample', 100), (2, 'example', 200);
```

```sql
SELECT
  *
FROM
  iceberg_demo.first_table
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/66b751e8-f542-4650-bc52-c9d38f6b2714.png)

#### UPDATE 文の実行

下記のように `UPDATE` を実行するとエラーになります。

```sql
UPDATE
  iceberg_demo.first_table
SET
  value = 150
WHERE
  id = 1;
```

> DeltaAnalysisException: UPDATE destination only supports Delta sources.
> Some(RelationV2[id#108, name#109, value#110] spark_catalog.iceberg_demo.first_table spark_catalog.iceberg_demo.first_table )

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/d0aa37db-5e0a-4c97-8fff-c0a493d39bef.png)

#### DELETE 文の実行

`id` が `2` のレコードを削除します。

```sql
DELETE FROM
  iceberg_demo.first_table
WHERE
  id = 2;
```

```sql
SELECT
  *
FROM
  iceberg_demo.first_table
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/5613bfea-ca33-4f04-a048-2547b18e31f2.png)

#### MERGE 文

下記のような `MERGE` 文を実行するとエラーになります。

```sql
-- 書き込み用データを準備
CREATE OR REPLACE TEMPORARY VIEW updates_table AS
SELECT
  1 AS id,
  'updated_sample' AS name,
  200 AS value
UNION ALL
SELECT
  3 AS id,
  'new_entry' AS name,
  300 AS value;
```

```sql
MERGE INTO
  iceberg_demo.first_table AS target
USING
  updates_table AS source
ON
  target.id = source.id
WHEN MATCHED THEN UPDATE SET target.name = source.name, target.value = source.value
WHEN NOT MATCHED THEN INSERT (id, name, value) VALUES (source.id, source.name, source.value);
```

> DeltaAnalysisException: MERGE destination only supports Delta sources.



### TRUNCATE TABLE 文

テーブル内の全データを削除します。

```sql
TRUNCATE TABLE iceberg_demo.first_table;
```

```sql
SELECT
  *
FROM
  iceberg_demo.first_table
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/0e4c2a6d-cc0e-43de-be5b-2a3867616810.png)



### 4. タイムトラベル

#### 事前準備

タイムトラベルを確認するため、新しいテーブルを作成し、2 回に分けてデータを挿入します。

```sql
CREATE OR REPLACE TABLE iceberg_demo.timestravel(
  id INT,
  data STRING
)
USING iceberg;
```

```sql
INSERT INTO
  iceberg_demo.timestravel
VALUES (1, 'first'), (2, 'first');
```

```sql
INSERT INTO
  iceberg_demo.timestravel
VALUES (3, 'second'), (4, 'second');
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/a67febc5-c1ab-4616-be9e-48f5cfde4f71.png)

#### `snapshot-id` の取得

Iceberg でテーブルの履歴を確認するための SQL は Databricks 上でエラーになります。

```sql
SELECT
  *
FROM
  iceberg_demo.timestravel.history;
```

そのためテーブルのディレクトリにある `metadata` ファイルを直接表示し、1 回目の `INSERT` で生成された `snapshot-id` (例: `5355881699654019663`) を取得します。

```python
%python
wh_dir = spark.conf.get("spark.sql.catalog.spark_catalog.warehouse")
namspace_name = "iceberg_demo"
table_name = "timestravel"
table_data_dir = f"{wh_dir}/{namspace_name}/{table_name}/metadata/v2.metadata.json"
print(dbutils.fs.head(table_data_dir))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/55bcbd59-602a-4fc7-ba49-e96e9d9c05fc.png)

#### `snapshot-id` を指定したクエリの実行

取得した `snapshot-id` を `VERSION AS OF` の後に指定して実行します。

```sql
SELECT
  *
FROM
  iceberg_demo.timestravel VERSION AS OF 5355881699654019663
```

```sql
SELECT
  *
FROM
  iceberg_demo.timestravel;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/3fccff9a-9dcf-4de4-b42f-711b348169ff.png)



### 5. スキーマ進化（Schema Evolution）

#### 事前準備

スキーマ進化を試すためのテーブルを作成します。

```sql
CREATE OR REPLACE TABLE iceberg_demo.schema_evo(
  id BIGINT,
  product STRING,
  quantity INT,
  price DECIMAL(10, 2)
)
USING iceberg;
```

#### カラムの追加

既存テーブルに新しいカラム `new_col` を追加します。

```sql
ALTER TABLE
  iceberg_demo.schema_evo
ADD
  COLUMNS (
    new_col STRING
    COMMENT '新しく追加するカラム'
  );
```

```sql
DESC iceberg_demo.schema_evo;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/3a87ba10-17d7-4dec-aeb4-e94afede91b1.png)

#### カラムの削除

先ほど追加した `new_col` を削除します。

```sql
ALTER TABLE
  iceberg_demo.schema_evo
DROP COLUMNS (
  new_col
);
```

```sql
DESC iceberg_demo.schema_evo;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/00aa1df4-46be-4559-b364-4937638b0c7a.png)

#### カラム名の変更

カラム名を変更します。

```sql
ALTER TABLE
  iceberg_demo.schema_evo
RENAME COLUMN
  id
TO
  `アイディ`;
```

```sql
DESC iceberg_demo.schema_evo;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/bef58fa2-cd54-42b1-9e46-99e51fe16385.png)

#### カラムのデータ型変更

既存カラムのデータ型を変更します。

```sql
ALTER TABLE
  iceberg_demo.schema_evo
ALTER COLUMN
  quantity
  TYPE BIGINT;
```

```sql
DESC iceberg_demo.schema_evo;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/4446a4ad-0865-4b87-8ec3-84e4865e1435.png)



### 6. パーティションの設定

#### パーティションを設定したテーブルの作成

`order_date` 列の日付を基準にパーティションを作成し、データを挿入します。

```sql
CREATE OR REPLACE TABLE iceberg_demo.partition_table(
  id BIGINT,
  customer_id BIGINT,
  order_date TIMESTAMP,
  total_amount DOUBLE
) USING ICEBERG
PARTITIONED BY (day(order_date));
```

```sql
INSERT INTO
  iceberg_demo.partition_table
VALUES
  (1, 101, CAST('2024-02-01 12:00:00' AS timestamp), 100.50),
  (2, 102, CAST('2024-02-02 14:30:00' AS timestamp), 200.75),
  (3, 103, CAST('2024-02-02 16:45:00' AS timestamp), 150.25);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/eda29f6e-5e60-4d0b-b7f9-e90ce72f0eea.png)

#### パーティションの設定を確認

Databricks 上では、Iceberg で利用可能なパーティション情報の表示 SQL はエラーになります。

```sql
SHOW PARTITIONS iceberg_demo.partition_table;
```

> AnalysisException: Table spark_catalog.iceberg_demo.partition_table does not support partition management.; line 1 pos 16;  
> ShowPartitions [partition#560]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/5a6defe8-a0e7-4b17-875e-6c21511a741d.png)

そのため、テーブルの `data` ディレクトリを直接確認します。`order_date_day=2024-02-01` と `order_date_day=2024-02-02` にディレクトリが分かれていることがわかります。TIMESTAMP 型カラムを `day(order_date)` と指定したので、日付単位でパーティションが作られています。

```python
%python
wh_dir = spark.conf.get("spark.sql.catalog.spark_catalog.warehouse")
namspace_name = "iceberg_demo"
table_name = "partition_table"
table_data_dir = f"{wh_dir}/{namspace_name}/{table_name}/data"
display(dbutils.fs.ls(table_data_dir))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/0f40f42b-ec29-4251-9459-f380f287d284.png)

#### パーティション進化（Partition evolution）の実施

Iceberg が提供するパーティション進化（Partition evolution）を Databricks 上で試そうとすると、以下の通りエラーが発生します。

```sql
ALTER TABLE iceberg_demo.partition_table
  REPLACE PARTITION
  FIELD day(order_date)
  WITH bucket(16, customer_id)
;
```

> ParseException:  
> [PARSE_SYNTAX_ERROR] Syntax error at or near 'PARTITION'.(line 1, pos 49)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/3b653ab2-1c9d-4651-a0ac-1f926655018d.png)



### 7. CoW（Copy on Write）と MoR（Merge on Read）の動作検証

#### CoW と MoR について

Iceberg ではテーブルの行レベル更新や削除の処理方式として **Copy-On-Write (CoW)** と **Merge-On-Read (MoR)** の 2 つをサポートしています。それぞれ次のような特徴があります。MoR 方式を選択した場合は定期的なコンパクション（Compaction）が必要になり、運用コストが上がる点に注意が必要です。

- **Copy-On-Write (CoW)**  
  更新・削除が発生すると、該当のデータファイル全体をコピーして書き換え、新しいファイルを作成する方式。  
  - **メリット**: 読み取り時は常に最新のファイルのみを参照すればよく、シンプルで高速  
  - **デメリット**: 書き込み時にファイルの再書き出しが必要になるため、更新負荷が高い

- **Merge-On-Read (MoR)**  
  既存のデータファイルはそのまま残し、更新・削除データを新規ファイル（および削除ファイル）として書き込む方式。  
  - **メリット**: 書き込み時は既存ファイルを直接書き換えないため高速  
  - **デメリット**: 読み取り時にマージ処理が必要となり、ファイル数が増えるほどパフォーマンスに影響する可能性がある

#### CoW テーブルと MoR テーブルの準備

下記のように、CoW / MoR それぞれのテーブルを作成してデータ挿入・削除を実行します。

```sql
-- CoW モードのテーブル作成
CREATE TABLE iceberg_demo.sample_cow (
  id INT,
  name STRING
)
USING iceberg
TBLPROPERTIES (
  'format-version' = '2',
  'write.delete.mode' = 'copy-on-write',
  'write.update.mode' = 'copy-on-write',
  'write.merge.mode' = 'copy-on-write'
);

-- MoR モードのテーブル作成
CREATE TABLE iceberg_demo.sample_mor (
  id INT,
  name STRING
)
USING iceberg
TBLPROPERTIES (
  'format-version' = '2',
  'write.delete.mode' = 'merge-on-read',
  'write.update.mode' = 'merge-on-read',
  'write.merge.mode' = 'merge-on-read'
);
```

```sql
-- CoW テーブルにデータ挿入
INSERT INTO iceberg_demo.sample_cow VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie');

-- MoR テーブルにデータ挿入
INSERT INTO iceberg_demo.sample_mor VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie');
```

```sql
-- CoW テーブルでの DELETE
DELETE FROM iceberg_demo.sample_cow WHERE id = 2;

-- MoR テーブルでの DELETE
DELETE FROM iceberg_demo.sample_mor WHERE id = 2;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/ffb16429-beb2-40b8-b1d2-9af7d0a55b82.png)

#### ファイル内容を確認

Iceberg では下記のようにファイル状況を確認する SQL をサポートしていますが、Databricks ではエラーが発生します。

```sql
-- CoW テーブルのファイル状況を確認
SELECT content, file_path
FROM iceberg_demo.sample_cow.files;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/23355697-c5b6-47ea-8624-d4625e7660fe.png)

そこで data のディレクトリを確認します。MoR のテーブルにて`-deletes.parquet`という名称で終わる Parquet 形式のファイルを確認できませんでした。Databricks 上では MoR が意図したとおりには動作していないようです。

```python
%python
wh_dir = spark.conf.get("spark.sql.catalog.spark_catalog.warehouse")
namspace_name = "iceberg_demo"
table_name = "sample_cow"
table_data_dir = f"{wh_dir}/{namspace_name}/{table_name}/data"
display(dbutils.fs.ls(table_data_dir))
```

```python
%python
wh_dir = spark.conf.get("spark.sql.catalog.spark_catalog.warehouse")
namspace_name = "iceberg_demo"
table_name = "sample_mor"
table_data_dir = f"{wh_dir}/{namspace_name}/{table_name}/data"
display(dbutils.fs.ls(table_data_dir))
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/583d4c57-b71e-4370-800b-b86ed34eef12.png)

#### Compaction の実施

Iceberg では Compaction（データファイルの再マージ処理）を行う SQL が用意されていますが、Databricks 上で実行するとエラーとなります。

```sql
CALL iceberg.system.rewrite_data_files ('iceberg_demo.sample_mor')
```

> ParseException:  
> [PARSE_SYNTAX_ERROR] Syntax error at or near 'CALL'.(line 1, pos 0)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/a9c9869c-f620-4485-ab18-eca2c71db2d8.png)

# まとめ

本記事では、Databricks 環境上で Hadoop Catalog を利用して Apache Iceberg を動作させる手順や、その際に発生するエラー・制限事項などを紹介しました。Iceberg ではテーブルのバージョン管理やスナップショット機能、スキーマ進化、パーティション進化、そして Copy-On-Write (CoW) / Merge-On-Read (MoR) といった高度なテーブル管理機能が利用できます。しかし、Databricks との組み合わせにおいては、Iceberg が本来備えている機能の一部がサポートされていなかったり、エラーが発生したりする状況があるようです。

実際に利用する際には、以下の点に留意するとよいでしょう。

- **クラスター構成**  
  Databricks Runtime のバージョンやアクセスモード、ライブラリのバージョン設定が重要で、正しく設定しないと Iceberg を認識できません。
- **Databricks との互換性**  
  UPDATE や MERGE などの SQL 操作、パーティション管理や Compaction 等の Iceberg 固有の機能が Databricks 上では実行できないケースがあります。
- **サービスベンダーのサポート状況**  
  Spark ベースだからといってすべての機能が動作保証されるわけではないため、実運用前に機能単位で検証・確認することが必要です。

Iceberg はローカル環境や他のプラットフォームでも動作するオープンソースのテーブルフォーマットであり、マルチエンジン・マルチクラウドな分析基盤を構築するうえで有力な選択肢のひとつです。ただし、今回のように Databricks 上で利用する際には、サービス側のサポート状況をしっかりと確認しながら機能検証を進めることが欠かせません。