[AWS Glue] テーブルへのカラム追加後に、どのような場合に INSERT INTO クエリが失敗するようになるのか確認してみた

2023.10.11

こんにちは、CX 事業本部 Delivery 部の若槻です。

今回は、AWS Glue テーブルにカラムを追加すると Amazon Athena の INSERT INTO クエリが失敗するようになることがあり、どのような場合に同事象が発生するのか確認してみました。

検証

カラム追加前のテーブルの実装

まず AWS CDK で絡む追加前の Glue テーブルを実装します。

lib/cdk-sample-stack.ts

import {
  aws_s3,
  RemovalPolicy,
  Stack,
  StackProps,
  CfnOutput,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as glue_alpha from '@aws-cdk/aws-glue-alpha';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const dataBucket = new aws_s3.Bucket(this, 'DataBucket', {
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    const database = new glue_alpha.Database(this, 'GlueDatabase');

    const glueTableName = new glue_alpha.S3Table(this, 'GlueTable', {
      database,
      bucket: dataBucket,
      s3Prefix: 'data/',
      dataFormat: glue_alpha.DataFormat.PARQUET,
      partitionKeys: [
        {
          name: 'year',
          type: glue_alpha.Schema.STRING,
        },
        {
          name: 'month',
          type: glue_alpha.Schema.STRING,
        },
        {
          name: 'day',
          type: glue_alpha.Schema.STRING,
        },
      ],
      columns: [
        {
          name: 'id',
          type: glue_alpha.Schema.STRING,
        },
        {
          name: 'airTemperature',
          type: glue_alpha.Schema.DOUBLE,
        },
      ],
    });

    new CfnOutput(this, 'DataBaseName', {
      value: database.databaseName,
    });
    new CfnOutput(this, 'TableName', {
      value: glueTableName.tableName,
    });
  }
}

テーブルは STRING 型の id と DOUBLE 型の airTemperature の 2 つのカラムを持っています。

動作確認

テーブル作成直後なので、パーティションは未作成です。

$ aws glue get-partitions \
  --database-name ${dataBaseName} \
  --table-name ${tableName} \
  --query "Partitions[].{Values: Values, Columns: StorageDescriptor.Columns}"
[]

INSERT INTO クエリを実行してデータを 1 つ作成します。

INSERT INTO ${dataBaseName}.${tableName}
    (id, airtemperature, year, month, day)
VALUES
    ('d001', 10.05, '2023', '06', '16')

パーティションが 1 つ追加されていることを確認します。

$ aws glue get-partitions \
  --database-name ${dataBaseName} \
  --table-name ${tableName} \
  --query "Partitions[].{Values: Values, Columns: StorageDescriptor.Columns}"
[
    {
        "Values": [
            "2023",
            "06",
            "16"
        ],
        "Columns": [
            {
                "Name": "id",
                "Type": "string",
                "Comment": ""
            },
            {
                "Name": "airtemperature",
                "Type": "double",
                "Comment": ""
            }
        ]
    }
]

SELECT クエリでデータを取得してみます。

SELECT * FROM ${dataBaseName}.${tableName}

取得結果は次のようになりました。

id airtemperature year month day
d001 10.05 2023 06 16

既存と同じ型のカラムを追加した場合(クエリ成功)

既存の airTemperature と同じ型のカラム waterTemperature を、敢えて既存のカラムの位置が変わるように追加します。

lib/cdk-sample-stack.ts

      columns: [
        {
          name: 'id',
          type: glue_alpha.Schema.STRING,
        },
+        {
+          name: 'waterTemperature',
+          type: glue_alpha.Schema.DOUBLE,
+        },
        {
          name: 'airTemperature',
          type: glue_alpha.Schema.DOUBLE,
        },
      ],

INSERT INTO クエリを実行して、追加したカラムを含むデータを、すでにパーティションが作成されているキーと、未作成のキーに 1 つずつ作成します。すると INSERT INTO クエリは正常に実行できました。

INSERT INTO ${dataBaseName}.${tableName}
    (id, airtemperature, watertemperature, year, month, day)
VALUES
    ('d002', 10.05, 21.05, '2023', '06', '16'),
    ('d002', 10.05, 21.05, '2023', '06', '17')

すると既存のパーティションはカラムに変更はなく、新規のパーティションには watertemperature を含んでいます。

$ aws glue get-partitions \
  --database-name ${dataBaseName} \
  --table-name ${tableName} \
  --query "Partitions[].{Values: Values, Columns: StorageDescriptor.Columns}"
[
    {
        "Values": [
            "2023",
            "06",
            "17"
        ],
        "Columns": [
            {
                "Name": "id",
                "Type": "string",
                "Comment": ""
            },
            {
                "Name": "watertemperature",
                "Type": "double",
                "Comment": ""
            },
            {
                "Name": "airtemperature",
                "Type": "double",
                "Comment": ""
            }
        ]
    },
    {
        "Values": [
            "2023",
            "06",
            "16"
        ],
        "Columns": [
            {
                "Name": "id",
                "Type": "string",
                "Comment": ""
            },
            {
                "Name": "airtemperature",
                "Type": "double",
                "Comment": ""
            }
        ]
    }
]

SELECT クエリでデータを取得してみます。

SELECT * FROM ${dataBaseName}.${tableName}

クエリ実行は成功し、取得結果は次のようになりました。問題は無さそうです。

id airtemperature watertemperature year month day
d002 10.05 21.05 2023 06 17
d002 10.05 21.05 2023 06 16
d001 10.05 2023 06 16

既存と異なる型のカラムを追加した場合(クエリ失敗)

続いて、既存の airTemperaturewaterTemperature と異なる型のカラム isRainy を追加します。ここでも敢えて既存のカラムの位置が変わるように追加します。

      columns: [
        {
          name: 'id',
          type: glue_alpha.Schema.STRING,
        },
        {
          name: 'waterTemperature',
          type: glue_alpha.Schema.DOUBLE,
        },
+        {
+          name: 'isRainy',
+          type: glue_alpha.Schema.BOOLEAN,
+        },
        {
          name: 'airTemperature',
          type: glue_alpha.Schema.DOUBLE,
        },
      ],

動作確認

さて、先ほどと同様に INSERT INTO クエリを実行して、追加したカラムを含むデータを、すでにパーティションが作成されているキーに作成しようとすると、クエリは失敗しました。

INSERT INTO ${dataBaseName}.${tableName}
    (id, airtemperature, isRainy, watertemperature, year, month, day)
VALUES
    ('d003', 10.05, true, 21.05, '2023', '06', '16'),
    ('d003', 10.05, false, 21.05, '2023', '06', '17')

HIVE_PARTITION_SCHEMA_MISMATCH: You are trying to write into an existing partition in a table. The table schema has changed since the creation of the partition. Inserting rows into such partition is not supported. The column 'israiny' in table 'cdksamplestackgluetable4d65f124' is declared as type 'boolean', but partition 'Optional[year=2023/month=06/day=17]' declared column 'airtemperature' as type 'double'. If a data manifest file was generated at 's3://aws-sam-cli-managed-default-samclisourcebucket-c376z02dmoa7/athena-query-result-primary-wg/Unsaved/2023/10/12/10662dcd-eeab-4e0c-8f7c-87a98131b5bd-manifest.csv', you may need to manually clean the data from locations specified in the manifest. Athena will not delete data in your account.

どうやら、isRainy カラム追加前のパーティションで、新規追加した isRainy と、既存の airTemperature の型が異なるためエラーとなっているようです。

カラム変更前のパーティションを使用して、カラム変更後のデータを INSERT INTO する場合は、同じ位置のカラムの型が一致していることが必要なようです。

対処方法

対処方法は パーティションの再作成 となります。

ALTER TABLE DROP PARTITION コマンドで、パーティションをすべて削除します。year のようにカーディナリティの低いキーを指定すると、少ない回数でパーティションの全削除が可能です。

ALTER TABLE ${dataBaseName}.${tableName} DROP PARTITION (year='2023');

パーティションがすべて削除されました。

$ aws glue get-partitions \
  --database-name ${dataBaseName} \
  --table-name ${tableName} \
  --query "Partitions[].{Values: Values, Columns: StorageDescriptor.Columns}"
[]

次に MSCK REPAIR TABLE コマンドでパーティションを再作成します。

MSCK REPAIR TABLE ${dataBaseName}.${tableName}

Partitions not in metastore: cdksamplestackgluetable4d65f124:year=2023/month=06/day=16 cdksamplestackgluetable4d65f124:year=2023/month=06/day=17
Repair: Added partition to metastore cdksamplestackgluedatabase6a27012d.cdksamplestackgluetable4d65f124:year=2023/month=06/day=16
Repair: Added partition to metastore cdksamplestackgluedatabase6a27012d.cdksamplestackgluetable4d65f124:year=2023/month=06/day=17

補足として、今回は Hive 形式のテーブルなので MSCK REPAIR TABLE コマンドが利用できましたが、非 Hive 形式のテーブルの場合は ALTER TABLE ADD PARTITION コマンドを利用する必要があります。

さて、MSCK REPAIR TABLE コマンド実行後のパーティションを確認すると、データが作成されているキーに、新規追加されてカラムを含むパーティションが作成されています。

$ aws glue get-partitions \
  --database-name ${dataBaseName} \
  --table-name ${tableName} \
  --query "Partitions[].{Values: Values, Columns: StorageDescriptor.Columns}"
[
    {
        "Values": [
            "2023",
            "06",
            "17"
        ],
        "Columns": [
            {
                "Name": "id",
                "Type": "string"
            },
            {
                "Name": "watertemperature",
                "Type": "double"
            },
            {
                "Name": "israiny",
                "Type": "boolean"
            },
            {
                "Name": "airtemperature",
                "Type": "double"
            }
        ]
    },
    {
        "Values": [
            "2023",
            "06",
            "16"
        ],
        "Columns": [
            {
                "Name": "id",
                "Type": "string"
            },
            {
                "Name": "watertemperature",
                "Type": "double"
            },
            {
                "Name": "israiny",
                "Type": "boolean"
            },
            {
                "Name": "airtemperature",
                "Type": "double"
            }
        ]
    }
]

INSERT INTO クエリも正常に実行できました。

INSERT INTO ${dataBaseName}.${tableName}
    (id, airtemperature, isRainy, watertemperature, year, month, day)
VALUES
    ('d003', 10.05, true, 21.05, '2023', '06', '16'),
    ('d003', 10.05, false, 21.05, '2023', '06', '17')

カラム追加前と追加後のデータが期待通りに作成されていることが確認できました。

id airtemperature watertemperature israiny year month day
d003 10.05 21.05 false 2023 06 17
d002 10.05 21.05 2023 06 16
d001 10.05 2023 06 16
d003 10.05 21.05 true 2023 06 16
d002 10.05 21.05 2023 06 17

既存のカラムの型を変更した場合も同様の対処が必要

既存のカラムの型を DOUBLE -> FLOAT のように変更した場合も同様の対処が必要です。

おわりに

AWS Glue テーブルへのカラム追加後に、どのような場合に INSERT INTO クエリが失敗するようになるのか確認してみました。

結論としては、カラムの追加や変更を行った時は原則としてパーティションの再作成を行う運用としたすると良さそうです。

参考

以上