Amazon Athenaのスキャンされるデータ量の検証とParquetのcount(1)が便利という話

# 概要
Amazon Athenaを本格的に運用に乗せてくると、データを更新した際などのレコード件数などの確認は重要となってきます。
しかしながら、扱いが困るくらい大きなデータだからこそAthenaなどを使っているわけで、データの確認に都度お金が掛かると、必要な事とはいえ心理的に阻害されることがあるかもしれません。

今回、運用していて「なるほどこのケースだとData scannedが0になるのか」と気づいた点がいくつかあったので、スキャンされるデータ量の検証もしつつ、まとめてみました。


# お断り
公式ドキュメントにおいては **特定ケースでスキャン量がゼロになる** とまで明言されているものを見つけられなかったので（あるかもしれません）2021年5月時点での仕様、かつ私が扱っているデータセット要因によるものである可能性もあります。


# 結論
- Parquet形式のデータの場合、whereやgroup byなどを付けない単純なcount(1)のSELECTであればData scanned 0KBでレコード数が確認できる。
- パーティションのキーであればwhereで指定しても、group by をしても同様にData scanned 0KBとなる。



# おさらい（Athenaの料金とスキャン）
公式にわかりやすくまとめられています。
https://aws.amazon.com/jp/athena/pricing/

一方で「スキャン」という単語に統一されており、「課金体系はスキャンされるデータ量だよ～」と説明はできますが「じゃあ例えばこういうクエリっていくらかかるの？」と問われると、回答が難しいと感じます。

というわけで、実際に見てみました。


# スキャンの発生検証

以下のパターンで試していきます。
## 対象テーブルと調査クエリ

### 50MB_NOT_PARTITIONED_TABLE 
- レコード数 250,000件
- パーティションなし
- TSV
- 実行クエリ
    - 1.  SELECT * FROM 50MB_NOT_PARTITIONED_TABLE limit 10;
    - 2.  SELECT * FROM 50MB_NOT_PARTITIONED_TABLE limit 1000;
    - 3.  SELECT * FROM 50MB_NOT_PARTITIONED_TABLE limit 10000;
    - 4.  SELECT * FROM 50MB_NOT_PARTITIONED_TABLE ;
    - 5.  SELECT column1 FROM 50MB_NOT_PARTITIONED_TABLE limit 10;
    - 6.  SELECT column1 FROM 50MB_NOT_PARTITIONED_TABLE limit 1000;
    - 7.  SELECT column1 FROM 50MB_NOT_PARTITIONED_TABLE limit 10000;
    - 8.  SELECT column1 FROM 50MB_NOT_PARTITIONED_TABLE ;
    - 9.  SELECT count(1) FROM 50MB_NOT_PARTITIONED_TABLE ;
 

### 250MB_PARTITIONED_TABLE_TSV
- レコード数 800,000件
- パーティションあり
- 指定したパーティション内のレコード数200,000件/67.99MB）
- TSV
- 実行クエリ
    - 1. SELECT * FROM 250MB_PARTITIONED_TABLE_TSV where partitionkey = 'xxx' limit 10;
    - 2. SELECT * FROM 250MB_PARTITIONED_TABLE_TSV where partitionkey = 'xxx' limit 1000;
    - 3. SELECT * FROM 250MB_PARTITIONED_TABLE_TSV where partitionkey = 'xxx' limit 10000;
    - 4. SELECT * FROM 250MB_PARTITIONED_TABLE_TSV where partitionkey = 'xxx' ;
    - 5. SELECT column1 FROM 250MB_PARTITIONED_TABLE_TSV where partitionkey = 'xxx' limit 10;
    - 6. SELECT column1 FROM 250MB_PARTITIONED_TABLE_TSV where partitionkey = 'xxx' limit　1000;
    - 7. SELECT column1 FROM 250MB_PARTITIONED_TABLE_TSV where partitionkey = 'xxx'  limit 10000;
    - 8. SELECT column1 FROM 250MB_PARTITIONED_TABLE_TSV where partitionkey = 'xxx' ;
    - 9. SELECT count(1) FROM 250MB_PARTITIONED_TABLE_TSV where partitionkey = 'xxx' ;
    - 10. SELECT count(1) FROM 250MB_PARTITIONED_TABLE_TSV ;
    - 11. SELECT partitionkey,count(1) FROM 250MB_PARTITIONED_TABLE_TSV group by 1 ;


