Delta Lake 3.2 の新機能である型拡張（Type widening）に関する検証結果


## 概要

Delta Lalek 3.2 の新機能である型拡張（Type widening）に関する検証結果を共有します。本記事では、 OSS Delta Lake ではなく、 Databricks にフォーカスした記事となっております。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/041165aa-2929-baa4-08fa-b591167b9a6c.png)

引用元：[型拡張 | Databricks on AWS](https://docs.databricks.com/ja/delta/type-widening.html)

## 型拡張（Type widening）について

### 型拡張（Type widening）とは

Delta Lake 3.2 にて実装された機能であり、ソースのデータ型に応じてターゲットのターゲットテーブルのデータ型をデータ書き込み時に変更する機能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/122a14cf-81d8-3753-2ae0-49f60e00630a.png)

引用元：[Delta Lake 3.2 | Delta Lake](https://delta.io/blog/delta-lake-3-2/)

Databricks Runtime 15.2 以降で利用できる機能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/041165aa-2929-baa4-08fa-b591167b9a6c.png)

引用元：[型拡張 | Databricks on AWS](https://docs.databricks.com/ja/delta/type-widening.html)

### サポートされている型の変更

変換元と変換先のデータ型については下記のように記述されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/9c689195-1868-75e2-e3d9-a23923a57802.png)

引用元：[型拡張 | Databricks on AWS](https://docs.databricks.com/ja/delta/type-widening.html#supported-type-changes)

### 利用方法

1. 事前準備
   1. テーブルに対して`delta.enableTypeWidening`テーブルプロパティを`true`に設定
   1. Date 型を timestampNTZ 型に変更したい場合には`delta.feature.timestampNtz`テーブルプロパティを`supported`に設定
1. データ書き込み時にスキーマ展開を許可する方法
   1. スキーマ展開を許可（`spark.databricks.delta.schema.autoMerge.enabled`を``とする）して INSERT 、あるいは、 MERGE INTO を実行
1. ALTER 文により手動でデータ型を変更
   1. ALTER COLUMNコマンドを実行

### 注意事項

#### 1. 数値型を decimal 型に変更する際には手動での実施が推奨

数値型（INT 型や long 型）を decimal に変更する際には桁数が一定を超えない場合にはデータが丸められてしまうため、ALTER COLUMN により手動でデータ型を変更する方が望ましいです。

> 数値型を decimalに変更する場合、合計精度は開始精度以上である必要があります。 スケールも大きくする場合は、合計精度を対応する量だけ増やす必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/22f0a1d4-726f-0e1d-d3b1-4ac96e2e296f.png)

引用元：[型拡張 | Databricks on AWS](https://docs.databricks.com/ja/delta/type-widening.html#supported-type-changes)

#### 2. Databricks Auto Loader で型拡張を実施するにはスキーマ推論に関する情報のディレクトリ（_schema）を削除する必要あり

`cloudFiles.schemaLocation`で指定したディレクトリの`_schemas`フォルダにスキーマ推論のデータ型が保持されてしまうため、異なるデータ型のレコードは`_rescued_data`列に格納されます。2024年6月20日時点では、手動で`_schemas`フォルダ内のファイルを削除する必要があるようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/d59b8eb1-a495-6c82-32a1-988de93b565b.png)

引用元：[Auto Loaderでスキーマ推論と進化を設定する | Databricks on AWS](https://docs.databricks.com/ja/ingestion/auto-loader/schema.html#how-does-auto-loader-schema-inference-work)

#### 3. Delta Lake のプロトコルがバージョンアップされることに注意

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/ee904d0b-1627-b9dc-ef00-8ecfabf38d5b.png)

