マネージドテーブルとアンマネージドテーブルと外部テーブルと

Apache Sparkでやや混同しがちなテーブル種類について説明します。要約を触れた後に、詳細を説明します。

# ざっくり言うと

**マネージドテーブル** は、ユーザーがテーブル名だけを使ってデータ(Sparkデータフレーム)を永続化する方法で、データの保存先(ストレージとパス)はSparkシステム側で自動的にアサインされます。つまり、ユーザーは保存先などストレージ側を意識する必要がありません。データを削除する場合も、`DROP TABLE`文を実行するだけで済み、テーブルのメタデータの削除と、データ実体の削除が同時に実行されます。データの保存先が自動管理されるので **「マネージド」テーブル** と呼びます。

一方、**アンマネージドテーブル**は、ユーザーがデータの保存先パスを指定して永続化し、後からテーブル名とデータ保存先をリンクします。この場合、データの削除には、`DROP TABLE`文でのテーブル(のメタデータ)削除に加えて、データ保存先のデータ実体を削除する必要があります。つまり、ユーザーはテーブル(メタデータ)とデータ実体の保存先の両方を意識する必要があり、よって、**「アンマネージド」テーブル**と呼ばれています。この方法だと、Sparkシステム管理のストレージ外上に置かれているデータに対してもテーブル名とリンクができるため、この意味で、**アンマネージドテーブルは「外部テーブル(External Table)」** とも呼ばれます。

上記の説明は、いくつかの前提知識がないと理解できないと思いますので、それを含めて以下詳細を説明します。

# 詳細

## Apache Sparkにおけるデータの実体とテーブル・メタデータ

Apache Sparkをはじめとするデータレイク系のフレームワークでは、ストレージ上にCSV, JSON, Parquet, DeltaLake, Avroなどのファイルとして保存しておいて、それらをプログラム上でテーブル形式にして扱うことが一般的です。例えば、SparkのPython APIであるPySparkだと、`df = spark.read.csv('/path/to/file.csv')`のようにデータフレームとして読み込ませることができ、CSVファイルをそのままテーブル形式として扱うことができます。つまり、データを「ファイル」として扱うことが基本的になります。

一方で、テーブルを扱うなら、特に検索や集計処理をする場合であれば、SQLを使いたくなるのは自然な流れだと思います。ただし、SQLは「ファイル」ではなく「テーブル名」としてデータ参照を行う必要があります。

