AthenaのPartition Projectionを使用したテーブルの作成方法

## はじめに

Athenaではスキャンしたデータによって料金が課金されます。
AthenaでWAFログなどを分析する際、全ログに対してテーブルを作成すると、分析に必要ない範囲まで読み込んでしまうことによって、無駄な料金がかかり、検索時間も長くなってしまいます。

この悩みは Projection Partition を使用して、解消することができます。
本記事は、 Projection Partition を使用してWAFログをパーティション分割し、コスト削減および検索時間を短縮する方法について紹介します。

## Partition Projection とは？

パーティション分割を自動化できるのが `Partition Projection` です。

通常、パーティション分割されたテーブルを作成するには、手動でパーティション分割用のクエリを実行する必要があります。

ですが、 Projection Partition を使用してテーブルを作成した場合、クエリに記載されたパーティション構造に沿ってデータをパーティション分割します。
新しいデータがS3に格納された場合、パーティション構造に沿って自動でパーティションが作成されます。
パーティション分割されたデータをクエリすることで、スキャン対象が減り、Athenaのコストを削減することができます。

##  Projection Partition を使用したテーブルの作成のポイント

以下はAWS公式ドキュメントで記載されている、WAFログを Projection Partition を使用して、テーブルを作成するクエリ例になります。[参照](https://docs.aws.amazon.com/ja_jp/athena/latest/ug/waf-logs.html#create-waf-table-partition-projection)
このクエリをもとに、Projection Partition を使用してテーブルを作成する際のポイントについて説明します。

```SQL
CREATE EXTERNAL TABLE `waf_logs`(
  `timestamp` bigint,
  `formatversion` int,
  `webaclid` string,
  `terminatingruleid` string,
  `terminatingruletype` string,
  // <一部省略>
  `ja3Fingerprint` string
)
PARTITIONED BY ( 
`region` string, 
`date` string) 
ROW FORMAT SERDE 
  'org.openx.data.jsonserde.JsonSerDe' 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.mapred.TextInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://DOC-EXAMPLE-BUCKET/AWSLogs/accountID/WAFLogs/region/DOC-EXAMPLE-WEBACL/'
TBLPROPERTIES(
 'projection.enabled' = 'true',
 'projection.region.type' = 'enum',
 'projection.region.values' = 'us-east-1,us-west-2,eu-central-1,eu-west-1',
 'projection.date.type' = 'date',
 'projection.date.range' = '2021/01/01,NOW',
 'projection.date.format' = 'yyyy/MM/dd',
 'projection.date.interval' = '1',
 'projection.date.interval.unit' = 'DAYS',
 'storage.location.template' = 's3://DOC-EXAMPLE-BUCKET/AWSLogs/accountID/WAFLogs/${region}/DOC-EXAMPLE-WEBACL/${date}/')
```

### PARTITIONED BY句
ここに、パーティションしたいカラム名および型を指定します。


```SQL
PARTITIONED BY ( 
`region` string, 
`date` string) 
```
### ROW FORMAT など
次のクエリは、データの形式や、入力・出力データのフォーマットを指定するクエリになります。
個人的に、このあたりの記述は Projection Partition のテーブルの作成に重要なポイントではないと思ってます。
[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/athena/latest/ug/querying-aws-service-logs.html)に、AWSサービスの各ログ毎にクエリが用意されているので、
素直にそちらの記述をコピーすればよいと思います。

```SQL
ROW FORMAT SERDE 
  'org.openx.data.jsonserde.JsonSerDe' 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.mapred.TextInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
```

### LOCATION 句
`LOCATION`句には、データの格納場所を指定します。
```SQL
LOCATION
  's3://DOC-EXAMPLE-BUCKET/AWSLogs/accountID/WAFLogs/region/DOC-EXAMPLE-WEBACL/'
```

### TBLPROPERTIES 句
ここが一番のポイントとなります。
`TBLPROPERTIES` 句に、パーティションの構造を記述します。
、 Projection Partition に使用するプロパティ等については、[こちら](https://docs.aws.amazon.com/ja_jp/athena/latest/ug/partition-projection-supported-types.html)のドキュメントを参照してください。

```SQL
TBLPROPERTIES(
 'projection.enabled' = 'true',
 'projection.region.type' = 'enum',
 'projection.region.values' = 'us-east-1,us-west-2,eu-central-1,eu-west-1',
 'projection.date.type' = 'date',
 'projection.date.range' = '2021/01/01,NOW',
 'projection.date.format' = 'yyyy/MM/dd',
 'projection.date.interval' = '1',
 'projection.date.interval.unit' = 'DAYS',
 'storage.location.template' = 's3://DOC-EXAMPLE-BUCKET/AWSLogs/accountID/WAFLogs/${region}/DOC-EXAMPLE-WEBACL/${date}/')
```
- `projection.enabled`
    - Projection Partition が有効になっているか。 `true` で有効。
- `projection.{columnName}.type`
    - パーティションの型を記述する。{columnName}には、`PARTITIONED BY` 句で記載したカラムを記載する。
    - 型によって、必要なパーティションプロパティが変わってくる。指定できる型については[こちら](https://docs.aws.amazon.com/ja_jp/athena/latest/ug/partition-projection-supported-types.html)

- `projection.{columnName}.values`
    - enum型を使用する場合に必須なプロパティ。カンマ区切りのリストを記述する。

- `projection.{columnName}.range`
    - 日付の範囲を指定するプロパティ。date型を使用する場合に必須。Javaのjava.time.*日付型と互換性のある形式で、最小値と最大値を記述する。
- `projection.{columnName}.format`
    - 日付のフォーマットを指定するプロパティ。date型を使用する場合に必須となる。Javaの日付フォーマットに沿った形式を使用できる。[Javaの日付フォーマットはこちら](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html)
- `projection.{columnName}.interval`
- `projection.{columnName}.interval.unit`
    - パーティションの間隔を決定することができるプロパティ。date型で使用できるプロパティ。`interval.unit`で時間単位を決め、`interval`で、間隔をきめる　。
指定しない場合、1日または1ヶ月の間隔になる。

- `storage.location.template`
    - パーティションを含んだS3のパスを指定する。パーティションのキーは${}で囲む。

    
## Projection Partition を使用したテーブルの作成

WAFログが以下の構造で格納されているS3に格納されているとします。
このデータに対してProjection Partitionのテーブルを作成していきます。

```shell
aws-waf-logs-sample
└── AWSLogs
    └── Account_ID
        └── WAFLogs
            └── ap-northeast-1
                ├── test-alb-waf
                │   └── 2023
                │       └── 12
                │           └── 10
                │               └── 08
                │                   └── 30
                │                       ├── Account_ID_waflogs_ap-northeast-1_test-alb-waf_20231210T0830Z_27be5e3e.log.gz
                │                       └── Account_ID_waflogs_ap-northeast-1_test-alb-waf_20231210T0830Z_bc2aed45.log.gz
                └── test-waf
                    └── 2023
                        └── 12
                            ├── 10
                            │   ├── 15
                            │   │   └── 50
                            │   │       ├── Account_ID_waflogs_ap-northeast-1_test-waf_20231210T1550Z_16ab0d2d.log.gz
                            │   │       └── Account_ID_waflogs_ap-northeast-1_test-waf_20231210T1550Z_2f4ac5f8.log.gz
                            │   └── 17
                            │       └── 05
                            │           └── Account_ID_waflogs_ap-northeast-1_test-waf_20231210T1705Z_a6e54bde.log.gz
                            └── 11
                                └── 00
                                    ├── 30
                                    │   └── Account_ID_waflogs_ap-northeast-1_test-waf_20231211T0030Z_0ebb1860.log.gz
                                    └── 35
                                        └── Account_ID_waflogs_ap-northeast-1_test-waf_20231211T0035Z_40869bfc.log.gz
```

### パーティション分割したい箇所の把握
まずは、パーティション分割したい箇所はどこかを把握することがポイントになります。
例のS3オブジェクトの構造では、WAFのWebACL毎にログが出力されているので、WebACLと日付毎に分けたいと思います。

| パーティション箇所 | 分割単位               | 
| ------------------ | ---------------------- | 
| WebACL             | test-alb-waf、test-waf | 
| 日付               | 1日毎                  | 

### PARTITIONED BY句
パーティション箇所を英語にしたカラム名を記述します。

```SQL
PARTITIONED BY ( 
`webacl` string, 
`date` string) 
```
### LOCATION 句
WebACLのWAFログはリージョン配下にデータが格納されてますので、`LOCATION` 句は次の記述とします。

```SQL
LOCATION
  's3://aws-waf-logs-sample/AWSLogs/Account_ID/WAFLogs/ap-northeast-1/'
```

### TBLPROPERTIES 句
ここには[パーティション分割したい箇所の把握](#3-パーティション分割したい箇所の把握)の内容に沿って、記述していきます。
例の場合、以下の記述になると思います。

```SQL
TBLPROPERTIES(
 'projection.enabled' = 'true',
 'projection.webacl.type' = 'enum',
 'projection.webacl.values' = 'test-alb-waf,test-waf',
 
 'projection.date.type' = 'date',
 'projection.date.range' = '2023/12/01,NOW',
 'projection.date.format' = 'yyyy/MM/dd',
 'projection.date.interval' = '1',
 'projection.date.interval.unit' = 'DAYS',
 
 'storage.location.template' = 's3://aws-waf-logs-sample/AWSLogs/Account_ID/WAFLogs/ap-northeast-1/${webacl}/${date}')
```
### 完成したクエリ
最終的に以下のクエリが完成しました！では、実際にテーブルを作成して、パーティション分割されているか確認していきましょう。

```SQL
CREATE EXTERNAL TABLE `waf_logs`(
  `timestamp` bigint,
  `formatversion` int,
  `webaclid` string,
  `terminatingruleid` string,
  `terminatingruletype` string,
  // <一部省略>
  `ja3Fingerprint` string
)
PARTITIONED BY ( 
`webacl` string, 
`date` string) 
ROW FORMAT SERDE 
  'org.openx.data.jsonserde.JsonSerDe' 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.mapred.TextInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://aws-waf-logs-sample/AWSLogs/Account_ID/WAFLogs/ap-northeast-1/'
TBLPROPERTIES(
 'projection.enabled' = 'true',
 'projection.webacl.type' = 'enum',
 'projection.webacl.values' = 'test-alb-waf,test-waf',
 'projection.date.type' = 'date',
 'projection.date.range' = '2023/12/01,NOW',
 'projection.date.format' = 'yyyy/MM/dd',
 'projection.date.interval' = '1',
 'projection.date.interval.unit' = 'DAYS',
 'storage.location.template' = 's3://aws-waf-logs-sample/AWSLogs/Account_ID/WAFLogs/ap-northeast-1/${webacl}/${date}')
```

### パーティションされているかの確認
まず、最初にテーブルの全データに対してクエリを行います。

```SQL
SELECT ("timestamp", "webacl", "date")
FROM "default"."waf_logs";
```

![スクリーンショット 2024-05-02 9.54.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317229/627ad4b8-9067-6843-9e89-e1fd2cc95b7e.png)


スキャンしたデータが7.14KBになっていることを確認しました。

次に日付を指定して、データをクエリしてみます。

```SQL
SELECT "timestamp",
	"webacl",
	"date"
FROM "default"."waf_logs"
WHERE "date" = '2023/12/11';
```
スキャンしたデータが2.3KBと、スキャン容量が少なくなっていることを確認できました！
無事にパーティション分割されていそうです！

## 参考
- [Amazon Athena のPartition Projection(パーティション射影)の設定方法の確認と、手動でパーティションを追加する場合との比較](https://dev.classmethod.jp/articles/tried-to-check-how-to-set-amazon-athena-partition-projection/)
- [パーティション射影を使用して Athena で AWS WAF S3 ログ用テーブルの作成](https://docs.aws.amazon.com/ja_jp/athena/latest/ug/waf-logs.html#create-waf-table-partition-projection)
- [Amazon Athena でのパーティション射影](https://docs.aws.amazon.com/ja_jp/athena/latest/ug/partition-projection.html)