引用元：[Databricks は Delta Lake 機能の互換性をどのように管理しますか? | Databricks on AWS](https://docs.databricks.com/ja/delta/feature-compatibility.html#features-by-protocol-version)


## 検証コードと実行結果

### 書き込みによるデータ型拡張の実施（int 型から decimal 型への変換）

#### 1. スキーマとテーブルを作成

```sql
%sql
CREATE SCHEMA IF NOT EXISTS hive_metastore.manabian_type_widening;
CREATE OR REPLACE TABLE hive_metastore.manabian_type_widening.table_01
(
  id int,
  byte_to_others BYTE,
  short_to_others SHORT,
  int_to_others INT,
  long_to_decimal long,
  float_to_double float,
  decimal_to_decimal_greater_precision DECIMAL(3, 2),
  date_to_timestampNTZ date
);
INSERT OVERWRITE hive_metastore.manabian_type_widening.table_01
(
  id,
  byte_to_others,
  short_to_others,
  int_to_others,
  long_to_decimal,
  float_to_double,
  decimal_to_decimal_greater_precision,
  date_to_timestampNTZ
)
SELECT
  1,
  CAST(1 AS byte),
  CAST(1 AS short),
  CAST(1 AS int),
  CAST(1 AS LONG),
  CAST(1 AS FLOAT),
  CAST(1 AS DECIMAL(3,2)),
  CAST('2020-01-01' AS DATE)
;
SELECT
  *
  FROM
    hive_metastore.manabian_type_widening.table_01
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/56a9fb29-8d29-aed4-d01f-946a65a7f90d.png)

#### 2. テーブル定義を確認

```sql
%sql
DESC TABLE hive_metastore.manabian_type_widening.table_01
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/0d625224-c210-a64d-39a8-ad0fde3625f0.png)

#### 3. DECIMAL(3, 2) のカラムに DECIMAL(5, 2) のデータを挿入する場合にエラーとなることを確認

```sql
%sql
-- 指定桁数以上の値を挿入
DELETE FROM hive_metastore.manabian_type_widening.table_01 WHERE id = 2;
INSERT INTO hive_metastore.manabian_type_widening.table_01
(
  id,
  decimal_to_decimal_greater_precision
)
SELECT
  2,
  CAST(123.12 as DECIMAL(5,2))
;

SELECT
  id,
  decimal_to_decimal_greater_precision
  FROM 
    hive_metastore.manabian_type_widening.table_01
  WHERE
    id = 2
```

> [CAST_OVERFLOW_IN_TABLE_INSERT] Fail to assign a value of "DECIMAL(5,2)" type to the "DECIMAL(3,2)" type column or variable `decimal_to_decimal_greater_precision` due to an overflow. Use `try_cast` on the input value to tolerate overflow and return NULL instead. SQLSTATE: 22003

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/21e98167-b5f6-7f04-1fb8-6ee1a103373b.png)

#### 3. テーブルで型拡張を有効にしてテーブルのデータ型が DECIMAL(5, 2) となっていることを確認

```sql
%sql
ALTER TABLE hive_metastore.manabian_type_widening.table_01
  SET TBLPROPERTIES (
    'delta.enableTypeWidening' = 'true'
  )
;
DESC DETAIL hive_metastore.manabian_type_widening.table_01;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/e82e1874-0b16-0584-5013-6e2d1212b3cd.png)

```sql
%sql

-- スキーマ展開を許可
SET spark.databricks.delta.schema.autoMerge.enabled= True;

-- 指定桁数以上の値を挿入
DELETE FROM hive_metastore.manabian_type_widening.table_01 WHERE id = 2;
INSERT INTO hive_metastore.manabian_type_widening.table_01
(
  id,
  decimal_to_decimal_greater_precision
)
SELECT
  2,
  CAST(123.12 as DECIMAL(5,2))
;

SELECT
  id,
  decimal_to_decimal_greater_precision
  FROM 
    hive_metastore.manabian_type_widening.table_01
  WHERE
    id = 2;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/dd7e6657-419d-65e5-d2d6-8f275fa21a9d.png)

```sql
%sql
DESC TABLE hive_metastore.manabian_type_widening.table_01
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/0a16797b-45db-ba6f-7748-3e3ba56fda51.png)

### 書き込みによるデータ型拡張の実施（date 型から timestampNTZ 型への変換）

#### 1. TIMESTAMP_NTZ 型のデータを挿入した場合に TIMESTAMP_NTZ 型ではなく DATE 型として挿入されることを確認

```sql
%sql
-- スキーマ展開を許可
SET spark.databricks.delta.schema.autoMerge.enabled = True;