このギャップを埋めるために、Apache SparkではMetastoreと呼ばれるサービス上で「テーブル名」と「ファイル」の紐付けができる様になっています。このMetastoreサービスのオプションとしては、ドライバノードローカル(Derby), Hive Metastore, Unity Catalog(Databricksプロダクト)などがあります([Sparkのシステムの環境設定](https://spark.apache.org/docs/latest/sql-data-sources-hive-tables.html)で定義されています)。もちろん、SQLのテーブルとして使用するため、テーブル名に対応するファイル保存先パスの他にも、カラム名などのスキーマ情報や、テーブル設定(テーブルプロパティ)なども同時に記録管理されます。

以上、まとめると、Apache Sparkではデータ保存(永続化)とその参照の枠組みとして、データの実態はファイルとしてストレージ上に保存し、そのデータに紐づくテーブル名やスキーマ情報などはMetastoreに保存されます。つまり、ユーザーは、データの実体とテーブル・メタデータの2つを意識する必要があります。

## 使い方の実際

### アンマネージドテーブル(外部テーブル)

説明の流れから、アンマネージドテーブル(外部テーブル)から見ていきたいと思います。
ここでのシナリオは、以下のステップを実施して、アンマネージドテーブルの作法を見ていきます。

1. Spark上でデータフレームとして適当にデータをロードする
1. 上記のデータをDeltaLake形式でオブジェクトストレージ上に保存する
1. 上記のデータに対してテーブル名を適当に割り当てて、Metastoreに登録する
1. 動作確認
1. データを削除する

#### 1. Spark上でデータフレームとして適当にデータをロードする

```python
df = (
  spark.read
  .format('csv')
  .option('Header', True)
  .option('inferSchema', True)
  .load('/databricks-datasets/Rdatasets/data-001/csv/ggplot2/diamonds.csv') 
)
display(df)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62653/ced92f93-e945-17d0-9c18-7f27483d42e4.png)

#### 2. 上記のデータをDeltaLake形式でオブジェクトストレージ上に保存する

```python
(
  df.write.format('delta')
  .mode('overwrite')
  .save('/tmp/diamond.delta')
)
```

念の為、ストレージ上にファイル保存されたことを確認しておく。

```sh
%fs ls /tmp/diamond.delta
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62653/e6189f4e-389e-c26d-25c2-74a5758ec6e7.png)

#### 3. 上記のデータに対してテーブル名を適当に割り当てて、Metastoreに登録する

上記で保存したDeltaLakeのファイルに対して、テーブル名を割り当て、紐付けます。ここでは、テーブル名として`diamond_delta`を使います。

```sql
%sql
CREATE TABLE diamond_delta      -- テーブル名 
USING delta                     -- ファイルのフォーマット
LOCATION '/tmp/diamond.delta';  -- ファイルのパス
```

これで、データ実体のファイル`/tmp/diamond.delta`とテーブル名`diamond_delta`が紐付けられました。

#### 4. 動作確認

それでは、SQLで上記のデータを参照してみましょう。

```sql
%sql
SELECT * FROM diamond_delta
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62653/707784b1-7e43-24d6-3c88-f6ada617dbb2.png)

また、テーブルのメタデータも確認してみます。

```python
spark.sql('DESCRIBE EXTENDED diamond_delta').show(n=30, truncate=False)

-----
[結果をテキストで見るために、`spark.sql().show()`を使いました]

+----------------------------+---------------------------------------------------+-------+
|col_name                    |data_type                                          |comment|
+----------------------------+---------------------------------------------------+-------+
|_c0                         |int                                                |null   |
|carat                       |double                                             |null   |
|cut                         |string                                             |null   |
|color                       |string                                             |null   |
|clarity                     |string                                             |null   |
|depth                       |double                                             |null   |
|table                       |double                                             |null   |
|price                       |int                                                |null   |
|x                           |double                                             |null   |
|y                           |double                                             |null   |
|z                           |double                                             |null   |
|                            |                                                   |       |
|# Detailed Table Information|                                                   |       |
|Catalog                     |hive_metastore                                     |       |
|Database                    |default                                            |       |
|Table                       |diamond_delta                                      |       |
|Type                        |EXTERNAL                                           |       |
|Location                    |dbfs:/tmp/diamond.delta                            |       |
|Provider                    |delta                                              |       |
|Owner                       |root                                               |       |
|Table Properties            |[delta.minReaderVersion=1,delta.minWriterVersion=2]|       |
+----------------------------+---------------------------------------------------+-------+
```

上記の結果より、メタデータとして、ファイルパスの他に、スキーマやテーブルプロパティなどが含まれていることがわかります。また、`Type`が`EXTERNAL`となっていて、「外部テーブル」(=「アンマネージドテーブル」)であることもわかります。

#### 5. データを削除する

最後にデータの削除をしていきます。先ほど説明した通り、テーブルメタデータを削除した後で、ファイル実体も削除します。

まずは、メタデータの削除。
```sql
%sql
DROP TABLE diamond_delta;
```

続いて、ファイルの削除

```sh
%fs rm -r /tmp/diamond.delta
```

これでアンマネージドテーブルの削除が完了しました。


### アンマネージドテーブルの特徴

いかがでしょうか。アンマネージドテーブルの場合は、要はファイルとテーブル名が分離していると言うことです。これにより、運用上のいくつかのメリット・デメリットが見えてくると思います。

良い点としては、
* 保存するファイルのストレージロケーション(バケツ、コンテナ、フォルダ構成、ファイルパス)などがユーザー側で制御できる
* ファイルのバックアップ運用などと柔軟に調和できる
* 一つのデータを複数のメタストアに登録して、複数の環境から参照できる

などがあると思います。

一方で、面倒だと思う点として、

* テーブルを削除するのに2段階踏まないといけない
* 使う側としては、テーブル名だけを指定して、ストレージ保存先は適当に決めてほしい
* 使う側に対して、ストレージ情報は隠蔽・抽象化させておきたい(ストレージ情報を公開、提供しないといけない)

などがあると思います。

こうした場合は、マネージドテーブルが便利になります。

### マネージドテーブル

それではマネージドテーブルの作法を見ていきます。

1. Spark上でデータフレームとして適当にデータをロードする
1. テーブル名を使って保存(永続化)する
1. 動作確認
1. データを削除する

#### 1.  Spark上でデータフレームとして適当にデータをロードする

こちらは先ほどと同じです(再掲)。

```python
df = (
  spark.read
  .format('csv')
  .option('Header', True)
  .option('inferSchema', True)
  .load('/databricks-datasets/Rdatasets/data-001/csv/ggplot2/diamonds.csv') 
)
```

#### 2. テーブル名を使って保存(永続化)する

`saveAsTable()`にテーブル名を渡すだけです。ここでは、`diamond_delta_managed`というテーブル名を使いました。

```python
(
  df.write.format('delta')
  .mode('overwrite')
  .saveAsTable('diamond_delta_managed')
)
```

上記を実行すると、ファイルの保存先(ストレージ)はシステム側で自動的に払い出してくれ、上記のテーブル名との紐付けも行ってくれます。楽です。

#### 3. 動作確認

動作確認方法は、先ほどと同様です。
まずテーブルの参照です。

```sql
%sql
SELECT * FROM diamond_delta_managed
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62653/d9cdaecf-f145-bcba-4901-b8ef19d4f71f.png)

