AWS の Data Lake の Partition Key は「常に STRING」で良いのか?幻想を打ち砕く、型選択のベストプラクティス
クラウド事業本部の石川です。Amazon Athena 公式のパフォーマンスチューニングドキュメントには「Always use STRING as the type for partition keys」と明確に書かれています。一方で実際にテーブル設計をしていると、「Redshift 側の DATE 列と JOIN するから型を揃えたい」「last_day() や day_of_week() を使いたいから DATE 型にしたい」「Iceberg や S3 Tables を使う場合はどうなる?」といった要望や疑問が出てきます。
本記事では、AWS Glue、Amazon Athena、Amazon Redshift Spectrum、Apache Iceberg(汎用 S3 上の Iceberg テーブル および S3 Tables)における partition key の型選択について、公式ドキュメントの根拠と内部メカニズムを踏まえたうえで、現場で意思決定するためのベストプラクティスを整理します。
結論
結論を先に言うと、「常に STRING」は Hive 形式の external table 文脈での原則であり、Iceberg では真逆で、TIMESTAMP/DATE などのネイティブ型 + transform を使うのがベストプラクティスになります。
最初に結論をまとめます。テーブルフォーマットによって推奨が真逆になる点が最大のポイントです。
| テーブルフォーマット / 構成 | 推奨される partition key の型 |
|---|---|
| Hive 形式 external table、Athena のみ | STRING(ISO 8601 形式 yyyy-MM-dd) |
| Hive 形式 external table、Athena + Redshift Spectrum 両方 | STRING(ISO 8601 形式) |
| Hive 形式 external table、Redshift Spectrum 単独 | STRING が無難、DATE も選択肢 |
| Hive 形式 + Athena Partition Projection | STRING + projection.dt.type = 'date' |
| Apache Iceberg(汎用 S3) | TIMESTAMP/DATE + days() / months() / years() などの transform |
| Amazon S3 Tables(マネージド Iceberg) | TIMESTAMP/DATE + transform(Iceberg と同じ) |
「常に STRING」という AWS 公式の文言は、Hive 形式のテーブルにおいて partition pruning を効かせるための原則であり、Iceberg のような open table format ではむしろ逆で、ネイティブ型 + partition transform を使うのが推奨されます。背景となる仕組みを理解したうえで、テーブルフォーマットとワークロードに応じて判断するのが重要です。
1. Amazon Athena 公式が「STRING」を推奨する理由
1.1 公式ドキュメントの記述
Athena のパフォーマンスチューニングドキュメントには、ベストプラクティスとして明確に「Always use STRING as the type for partition keys」が挙げられています。
When you query on partition keys, remember that Athena requires partition keys to be of type STRING in order to push down partition filtering into AWS Glue. If the number of partitions is not small, using other types can lead to worse performance. If your partition key values are date-like or number-like, cast them to the appropriate type in your query.
つまり Athena が partition filter を Glue Data Catalog 側に push down できるのは partition key が STRING の場合のみで、それ以外の型を使うとパーティション数が多いケースでパフォーマンスが悪化する、というのが公式見解です。「日付や数値として扱いたい場合はクエリ側で CAST しろ」とまで踏み込んで明言されています。
1.2 AWS Glue Data Catalog の内部表現
なぜ STRING 推奨なのかは、Glue Data Catalog の Partition API 仕様を見るとはっきりします。
PartitionInput の Values フィールドは UTF-8 文字列の配列として定義されており、GetPartition / BatchGetPartition / UpdatePartition 等すべての partition 関連 API でも partition の値は文字列配列として渡される構造になっています。
さらに Glue Crawler の挙動について以下のように明記されており、
When you define a crawler, the partitionKey type is created as a STRING, to be compatible with the catalog partitions.
Glue 内部では partition value は物理的に UTF-8 string として保持されていることが分かります。テーブル DDL で PARTITIONED BY (dt date) と書いても、Glue が S3 prefix から抽出して保持する partition value は依然として文字列です。型は「メタデータ上の宣言」に過ぎず、物理格納形式は変わりません。
1.3 なぜ DATE 型化で pruning 効率が落ちうるのか
Athena のパーティションフィルタリングには 2 つのステップあります。
Step1: Glue 側の partition pushdown
GetPartitions API の Expression パラメータに WHERE 句の述語を渡し、Glue Data Catalog 内で該当パーティションだけを返してもらう仕組みです。Athena が partition pushdown を行うのは partition key が STRING のときだけ、というのが冒頭の公式記述の意味です。
Step 2: Athena 側でメタデータを受け取った後のクエリエンジンレベルのフィルタリング
型に関係なく動きますが、すでに全パーティションのメタデータ取得コストは支払った後です。
partition key を DATE 型にすると、Athena は型が STRING ではないと判断して partition pushdown を諦め、Glue から全パーティション(あるいは大きなサブセット)のメタデータを取得してからエンジン側で日付比較フィルタを評価するフォールバック動作になりえます。
パーティションが数百程度なら体感差は小さいですが、年×月×日のような構成で数千〜数万パーティションある場合、GetPartitions のページネーション往復が増えてクエリプランニング時間が悪化します。Athena が 1 スキャンで読めるパーティション数の上限は 100 万件ですが、pushdown が効かなければこの上限に近づきやすくなります。
2. Amazon Redshift Spectrum では事情が異なる
ここから話が複雑になります。Spectrum は Athena と同じく Glue Data Catalog を使いますが、partition key の扱いに関するメッセージが Athena とは異なります。
2.1 Spectrum は DATE/TIMESTAMP 型を公式サポート
Spectrum の external table ドキュメントには以下の記述があります。
The data type can be SMALLINT, INTEGER, BIGINT, DECIMAL, REAL, DOUBLE PRECISION, BOOLEAN, CHAR, VARCHAR, DATE, or TIMESTAMP data type.
さらに AWS Big Data Blog の Spectrum ベストプラクティス記事では、むしろ DATE 型の活用を勧める記述があります。
Amazon Redshift Spectrum supports DATE type in Parquet. Take advantage of this and use DATE type for fast filtering or partition pruning.
つまり、Athena の「STRING 推奨」と Spectrum の「DATE も活用せよ」はメッセージとして必ずしも一致していません。Athena の推奨理由が「Glue への partition pushdown が STRING でしか効かない」という Athena エンジンの実装に起因するのに対し、Spectrum は Redshift 自前のオプティマイザでパーティションを評価しているため、事情が異なります。
2.2 Dynamic Partition Pruning による救済
Spectrum には Redshift オプティマイザによる Dynamic Partition Pruning (DPP) があります。
Amazon Redshift employs both static and dynamic partition pruning for external tables.
JOIN の駆動表(小さいディメンション表など)から動的に値を取り出して、それを fact 表の partition filter として後段に渡す仕組みが組み込まれています。DPP の動作を EXPLAIN で見ると、内部的に Filter: (subplan 0: (($3)::text = (date)::text)) のような形になり、両側を text にキャストしてから比較しています。
これは Spectrum が partition value を文字列として保持している(前述の Glue Data Catalog の内部仕様)ことの帰結で、結果として STRING partition との相性は非常に良い動作です。
3. JOIN シナリオの実態:CAST の向きが本質
「Redshift native テーブルの DATE 列と Spectrum external テーブルの STRING partition で JOIN したら、型変換でフルスキャンになるのでは?」という懸念を実務でよく耳にします。これは半分正しく、半分誤解です。
3.1 partition column 側に CAST が適用されると pruning が壊れる
問題が起きるのは partition column 側に関数や型変換が適用されたときです。
-- partition column 側に CAST が適用されるパターン → pruning 無効化
SELECT *
FROM rs_fact r
JOIN spec_dim s ON r.dt = CAST(s.dt AS DATE);
partition column に関数が適用されると pushdown が無効化され、全パーティションをスキャンしてから Redshift 側で評価することになります。SVL_S3QUERY_SUMMARY を見ると is_partitioned が true でも qualified_partitions が total_partitions と一致してしまう、というのが典型的な症状です。
3.2 暗黙変換の方向が partition column 側でなければ pruning は維持される
逆に、暗黙変換が Redshift 側のカラムに適用される場合は問題ありません。
-- 暗黙変換が Redshift 側に適用されるパターン → pruning は維持される
SELECT *
FROM rs_fact r
JOIN spec_dim s ON r.dt = s.dt; -- r.dt は DATE, s.dt は VARCHAR(ISO 8601)
Redshift の暗黙変換ルールでは、DATE → VARCHAR の方向で変換され、内部的には文字列比較になります。CAST 関数は partition column 側ではなく Redshift 側のカラムに適用されるため、Spectrum 層に渡される述語は依然として「partition column に対する等値比較」のままです。
つまり「データ型が違うから自動的に全件スキャン」ではなく、「partition column 側に関数や型変換が適用されるクエリを書いてしまうと全件スキャン」が正確な表現になります。partition value を ISO 8601 (yyyy-MM-dd) フォーマットで統一しておけば、暗黙変換の方向は安全な側に倒れます。
4. 「DATE 関数の便利さ」の幻想と現実
「last_day(dt)、day_of_week(dt)、extract(month from dt) のようなカレンダー関数を使いたいから DATE 型にしたい」という要望は強くあります。ここは正直に向き合うべき論点です。
4.1 partition column への関数適用は型に関係なく pruning を壊す
最も重要な事実として、partition column 自身にカレンダー関数を適用してから比較する型のクエリは、partition key が STRING でも DATE でも pruning が効きません。
-- DATE 型 partition key でも pruning は効かない
WHERE day_of_week(dt) = 5 AND extract(day from dt) <= 7 -- 第一金曜日
WHERE dt = last_day(dt) -- 月末
これらは型選択の問題ではなく、述語の構造の問題です。Athena/Spectrum/Trino のオプティマイザは、partition column に関数が適用された時点で「この述語を partition メタデータの値域と直接突き合わせる形に正規化できない」と判断し、全パーティションのメタデータを引いてからエンジン側で評価するフォールバックに入ります。
4.2 可読性が論点なら、回避パターンは STRING でも書ける
可読性が論点になった瞬間に、別の解法が浮上します。それは「target となる日付集合を右辺で先に計算して、partition column と等値/IN 比較する」パターンです。
-- 右辺で集合を計算 → STRING / DATE どちらでも pruning が効く
WITH first_fridays AS (
SELECT date_format(d, '%Y-%m-%d') AS dt
FROM (
SELECT date_add('day',
(5 - day_of_week(date_trunc('month', m)) + 7) % 7,
date_trunc('month', m)) AS d
FROM (VALUES (DATE '2026-01-01'), (DATE '2026-02-01')) AS t(m)
)
)
SELECT * FROM events
WHERE dt IN (SELECT dt FROM first_fridays);
つまり「DATE 関数の便利さ」は ad-hoc 探索クエリでは確実に効きますが、定常運用クエリでは結局右辺集計のパターンに書き換えることになり、その時点で型選択の差は消えます。
4.3 それでも DATE 関数が必要なワークロードへの対処
それでも last_day() や営業日判定を partition column に直接書きたいというワークロードがある場合、設計を変えるのが本筋です。
第一に、データ側に判定フラグを持たせる ELT 設計、is_month_end BOOLEAN、is_first_friday BOOLEAN、business_day_seq INT のような派生列を ETL/ELT パイプラインで計算してデータに導出しておき、クエリは WHERE dt BETWEEN '...' AND '...' AND is_first_friday のように書く。partition は通常通り日次 STRING で、pruning は範囲指定で効かせます。判定ロジックの定義場所が ETL 側に集約されて、ガバナンス的にも良い設計です。
第二に、Iceberg + hidden partitioning(次節で詳述)。
5. 現代的な選択肢:型選択のジレンマから抜け出す
最近の AWS では、文字列 partition と日付セマンティクスを両立させる仕組みが整っており、二者択一にする必要は薄れています。
5.1 Athena Partition Projection の date タイプ
Partition projection を使えば、partition key を STRING で宣言したまま、Athena に「これは日付として解釈せよ」と伝えられます。
CREATE EXTERNAL TABLE events (
event_id BIGINT,
user_id BIGINT,
payload STRING
)
PARTITIONED BY (dt STRING)
STORED AS PARQUET
LOCATION 's3://my-bucket/events/'
TBLPROPERTIES (
'projection.enabled' = 'true',
'projection.dt.type' = 'date',
'projection.dt.format' = 'yyyy-MM-dd',
'projection.dt.range' = '2020-01-01,NOW',
'projection.dt.interval' = '1',
'projection.dt.interval.unit' = 'DAYS'
);
物理的な partition value は UTF-8 string のまま、Athena の partition pushdown も維持され、かつ NOW を使った相対参照や欠損日付の自動補完など projection の利便性も得られる、というハイブリッド構成です。
5.2 Iceberg の Hidden Partitioning:「STRING 推奨」が当てはまらない世界
ここまで Hive 形式の前提で議論してきましたが、Iceberg では partition 設計の発想とメカニズムが根本的に異なります。「STRING 推奨」のルールはそのまま適用できません。
5.2.1 なぜ Iceberg では話が変わるのか
Hive 形式と Iceberg の最大の違いは、partition value がどこに、どう保存されるかです。
Hive 形式では partition value は Glue Data Catalog の partition オブジェクトに UTF-8 string の配列として保存され、S3 のディレクトリ構造(dt=2026-05-12/)にもエンコードされていました。これが「Athena は STRING でないと pushdown が効かない」という制約の根源でした。
一方、Iceberg では partition value はテーブル直下の manifest ファイル(Avro 形式)にネイティブ型で保存されます。S3 ディレクトリ構造には依存せず、Glue Catalog 側で partition pruning する必要もありません。クエリエンジン(Athena/Spark/Trino/Spectrum)は manifest を直接読み、各データファイルの partition value とカラム統計を見て pruning を行います。
A manifest stores files for a single partition spec. ... The partition spec of each manifest is also used to transform predicates on the table's data rows into predicates on partition values that are used during job planning to select files from a manifest.
つまり Iceberg では、Glue Catalog の UTF-8 string 制約も Athena の STRING 限定 pushdown 制約もそもそも存在しない世界線にいます。
5.2.2 Iceberg の Partition Transforms
Iceberg は partition transform という概念で、ソース列の型を保ちながら派生 partition を生成する仕組みを持ちます。主要な transform は以下の通りです。
| Transform | 用途 | ソース列の型 | partition 値の物理表現 |
|---|---|---|---|
identity |
元の値をそのまま使う | 任意 | ソース型と同じ |
year(col) |
年単位 | TIMESTAMP / DATE | int(年からのオフセット) |
month(col) |
月単位 | TIMESTAMP / DATE | int(月からのオフセット) |
day(col) |
日単位 | TIMESTAMP / DATE | int(日からのオフセット、表示は date) |
hour(col) |
時単位 | TIMESTAMP | int |
bucket(N, col) |
ハッシュで N 個のバケットに分割 | 任意 | int |
truncate(W, col) |
W 単位で切り詰め | string / int / long / decimal | ソース型と同じ |
ここで重要なのは、日付・時刻系の partition は TIMESTAMP/DATE 列に transform を適用するのが正規パターンで、STRING 化したカラムをそのまま partition key にするのは Iceberg 的にはアンチパターンに近い、という点です。
5.2.3 AWS 公式が Iceberg で推奨する partition 設計
AWS Prescriptive Guidance の Iceberg ベストプラクティスには、以下のように明記されています。
If your queries commonly filter on a derivative of a table column, use hidden partitions instead of explicitly creating new columns to work as partitions. ... For example, in a dataset that has a timestamp column (for example, 2023-01-01 09:00:00), instead of creating a new column with the parsed date (for example, 2023-01-01), use partition transforms to extract the date part from the timestamp and create these partitions on the fly.
要約すると「派生用 STRING カラムを別途持つな、TIMESTAMP 列に transform を適用しろ」というのが AWS 公式の Iceberg 向け推奨です。これは Hive 文脈の「STRING 推奨」と真逆のメッセージです。
5.2.4 実装例:Iceberg での partition spec
具体的な DDL は以下のようになります。
-- Iceberg 推奨:TIMESTAMP 列 + day transform
CREATE TABLE events (
event_id BIGINT,
event_time TIMESTAMP,
region STRING,
payload STRING
)
PARTITIONED BY (days(event_time), region)
LOCATION 's3://my-bucket/events/'
TBLPROPERTIES ('table_type' = 'ICEBERG');
クエリ側はソース列に対して自然に書けます。
-- partition column を意識しない自然なクエリ
SELECT count(*)
FROM events
WHERE event_time >= TIMESTAMP '2026-05-01 00:00:00'
AND event_time < TIMESTAMP '2026-06-01 00:00:00'
AND region = 'ap-northeast-1';
Iceberg がクエリの述語を partition spec の transform を通じて partition value の述語に変換し、関連 manifest を絞り込みます。クエリ側で partition_dt のような派生 STRING に WHERE をかける必要がないのが hidden partitioning の本質です。
5.2.5 Iceberg でも残る注意点
Iceberg は柔軟ですが、partition column に「想定外の」型変換関数を適用すると pruning が効かなくなる点は同じです。AWS Prescriptive Guidance には以下の注意書きがあります。
Iceberg cannot perform partition pruning for functions that yield a different data type; for example, substring(event_time, 1, 10) = '2022-01-01'.
year(event_time) や month(event_time) のような Iceberg が認識する関数は OK ですが、substring() のような任意の文字列関数を適用すると pruning が外れます。Iceberg だから何をしても速いわけではなく、partition transform と整合する述語を書く必要があるのは Hive と同様です。
5.3 Amazon S3 Tables(マネージド Iceberg)の場合
Amazon S3 Tables は AWS マネージドの Apache Iceberg テーブルです。コンパクションやスナップショット管理など Iceberg 運用の手間を自動化してくれる仕組みで、テーブル自体は標準的な Iceberg です。
つまり、S3 Tables の partition key 設計は通常の Iceberg と同じ原則が適用されるということです。days(event_time) のような transform を使い、ネイティブ型を保つのがベストプラクティスになります。
S3 Tables 固有の追加考慮事項としては、自動コンパクションが効くため小さいパーティションを大量に作るオーバーパーティションは避けるべき、という点があります。AWS ドキュメントでも以下のように注意喚起されています。
Review partition strategy. Over partitioning can lead to small files.
hours(event_time) などで時間粒度を細かくしすぎると、データ量によっては 1 パーティション数 MB の小さなファイルが大量に生成されてしまい、S3 Tables の自動コンパクションがあっても性能が出にくくなります。日次(days())から始めて、データ量に応じて粒度を調整するのが安全です。
5.4 Iceberg の Glue Data Catalog 連携と partition 管理
最近では Glue Data Catalog の API レベルで Iceberg の partition spec を直接管理できるようになっています。
PartitionSpec の Fields に Transform: "year" や Transform: "identity" を直接指定する形になり、Hive 形式の partition オブジェクトとは完全に別物として扱われます。CloudFormation や CDK でテーブルを管理する場合も、Iceberg 形式と Hive 形式は別の API パス(OpenTableFormatInput.IcebergInput vs PartitionKeys)になるので、IaC のテンプレート設計時に注意が必要です。
5.5 二重持ち(STRING partition + DATE 通常カラム)
Hive 形式の既存テーブルで partition の型を変更できない場合、テーブル内に通常カラムとして event_date DATE を持っておき、ETL 側で partition の dt 文字列と同期させる、という運用もあります。集計や日付演算は event_date 列を使い、pruning が必要なときは WHERE dt = '...' AND event_date = DATE '...' のように両方書きます。冗長ですが既存資産との互換性を保ちつつ移行できる現実的な妥協案です。
新規設計なら、この二重持ちパターンを採用するくらいなら最初から Iceberg にしてしまうほうが圧倒的に筋が良いです。
6. 実装時のチェックリスト
実際に partition key を STRING で運用する際の運用ルールをまとめます。
設計時
- partition value のフォーマットは ISO 8601 (
yyyy-MM-dd) で統一する。辞書順と時系列順が一致するため、BETWEEN範囲指定が想定通りに動く - パーティション軸を増やしすぎない。多軸パーティションはパーティション数が爆発する
- 新規設計なら Iceberg + hidden partitioning、または Partition Projection の利用を検討する
クエリ時
- partition column 側に CAST や関数を適用しない(
WHERE CAST(dt AS DATE) = ...のような書き方を禁止) - JOIN 条件だけに頼らず、可能な限り Spectrum 側にも明示的な
WHERE spec.dt BETWEEN '...' AND '...'を併記する - DPP に頼り切らない静的 pruning の保険を残す
検証時
- Athena は
EXPLAIN/EXPLAIN ANALYZEで partition 数と data scanned を確認する - Redshift Spectrum は
SVL_S3PARTITIONのqualified_partitions / total_partitions比率、SVL_S3QUERY_SUMMARYのis_partitioned、s3_scanned_bytesで pruning の効きを確認する - BI ツールから自動生成されるクエリで CAST が partition column 側に適用するパターンがないか、view やマクロでラップして制御する
最後に
Athena 公式の「Always use STRING as the type for partition keys」は、Glue Data Catalog の内部仕様(partition value が UTF-8 string で保持される)と、Athena の partition pushdown 実装に基づいたHive 形式テーブルにおける正しい原則です。
しかし、これはあくまで Hive 形式の文脈での話であり、以下のような状況では機械的に当てはまらないことを理解しておく必要があります。
- Redshift Spectrum は DATE/TIMESTAMP 型 partition key を公式サポートしており、DPP も組み合わさるため、Athena ほど厳しい型制約はない
- partition column にカレンダー関数を直接適用する分析クエリは、型選択に関係なく pruning が効かない
- Iceberg(汎用 S3 / S3 Tables 問わず)では partition value が manifest にネイティブ型で保存されるため、「STRING 推奨」のルール自体が当てはまらない
- Iceberg では AWS 公式が「派生 STRING カラムを作らず、TIMESTAMP/DATE に transform を適用せよ」と明確に推奨している
- Hive 形式新規設計でも Athena Partition Projection の
datetype で、型のジレンマ自体を回避できる
実務的な落としどころは以下になります。
- Hive 形式の既存テーブルで Athena/Spectrum 両方を消費者として想定する場合 → STRING(ISO 8601 形式)で統一
- 新規設計で選択肢があるなら → Apache Iceberg(汎用 S3)または S3 Tables を第一候補に検討、partition は TIMESTAMP/DATE + transform
- partition column に複雑なカレンダー判定を書きたいワークロード → Iceberg + hidden partitioning または ETL で派生フラグ列を追加
「partition key は常に STRING」という単純なルールで設計を決めるのではなく、テーブルフォーマット・クエリエンジン・ワークロードの 3 軸で判断するのが本質的な答えになります。