-- 日付型に timesptamp 型を挿入
DELETE FROM hive_metastore.manabian_type_widening.table_01 WHERE id = 3;
INSERT INTO hive_metastore.manabian_type_widening.table_01
(
  id,
  date_to_timestampNTZ
)
SELECT
  3,
  CAST('2020-01-01 12:12:12' AS TIMESTAMP_NTZ)
;

SELECT
  id,
  date_to_timestampNTZ
  FROM 
    hive_metastore.manabian_type_widening.table_01
  WHERE
    id = 3
;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/b576192a-66be-84a3-faef-23f82dd7460f.png)


#### 2. テーブルに対して TIMESTAMP_NTZ 型を有効にしてカラムが TIMESTAMP_NTZ 型に変更されることを確認

```sql
%sql
ALTER TABLE hive_metastore.manabian_type_widening.table_01
  SET TBLPROPERTIES (
    'delta.feature.timestampNtz' = 'supported'
  )
;
DESC DETAIL hive_metastore.manabian_type_widening.table_01;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f2650681-878a-47c7-9a12-60e4a934d751.png)


```sql
%sql
-- スキーマ展開を許可
SET spark.databricks.delta.schema.autoMerge.enabled = True;

-- 日付型に timesptamp 型を挿入
DELETE FROM hive_metastore.manabian_type_widening.table_01 WHERE id = 3;
INSERT INTO hive_metastore.manabian_type_widening.table_01
(
  id,
  date_to_timestampNTZ
)
SELECT
  3,
  CAST('2020-01-01 12:12:12' AS TIMESTAMP_NTZ)
;

SELECT
  id,
  date_to_timestampNTZ
  FROM 
    hive_metastore.manabian_type_widening.table_01
  WHERE
    id = 3
;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/98bf4251-cd0c-c64f-181e-0ba903d3096e.png)

```sql
%sql
DESC TABLE hive_metastore.manabian_type_widening.table_01
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/b9fea9db-f9df-4c09-8741-10c371e44d6b.png)

### 手動によるデータ型変換

#### 1. ALTER COLUMNコマンドを実行することで decimal(38, 10) に変更されることを確認

```sql
%sql
ALTER TABLE hive_metastore.manabian_type_widening.table_01 
  ALTER COLUMN long_to_decimal TYPE decimal(38, 10)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/c231123a-f7af-de76-db89-cd9c6852f83b.png)

```sql
%sql
DESC TABLE hive_metastore.manabian_type_widening.table_01
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/2f7162f4-cb0b-a19b-21c0-99ce150b0aab.png)

### Databricks Auto Loader による型拡張の検証

#### 1. テーブルを作成

```sql
%sql
DROP TABLE IF EXISTS hive_metastore.manabian_type_widening.table_04;
CREATE TABLE hive_metastore.manabian_type_widening.table_04
(
  id int,
  int_to_long INT,
  float_to_double float,
  decimal_to_decimal_greater_precision DECIMAL(3, 2),
  date_to_timestampNTZ date
)
TBLPROPERTIES (
  'delta.enableTypeWidening' = 'true',
  'delta.feature.timestampNtz' = 'supported'
)
;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/a9035fd0-03f6-8468-57f0-7d542b219c00.png)

#### 2. ソースディレクトリとチェックポイントの初期化

```python
src_dir = "dbfs:/user/hive/warehouse/manabian_type_widening.db/src"
checkpoint_dir = "dbfs:/user/hive/warehouse/manabian_type_widening.db/checkpoint"
tgt_tbl_name = "hive_metastore.manabian_type_widening.table_04"

# ソースディレクトリとチェックポイントの初期化
dbutils.fs.rm(src_dir, True)
dbutils.fs.rm(checkpoint_dir, True)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f5112b95-494c-c1ff-5e3b-bb6e5ddfd0c1.png)

#### 3. 初回取り込みを実行

```python
# 初回データの書き込み
src_df = spark.sql(
    """
    SELECT
        1 AS id,
        CAST(1 AS int) AS int_to_long,
        CAST(1 AS FLOAT) AS float_to_double,
        CAST(1 AS DECIMAL(3,2)) AS decimal_to_decimal_greater_precision,
        CAST('2020-01-01' AS DATE) AS date_to_timestampNTZ
    """
)