### NGB_PARTITIONED_TABLE_PARQUET
- レコード数 200,000,000件
- パーティションあり
- 指定したパーティション内のレコード数 1,900,000 万件/ 容量未調査）
- Parquet
- 実行クエリ
    - 1. SELECT * FROM NGB_PARTITIONED_TABLE_PARQUET where partitionkey = 'xxx' limit 10;
    - 2. SELECT column1 FROM NGB_PARTITIONED_TABLE_PARQUET where partitionkey = 'xxx' limit　10;
    - 3. SELECT column1 FROM NGB_PARTITIONED_TABLE_PARQUET where partitionkey = 'xxx'  limit 1000;
    - 4. SELECT column1 FROM NGB_PARTITIONED_TABLE_PARQUET where partitionkey = 'xxx'  limit 10000;
    - 5. SELECT column1 FROM NGB_PARTITIONED_TABLE_PARQUET where partitionkey = 'xxx' ;
    - 6. SELECT count(1) FROM NGB_PARTITIONED_TABLE_PARQUET where partitionkey = 'xxx' ;
    - 7. SELECT count(1) FROM NGB_PARTITIONED_TABLE_PARQUET ;
    - 8. SELECT partitionkey,count(1) FROM NGB_PARTITIONED_TABLE_PARQUET group by 1 ;
    - 9. SELECT count(1) FROM NGB_PARTITIONED_TABLE_PARQUET where partitionkey = 'xxx' and notpartitionkey = 'yyy';
    - 10. SELECT count(1) FROM NGB_PARTITIONED_TABLE_PARQUET where partitionkey = 'xxx' and notpartitionkey is not null;
    - 11. SELECT count(1) FROM NGB_PARTITIONED_TABLE_PARQUET where notpartitionkey = 'yyy'; --※このクエリは危険です
    - 12. SELECT count(1) FROM NGB_PARTITIONED_TABLE_PARQUET where notpartitionkey is not null;--※このクエリは危険です
    - 13. SELECT count(1) FROM NGB_PARTITIONED_TABLE_PARQUET where partitionkey is not null ;
    - 14. SELECT count(1) FROM NGB_PARTITIONED_TABLE_PARQUET where partitionkey like '%';
    
    

# 実行結果

上記で書かれたSQLについて、10回程度実行し平均、最大、最少を求めてます。
試行回数も少ないのと、どうもテーブルへの格納状況によっても変わるのかな～という感じもしており（※ただの感覚です）参考程度かもです。

## 50MB_NOT_PARTITIONED_TABLE

|No.|select|limit|平均(MB)|最大(MB)|最小(MB)|
|-:|:-|-:|-:|-:|-:|
|1|* |   10|1.37|2.42|0.80|
|2|*  | 1000|1.68|2.39|1.20|
|3|*  | 10000|3.88|5.60|2.80|
|4|* |(なし)|51.38|51.38|51.38|
|5|column1 |  10|11.38|21.97|3.69|
|6|column1 |  1000|10.31|14.69|7.30|
|7|column1 | 10000|14.32|32.98|7.32|
|8|column1|(なし)|51.38|51.38|51.38|
|9|count(1)|- |51.38|51.38|51.38|

- カラムを指定するよりも * によるSELECTのほうがスキャン量が少なくなる、という面白い結果になりました（違うサイズのカラムなどを選択しても同様でした）。
- おおよそ limit 1000 くらいまではあまりスキャン量は変わらないようです。
- TSVの場合は、count(1) としてもそのテーブルをすべてスキャンすることとなるようです。



## 250MB_PARTITIONED_TABLE_TSV