メタデータの確認をしてみみます。

```python
spark.sql('DESCRIBE EXTENDED diamond_delta_managed').show(n=30, truncate=False)

-----
[結果をテキストで見るために、`spark.sql().show()`を使いました]

+----------------------------+---------------------------------------------------+-------+
|col_name                    |data_type                                          |comment|
+----------------------------+---------------------------------------------------+-------+
|_c0                         |int                                                |null   |
|carat                       |double                                             |null   |
|cut                         |string                                             |null   |
|color                       |string                                             |null   |
|clarity                     |string                                             |null   |
|depth                       |double                                             |null   |
|table                       |double                                             |null   |
|price                       |int                                                |null   |
|x                           |double                                             |null   |
|y                           |double                                             |null   |
|z                           |double                                             |null   |
|                            |                                                   |       |
|# Detailed Table Information|                                                   |       |
|Catalog                     |hive_metastore                                     |       |
|Database                    |default                                            |       |
|Table                       |diamond_delta_managed                              |       |
|Type                        |MANAGED                                            |       |
|Location                    |dbfs:/user/hive/warehouse/diamond_delta_managed    |       |
|Provider                    |delta                                              |       |
|Owner                       |root                                               |       |
|Is_managed_location         |true                                               |       |
|Table Properties            |[delta.minReaderVersion=1,delta.minWriterVersion=2]|       |
+----------------------------+---------------------------------------------------+-------+
```

結果から分かることとして、

* `Location`が自動的にアサインされたファイルの保存先
* `Type`が`MANAGED`になっている

というのが先ほどとの違いとしてわかります。


#### 4. データを削除する

マネージドテーブルの場合は、`DROP TABLE`するだけで、関連するファイルも同時に削除されます。

```sql
%sql
DROP TABLE diamond_delta_managed
```

### マネージドテーブルの特徴

先ほどのアンマネージドテーブルの逆になりますが、ユーザーはテーブル名だけを管理すればよく、ストレージ側は意識する必要がなくなる、と言う点がポイントかと思います。


# まとめ

今回は、マネージドテーブルとアンマネージドテーブル(外部テーブル)を説明しました。特に要件がない場合は、マネージドテーブルを使うのがシンプルになると思います。実際の使用する環境や場面に応じて使い分けてください。