src_df.write.mode("append").format("parquet").save(src_dir)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/0b6d101b-f2e0-1b43-2c58-c6802c8a3d1b.png)


```python
src_df = (
    spark.readStream.format("cloudFiles")
    .option("cloudFiles.format", "parquet")
    .option("cloudFiles.schemaLocation", checkpoint_dir)
    .option("cloudFiles.inferColumnTypes", False)
    .option("inferSchema", True)
    .load(src_dir)
)
(
    src_df.writeStream
    .option("mergeSchema", True)
    .option("checkpointLocation", checkpoint_dir)
    .trigger(availableNow=True)
    .toTable(tgt_tbl_name)
)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/860fe06b-3d3b-a686-c988-7aaa52c0d548.png)

```python
spark.table(tgt_tbl_name).display()
spark.sql(f"DESC {tgt_tbl_name}").display()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/dc1ad1f3-8f8e-8890-78de-dfef3988eba2.png)

#### 4. 型拡張が実施されないことを確認

```python
# 2 回目のデータを書き込み
src_df = spark.sql(
    """
    SELECT
        2 AS id,
        CAST(123.12 AS LONG) AS int_to_long,
        CAST(123.12 AS double) AS float_to_double,
        CAST(123.12 AS DECIMAL(5,2)) AS decimal_to_decimal_greater_precision,
        CAST('2020-01-01' AS TIMESTAMP_NTZ) AS date_to_timestampNTZ
    """
)

src_df.write.mode("append").format("parquet").save(src_dir)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/aac069c4-9e34-26ca-df04-e31678aa8ff2.png)

```python
src_df = (
    spark.readStream.format("cloudFiles")
    .option("cloudFiles.format", "parquet")
    .option("cloudFiles.schemaLocation", checkpoint_dir)
    .option("cloudFiles.inferColumnTypes", False)
    .option("inferSchema", True)
    .load(src_dir)
)
(
    src_df.writeStream
    .option("mergeSchema", True)
    .option("checkpointLocation", checkpoint_dir)
    .trigger(availableNow=True)
    .toTable(tgt_tbl_name)
)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f9344df4-5f24-d51a-c8eb-cbbbd6484452.png)

```python
spark.table(tgt_tbl_name).display()
spark.sql(f"DESC {tgt_tbl_name}").display()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/26561c7c-1148-00c4-2aa8-45e4ffc81b15.png)

#### 5. スキーマ推論に関する情報が格納されているディレクトリを初期化後にデータ取り込むことで型拡張されることを確認

```python
dbutils.fs.rm(f"{checkpoint_dir}/_schemas", True)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/4773cdc9-7edc-e454-bf90-49289b6366c6.png)

```python
# 3 回目のデータを書き込み
src_df = spark.sql(
    """
    SELECT
        3 AS id,
        CAST(123.12 AS LONG) AS int_to_long,
        CAST(123.12 AS double) AS float_to_double,
        CAST(123.12 AS DECIMAL(5,2)) AS decimal_to_decimal_greater_precision,
        CAST('2020-01-01' AS TIMESTAMP_NTZ) AS date_to_timestampNTZ
    """
)

src_df.write.mode("append").format("parquet").save(src_dir)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/ca5e31f6-048f-9cba-5158-ddfab96f3f1f.png)

```python
src_df = (
    spark.readStream.format("cloudFiles")
    .option("cloudFiles.format", "parquet")
    .option("cloudFiles.schemaLocation", checkpoint_dir)
    .option("cloudFiles.inferColumnTypes", False)
    .option("inferSchema", True)
    .load(src_dir)
)
(
    src_df.writeStream
    .option("mergeSchema", True)
    .option("checkpointLocation", checkpoint_dir)
    .trigger(availableNow=True)
    .toTable(tgt_tbl_name)
)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/7fa486b3-4bcc-fa6d-ee80-d3f782590bf8.png)

```python
spark.table(tgt_tbl_name).display()
spark.sql(f"DESC {tgt_tbl_name}").display()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/2bc4d36e-2255-feec-3634-40949e64478d.png)