🏹

空のパーティションにダミーファイルを置くとAthenaのクエリのパフォーマンスは向上するのか?

に公開

背景

Athenaにはパーティション射影という機能があり、この機能を用いることでパーティションの管理を自動化することができます。一方で射影されたパーティションに対して空のパーティションが多い場合は、クエリのパフォーマンスが低下する可能性があります。そこで形式的にダミーファイルを空のパーティションに配置することで、パーティションが空の場合に対してパフォーマンスがどう変化するのかが気になり調べました。

https://docs.aws.amazon.com/ja_jp/athena/latest/ug/partition-projection.html

パーティション化とは

データを特定の列(日付など)の値で分割することをパーティション化といいます。例えば、

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)
モリサワ Tech Blog

Discussion