Amazon Athena のPartition Projection(パーティション射影)の設定方法の確認と、手動でパーティションを追加する場合との比較

2023.03.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

事業本部Delivery部のアベシです。

AthenaのPartition projectionという機能を使ってみて、手動でパーティションを追加する場合と比べみたので、その際の方法をブログに残しました。 私はまだAthenaの使用歴が短く、自分に対する備忘録も兼ねてできるだけ詳細に書きました。

はじめに

データのパーティティションについて

データをパーティティションしていない場合、Athenaはクエリの際に全てのデータをスキャンして条件に合うデータを抽出します。
この場合、クエリに時間がかかりパフォーマンスが悪くなり、場合によってはクエリがタイムアウトしてしまう事もあります。デフォルトのクエリタイムアウトは30分です。
また、Athenaはスキャンしたバイト数に対して課金されますので、クエリの際にスキャンデータの量が多いと、その分AWSからの課金額が高くなってしまいます。

Athenaではデータをパーティティションに分けて、パーティションを指定してクエリする事でスキャンする対象を該当のパーティティションに含まれるデータのみに限定できます。
こうすることでクエリの応答のパフォーマンスが向上し、課金額も抑えることができます。

Partition Projectionとは

パーティションの設定は手動で行うこともできますが、AthenaではPartition Projectionという機能を提供しています。
この機能を使えばパーティション管理を自動化できます。
手動の場合、パーティションのデータが増えるたびに追加する必要がありますが、Partition Projectionを使うことで増えた場合に自動で認識しパーティションを追加してくれます。

ブログ内で使用するS3のデータについて

今回は、以前のブログで作成したデータを使用します。
このブログではAmazon Kinesis Data Firehoseを使って、CloudWatch LogsからS3にログを送信しています。
この場合S3に送られるオブジェクトのキーは、デフォルトでは以下のような形を取ります。
s3://S3のバケット名/prefix/${year}/${month}/${day}/${hour}/オブジェクト名
${year}/${month}/${day}/${hour}/の部分をパーティションとして使用します。
バケットには${hour}/の異なるオブジェクトが複数存在します。
手動でパーティションを追加する方法では${hour}/の異なる複数のパーティションを一個一個手で追加する必要があります。

前提

データベースの作成まではできている前提でテーブル作成から進めていきます。

手動でパーティション設定

テーブル作成

下に記載したDDLでテーブルを作成します。
PARTITIONED BYでパーティション名と値の型を設定します。
データは以下のように各行に1つのJSONが1行で表わされた形となっています。これをJSON Linesといいます。いずれにしてもJSONですのでROW FORMAT SERDEでJsonSerDeを指定します。

{"temperature": 21.84, "pressure": 1018.09, "humidity": 55.04, "date": 20230212170241}
{"temperature": 21.85, "pressure": 1018.07, "humidity": 54.73, "date": 20230212170245}
・・・

テーブルを作成DDLは以下の通りです。

