空のパーティションにダミーファイルを置くとAthenaのクエリのパフォーマンスは向上するのか?
背景
Athenaにはパーティション射影という機能があり、この機能を用いることでパーティションの管理を自動化することができます。一方で射影されたパーティションに対して空のパーティションが多い場合は、クエリのパフォーマンスが低下する可能性があります。そこで形式的にダミーファイルを空のパーティションに配置することで、パーティションが空の場合に対してパフォーマンスがどう変化するのかが気になり調べました。
パーティション化とは
データを特定の列(日付など)の値で分割することをパーティション化といいます。例えば、
s3://example-bucket/sales/date=20250101/hoge.json
s3://example-bucket/sales/date=20250102/fuga.json
のように売り上げデータを日付単位でパーティション化することができます。パーティションを設定することで、クエリに必要なデータだけをスキャンすることが可能になり、パフォーマンスを向上させることができます。
例えば、
select sum(amount)
from sample_table
where date = '20250101';
のようなクエリの場合、date=20250101
のパーティションにあるデータのみをスキャンし date=20250102
のパーティションにあるデータはスキャンしないため、クエリのパフォーマンスが向上します。Athenaは、デフォルトではGlue Data Catalogに登録されたパーティションの情報を利用します。つまり、新しいパーティションが作成された場合はGlue Data Catalogのパーティション情報もあわせて更新する必要があります。
パーティション射影とは
一方で、パーティション射影は事前にテーブルに設定された情報に基づいて、パーティションを計算します。そのためGlue Data Catalogからパーティション情報を取得しませんし、パーティション情報の更新も不要です。例えば
TBLPROPERTIES (
'projection.enabled' = 'true',
'projection.date.type' = 'date',
'projection.date.range' = '20000101,20301231',
'projection.date.format' = 'yyyyMMdd',
'storage.location.template' = 's3://example-bucket/sales/date=${date}/'
);
のようにパーティション射影を設定すると、date
列の値が 20000101
から 20301231
の範囲のパーティションのデータに対してクエリすることが可能になります。
パーティション情報の管理が不要になるためパーティション射影は便利ですが、データが存在しないパーティションに対してもファイルの存在確認を行うため、空のパーティションが多い場合、クエリのパフォーマンスが低下する可能性がありそうです。
空のパーティションにダミーファイルを置くとパフォーマンスは向上するか
先述のとおり、空のパーティションが多い場合パフォーマンスが低下する可能性がありそうです。では、空のパーティションにダミーファイルを置くとパフォーマンスは向上するのでしょうか。以下のとおり検証してみました。
検証方法
- パーティション数はパーティション射影で設定された20200101から20301231までの約11,000日分とした。
- 空のパーティションの割合を0%, 10%, 20%, ...と10%刻みで変更し、空のパーティションのままの場合とダミーファイルを置いた場合でクエリのパフォーマンスを比較した。
- 検証クエリは約11,000のパーティションをフルスキャンするものとした。
- パフォーマンスについてクエリの11回の実行時間の中央値で比較した。
データ作成に用いたコードは末尾に掲載します。
検証結果
検証結果としては、想定とは違い ほとんどのケースでダミーファイルを配置したほうが速い 傾向にありました。ダミーファイルを置く実装が追加で必要になるため、実際に置くかどうかは検討が必要ですが、速度の観点からはダミーファイルを置くほうがよさそうです。
Appendix
以下のコードで検証用のファイルを生成しました
import gzip
import json
import os
import shutil
from datetime import datetime, timedelta
import awswrangler as wr
def create_test_blank_data(target_remainders, make_blank_file):
# sales/下のファイルを削除
if os.path.exists("sales"):
shutil.rmtree("sales")
start_date = datetime(2000, 1, 1)
end_date = datetime(2030, 12, 31)
current_date = start_date
while current_date <= end_date:
date_str = current_date.strftime("%Y%m%d")
day_count = (current_date - start_date).days
# 指定した割合でデータありファイル作成
if day_count % 10 in target_remainders:
data = [
{"id": "user1", "amount": 1},
{"id": "user2", "amount": 2},
{"id": "user3", "amount": 3},
]
os.makedirs(f"sales/date={date_str}", exist_ok=True)
with gzip.open(f"sales/date={date_str}/data.json.gz", "wt") as f:
for record in data:
f.write(json.dumps(record) + "\n")
elif make_blank_file:
# ダミーファイル作成
os.makedirs(f"sales/date={date_str}", exist_ok=True)
with gzip.open(f"sales/date={date_str}/data.json.gz", "wt") as f:
f.write("{}")
else:
pass
current_date += timedelta(days=1)
Discussion