|No.|select|limit|where|平均(MB)|最大(MB)|最小(MB)|
|-:|:-|:-|:-|-:|-:|-:|
|1|* |  10|partitionkey='xxx'|1.56|1.95|1.29|
|2|*  | 1000|partitionkey='xxx'|1.367|1.95|1.29|
|3|*  | 10000|partitionkey='xxx'|4.69|9.13|3.9|
|4|* |(なし)|partitionkey='xxx'|67.99|67.99|67.99|
|5|column1 | 10|partitionkey='xxx'|29.87|39.58|17.99|
|6|column1 |  1000|partitionkey='xxx'|30.23|57.57|17.99|
|7|column1 | 10000|partitionkey='xxx'|35.27|39.58|17.99|
|8|column1|(なし) |partitionkey='xxx'|67.99|67.99|67.99|
|9|count(1)|- |partitionkey='xxx'|67.99|67.99|67.99|
|10|count(1)|- |(なし)|256.89|256.89|256.89|
|11|partitionkey,count(1)|- |(なし)|256.89|256.89|256.89|

- 中身はTSVなので、基本的に結果は前項と同じになります。
- where句を外すと、partitionkeyでgroup by しようが当然すべてのデータをスキャンすることとなります。



## NGB_PARTITIONED_TABLE_PARQUET

|No.|select|limit|where|平均(MB)|最大(MB)|最小(MB)|
|-:|:-|:-|:-|-:|-:|-:|
|1|* | 10 | partitionkey='xxx'|8.92 |12.69 |5.00|
|2|column1 | 10 | partitionkey='xxx'|0.40 |0.82 |0.12|
|3|column1 | 1000 | partitionkey='xxx'|0.99 |3.28 |0.23|
|4|column1 | 10000 | partitionkey='xxx'|0.51 |0.68 |0.25|
|5|column1 | (なし) | partitionkey='xxx'|3.28 |3.28 |3.28|
|6|count(1) | (なし) | partitionkey='xxx'|0.00 |0.00 |0.00 |
|7|count(1) | -|(なし) |0.00 |0.00 |0.00 |
|8|partitionkey,count(1) | - | (なし)|0.00 |0.00 |0.00 |

- Parquetにすると、列数にもよりますがスキャン量が激減します。
- limitをかけても、その項目におけるフルスキャンが走る、ような挙動が時々ありました(No.3の最大がそれ)。
- count(1) は、whereをつけずにテーブル全体に行ったり、パーティションを指定した場合にスキャンが0KBになります。
- count(1)はパーティションキーでgroup by をしてもスキャンが0KBになります。


以下おまけ。

|No.|select|where|結果(MB)|
|-:|       :-|    :-|   -:|
|9 |count(1) | partitionkey = 'xxx' and notpartitionkey = 'yyy'     |6.05 (何度やっても)|
|10|count(1) | partitionkey = 'xxx' and notpartitionkey is not null     |6.05 (何度やっても)|
|11|count(1) | notpartitionkey = 'yyy'     |※ |
|12|count(1) | notpartitionkey is not null     |※ |
|13|count(1) | partitionkey is not null      |0.00 |
|14|count(1) | partitionkey like '%'     |0.00 |

- パーティションキー以外をwhere句につけると、スキャンが発生します。これは指定するカラムによって変わり、SELECTですべて取得した時の値と一致しているように見えます（9と10で結果が一致しているのも、どういう挙動かが分かる感じがしますね）。
- ※はパーティションキーを指定していないので、数秒でキャンセルしても数十MBに。やっちゃだめです。
- パーティションキーは、それ自体をwhere句に入れあれこれいじってもスキャン量は0のままのようでした。


# 雑感
スキャン量を減らす、ということは、そのままパフォーマンスチューニングに繋がるわけです。こうした挙動の肌感覚を持っておくことはAthenaを使っていくうえで重要と考えています。

ちなみにParquetが強いのは、[こちらのドキュメント](
https://docs.aws.amazon.com/athena/latest/ug/columnar-storage.html) を見ると、Predicate pushdown関連の恩恵なのかなと思います。統計情報のみで完結する、ということなのだと思うのですが、リソース課金ではなくスキャンデータ量課金の場合、こうした恩恵に預かれるのですね。

そして、リソース課金のサービスも多いなか、必然、スキャンデータ量課金だと使う側の工夫や設計も変わってくるはずで、そのあたりが面白いなと感じます。