CREATE EXTERNAL TABLE IF NOT EXISTS `<データベース名>`.`partition_manual` (
  `temperature` string,
  `pressure` string,
  `humidity` string,
  `date` string
)
PARTITIONED BY (
  `year` int,
  `month` int,
  `day` int,
  `hour` int
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://<S3バケットのパス>/'

partition_manualがテーブル名です。
因みにテーブル名にハイフン使うとエラーが発生します。テーブル名に使える特殊文字はアンダースコアのみです。

パーティション追加

続きまして、パーティションを追加します。
パーティションを追加するには、ALTER TABLE ADD PARTITIONを使います。
Amazon Kinesis Data FirehoseからのS3に保存したオブジェクトのキーは2023/02/12/10な構成となり、この場合はALTER TABLE ADD PARTITIONコマンドで行います。
Hive形式のキーはyear=2023/month=02/day=12/hour=10のようになり、こちらはMSCK REPAIR TABLEでパーティション作成します。
LOCATIONにパーティションを含んだS3バケットのパスを指定します。

ALTER TABLE `<データベース名>`.`partition_manual`
ADD PARTITION (
year=2023,
month=02,
day=12,
hour=10
)
LOCATION 's3://<S3バケットのパス>/2023/02/12/10/'

これでパーティションが追加されました。
追加されたパーティションを確認するには、SHOW PARTITIONSを使います。

SHOW PARTITIONS `<データベース名>`.`partition_manual`

結果は以下のようになります。

year=2023/month=02/day=12/hour=10

クエリしてみます。

SELECT *
FROM "<データベース名>"."partition_manual"
WHERE year=2023 AND month=02 AND day=12 AND hour=10

以下の画像の様にオブジェクトのJSONデータの値が取得できました。

しかし他のパーティションをクエリすると以下のようクエリ結果が空になります。

当たり前ですがこれは対象のパーティションが追加されていないからです。

パーティションを更に追加できるが

このバケットには${hour}/の異なる複数のパーティションが沢山存在します。
これを先程のコマンドで一個一個追加するのは現実的ではなさそうです。 今後データが更に増えて${day}/が増えていくことも考えられます。そうなるとさらに大変です。

Partition Projectionを使用してパーティションを自動で読み込む

ということで、Partition Projectionを使って手動のパーティション追加作業をなくしていこうと思います。

テーブル作成

まずはDDLが以下のようになります。

CREATE EXTERNAL TABLE IF NOT EXISTS `<データベース名>`.`partition_test_table_01` (
  `temperature` string,
  `pressure` string,
  `humidity` string,
  `date` string
)
PARTITIONED BY (
`partition_date` string
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://<S3バケットのパス>/'
TBLPROPERTIES (
  'classification' = 'json',
  'projection.enabled'='true',
  'projection.partition_date.type' = 'date',
  'projection.partition_date.range' = '2020/10/01,NOW',
  'projection.partition_date.format' = 'yyyy/MM/dd/HH',
  'projection.partition_date.interval' = '1',
  'projection.partition_date.interval.unit' = 'HOURS',
  'storage.location.template' = 's3://<S3バケットのパス>/${partition_date}'
)

PARTITIONED BY ()のパーティション名の記述を先程と違いpartition_dateというものに変えてます。
partition_dateの構造や型はTBLPROPERTIES内に記述します。

  • 'classification' = 'json',
    • データの形式を指定しています。今回はJSONです。
  • 'projection.enabled'='true',
    • Projection Partitionを有効にする為にtrueにしています。
  • 'projection.partition_date.type' = 'date',
    • パーティションの型を指定ます。今回はdateです。
    • 型は以下の4つが指定できます。詳しくはこちらを参照してください。
    • enum
    • integer
    • date
    • injected
  • 'projection.partition_date.range' = '2020/10/01,NOW',
    • クエリ可能なパーティションの幅を限定できます。WHERE句でこの期間外のクエリをした場合、例えオブジェクトがあったとしてもクエリ結果に出て来なくなります。カンマ区切りで開始時点から終了時点を指定します。因みにNOWはUTCの現在時刻を表します。
  • 'projection.partition_date.format' = 'yyyy/MM/dd/HH',
    • パーティションpartition_dateのキー構造を指定します。
  • 'projection.partition_date.interval' = '1',
    • 1時間毎に区切っているのでの1を指定しています。
  • 'projection.partition_date.interval.unit' = 'HOURS',
    • パーティションの単位を表します。
  • 'storage.location.template' = 's3:///${date}'
    • パーティションを含んだS3のパスを指定します。パーティションのキーは${}で囲みます。

クエリ

以下のSQLでクエリしてみます。
WHEREでクエリ対象のパーティションの範囲を指定しています。
この指定範囲外のパーティションはスキャン対象から外れます。

SELECT *
FROM "partition_projection_test"
WHERE date BETWEEN '2023/02/12/08' AND '2023/02/12/15'
ORDER BY RAND()
limit 10;


このようにパーティションを自動で読み込んで、指定したパーティションの範囲のデータを取得できました。
以上。