Amazon Athenaの読み方は「アテナ」です。S3に置いたファイルへ、サーバーを用意せずにSQLを直接投げられるAWSの分析サービスで、料金は読み込んだデータ量で決まります。この記事が重点的に扱うのは、テーブル定義(DDL)とSELECT(DML)で文法の系統が違う点と、UTCで記録された時刻の日付境界の2つです。この記事では、仕組みと使い始めの手順を押さえたうえで、よく使うクエリの型をOSSのTrino 484で実行した結果付きで示し、エラーの直し方と東京リージョンの料金までまとめます。
まとめ:Athenaの要点とクエリを書くときの注意
Athenaは、S3のデータをGlueデータカタログのテーブル定義で読み、Trinoを基にしたエンジンでSQLを実行するサーバーレスのサービスです。東京リージョンのSQLクエリは読み込んだデータ1TBあたり5.00 USDで、1クエリの最小課金は10MBです。
- DDL(
CREATE EXTERNAL TABLEなど)はHive系の文法で、予約語はバッククォートで囲む。SELECTはTrino系の文法で、識別子はダブルクォート、文字列はシングルクォート。 - timestamp列は文字列と直接比べられない。
TIMESTAMP '2026-10-02 00:00:00'のように型付きで書く。UTCのログを日本時間で日付集計するならwith_timezone(列, 'UTC') AT TIME ZONE 'Asia/Tokyo'で変換してから日付にする。 - 料金と速度はパーティション列の絞り込みと読む列の数で決まる。集計の後ろに付けたLIMITは読み込み量を減らさない。
- 定型の集計はビューかCTASで再利用し、同じクエリを繰り返すならクエリ結果の再利用(最大7日)を使う。
Amazon Athena(アテナ)の読み方と概要
Athenaはカタカナで「アテナ」と表記されます。AWSの日本語ドキュメントにも、Athenaのクエリ例をまとめた「アテナクエリの操作」というページがあります。
サービスとしては、S3上のCSV・JSON・Parquet・ORCなどのファイルを、データベースへ取り込まずにそのままSQLで集計するためのものです。クラスターの起動や容量の見積もりが要らず、従量課金ならクエリを投げたときだけ課金されます(処理能力を時間で確保する予約容量は、クエリを流していない時間も課金されます)。AWSのAthena FAQは、SQLエンジンについて「Amazon Athena SQL engine version 3 built on Trino」と説明しています。SQLのほかにApache Sparkのノートブックで処理する使い方もありますが、この記事はSQLの使い方に絞ります。
向いているのは、CloudTrailやALBのアクセスログ、アプリのイベントログのように、S3にたまり続けるデータを必要なときだけ集計する用途です。1件ずつ頻繁に更新・参照するOLTPの処理や、ミリ秒単位の応答が必要な画面の裏側には向きません。OLTPとOLAPの違いから選び方を整理したい場合はOLTPとOLAPの違いとAWSのOLAP基盤Redshift・Athenaの選び方が参考になります。
Athenaの仕組み:S3・Glueデータカタログ・Trinoの役割分担
Athenaは、データ・テーブル定義・実行エンジンを別々のサービスに分けて動いています。データを動かさずに済むのはこの分担のおかげで、逆に言うとテーブル定義とS3上の実ファイルが食い違うと、HIVE_BAD_DATAなどのエラーで止まるか、空の結果や化けた値が返ります。
| 層 | 担当 | 持っているもの |
|---|---|---|
| データ | Amazon S3 | CSV・JSON・Parquet・ORCなどのファイル |
| テーブル定義 | AWS Glueデータカタログ | 列名・型・S3の場所・パーティション |
| 実行 | Athena(engine version 3) | Trinoを基にした分散SQLエンジン |
| 結果 | Amazon S3 | クエリ結果のCSVとメタデータ |
クエリを実行すると、Athenaはまずデータカタログからテーブルの列構成とS3の場所を読み、該当するファイルだけを並列に読み込みます。Parquetのような列指向のファイルなら、SELECT・WHERE・JOINなどクエリの処理に必要な列だけを読むので読み込み量が減ります。列指向形式の中身はParquetの仕組みとCSVとの違い、テーブル定義を自動で作るクローラーはAWS Glueの機能と使い方、エンジンの元になっているOSSはTrinoの仕組みとPresto・Sparkとの違いで扱っています。
Athenaを使い始める3つの手順
コンソールから最初のクエリを流すまでの手順です。この記事の構文はAthena engine version 3を前提にしているので、ワークグループのエンジンバージョンも最初に確認してください。
手順1:クエリ結果の保存先の設定
結果の置き場所は、Athenaが管理するマネージドクエリ結果と、自分のS3バケットの2通りから選べます。自分のバケットを使う場合は、結果の保存先(例:s3://my-athena-results/)をワークグループかクエリエディタの設定で指定します。分析対象のバケットとは分けておくと、結果ファイルが分析対象に混ざる事故を防げます。
手順2:S3のデータのテーブル定義
S3の場所と列構成をDDLで登録します。手でCREATE EXTERNAL TABLEを書く方法と、Glueクローラーに推定させる方法があります。ファイルの置き場所を日付ごとのフォルダ(dt=2026-10-01/など)に分けておくと、後述のパーティションで読み込み量を絞れます。
手順3:クエリエディタでのSQL実行
データベースを選んでSELECTを実行すると、結果が画面に表示されます。自分のバケットを保存先にした場合は、同じ内容がそこにCSVで残ります。実行後に表示される「スキャンしたデータ」が課金対象の量です。アプリやバッチから非同期に実行する方法はAWS Athenaの実装:SDKでのクエリ実行とフェデレーテッドクエリ・Iceberg更新で解説しています。
Athenaのクエリの書き方:DDLはHive系、SELECTはTrino系
ここからが書き方の本題です。AthenaのDDLリファレンスには「With some exceptions, Athena DDL is based on HiveQL DDL and Athena DML is based on Trino.」とあり、同じAthenaの中で文法の系統が2つあります。以下のSELECTの例は、OSSのTrino 484をローカルで起動し、memoryコネクタに作ったサンプル表で実行した結果です。Athena本体ではないため、関数の対応範囲やエラーメッセージの文言が一部異なる可能性があります。
識別子と文字列のクォートの使い分け
取り違えやすいのはクォートの記号です。予約語のエスケープの公式ページは、DDLでは予約語をバッククォート、SELECTとビューへのクエリではダブルクォートで囲むと定めています。
| 文の種類 | 文法の系統 | 予約語・識別子 | 文字列 |
|---|---|---|---|
| CREATE EXTERNAL TABLE・ALTER TABLE | Hive系 | `date` |
'...' |
| SELECT・CTAS・CREATE VIEW | Trino系 | "date" |
'...' |
SELECTでバッククォートを使うと、Trino 484では次のエラーで止まりました。文字列をダブルクォートで囲んだ場合は、値ではなく列名として解釈されます。
SELECT `user_id` FROM access_logs;
-- line 1:8: backquoted identifiers are not supported; use double quotes to quote identifiers
SELECT user_id FROM access_logs WHERE region = "tokyo";
-- line 1:48: Column 'tokyo' cannot be resolved
大文字の扱いにも注意が要ります。名前の付け方の公式ガイドによると、AthenaはDDLで作るデータベース名・テーブル名・列名を小文字に変えます。ダブルクォートで囲んでも大文字は保持されないので、最初から小文字とアンダースコアで命名してください。
CREATE EXTERNAL TABLEによるS3データのテーブル定義
日付フォルダに分けたJSONログを読むテーブル定義の例です。JSONは1行に1レコードで置き、整形して複数行にしたファイルは置かないでください。DDLなので予約語のdateを列名に使う場合はバッククォートで囲みます。TBLPROPERTIESのprojection.*は、パーティションの値をS3から探さずに計算させるパーティション射影の設定です(このDDLはAthena固有の構文のため、ローカルのTrinoでは実行していません)。
CREATE EXTERNAL TABLE access_logs (
request_time timestamp,
user_id string,
url string,
status int,
bytes bigint,
region string
)
PARTITIONED BY (dt string)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://my-log-bucket/access_logs/'
TBLPROPERTIES (
'projection.enabled' = 'true',
'projection.dt.type' = 'date',
'projection.dt.format' = 'yyyy-MM-dd',
'projection.dt.range' = '2026-01-01,NOW',
'projection.dt.interval' = '1',
'projection.dt.interval.unit' = 'DAYS',
'storage.location.template' = 's3://my-log-bucket/access_logs/dt=${dt}/'
);
パーティション射影は、Glueのテーブルプロパティからパーティションの値と場所を計算する仕組みで、射影の型はenum・integer・date・injectedの4種類です。射影を使わない場合は、新しいフォルダを置くたびにMSCK REPAIR TABLE access_logsでパーティションをカタログへ登録します。日付で毎日フォルダが増えるログなら、登録作業の要らない射影が向いています。ただし射影の範囲外の値や存在しないフォルダを指定してもエラーにはならず0行が返り、射影したパーティションの半分以上が空なら通常のパーティションを使うよう公式は勧めています。範囲の設定が実データと合っているかは最初に確かめてください。
SELECT・WHERE・GROUP BYの基本形
以降の例では、上のaccess_logsと同じ列を持つ6行のサンプル表と、会員マスターusers、JSONを持つeventsを使います。結果を手元で再現できるよう、Trinoのmemoryコネクタで作ったときの定義と投入データを載せます(Athenaではなく検証用のTrinoで使う文です)。
CREATE TABLE access_logs (request_time timestamp(3), dt varchar, user_id varchar,
url varchar, status integer, bytes bigint, region varchar);
INSERT INTO access_logs VALUES
(TIMESTAMP '2026-10-01 00:10:00', '2026-10-01', 'u1', '/tech/details/4865/', 200, 5120, 'tokyo'),
(TIMESTAMP '2026-10-01 09:15:00', '2026-10-01', 'u2', '/tech/details/5547/', 200, 3072, 'osaka'),
(TIMESTAMP '2026-10-01 15:30:00', '2026-10-01', 'u1', '/contact/', 500, 256, 'tokyo'),
(TIMESTAMP '2026-10-02 23:50:00', '2026-10-02', 'u3', '/tech/details/4865/', 404, 128, 'tokyo'),
(TIMESTAMP '2026-10-02 10:00:00', '2026-10-02', 'u2', '/tech/details/4865/', 200, 5120, NULL),
(TIMESTAMP '2026-10-03 08:00:00', '2026-10-03', 'u4', '/tech/details/5545/', 200, 4096, 'osaka');
CREATE TABLE users (user_id varchar, plan varchar);
INSERT INTO users VALUES ('u1', 'free'), ('u2', 'pro'), ('u3', 'free');
CREATE TABLE events (event_id integer, payload varchar);
INSERT INTO events VALUES
(1, '{"user":{"id":"u1","country":"JP"},"items":[{"sku":"A-1","qty":2},{"sku":"B-7","qty":1}]}'),
(2, '{"user":{"id":"u2","country":"US"},"items":[]}'),
(3, '{"user":{"id":"u3"},"items":[{"sku":"A-1","qty":5}]}');
まずはエラーが出た行を絞り込み、URLごとのPV・UU・転送量を集計する基本形です。
SELECT dt, user_id, url, status
FROM access_logs
WHERE dt BETWEEN '2026-10-01' AND '2026-10-02'
AND status >= 400
ORDER BY request_time;
-- dt | user_id | url | status
-- 2026-10-01 | u1 | /contact/ | 500
-- 2026-10-02 | u3 | /tech/details/4865/ | 404
SELECT url, count(*) AS pv, count(DISTINCT user_id) AS uu, sum(bytes) AS total_bytes
FROM access_logs
GROUP BY url
HAVING count(*) >= 2
ORDER BY pv DESC;
-- /tech/details/4865/ | pv=3 | uu=3 | total_bytes=10368
SELECTに書いた非集計列は、すべてGROUP BYに入れる必要があります。SELECT user_id, url ... GROUP BY user_idと書くと'url' must be an aggregate expression or appear in GROUP BY clauseで失敗しました。GROUP BY 1, 2のように列の位置番号でも書けます。
LEFT JOINによる会員マスターとの結合
アクセスログに会員マスターの区分を付ける例です。マスターに無いユーザーも残したいのでLEFT JOINにし、NULLをcoalesceで置き換えます。
SELECT a.user_id, coalesce(u.plan, 'unknown') AS plan, count(*) AS pv
FROM access_logs a
LEFT JOIN users u ON a.user_id = u.user_id
GROUP BY 1, 2
ORDER BY 1;
-- u1 | free | 2
-- u2 | pro | 2
-- u3 | free | 1
-- u4 | unknown | 1
AWSのAthenaのパフォーマンスチューニング10選は、大きいテーブルを結合の左、小さいテーブルを右に置くよう勧めています。右側のテーブルから検索用のテーブルを作るためで、同記事の例では順番を入れ替えるだけで約20%速くなっています。等値でない結合条件はクロス結合になりやすいので、EXPLAINでCrossJoinが出ていないかを見てください。
WITH句とウィンドウ関数による累計・前日差・最新1件の抽出
日別PVの累計と前日差、ユーザーごとの最新アクセスを出す例です。WITH句で日別集計を作り、その結果にウィンドウ関数を重ねます。
WITH daily AS (
SELECT dt, count(*) AS pv FROM access_logs GROUP BY dt
)
SELECT dt, pv,
sum(pv) OVER (ORDER BY dt) AS cumulative_pv,
pv - lag(pv) OVER (ORDER BY dt) AS diff
FROM daily
ORDER BY dt;
-- 2026-10-01 | 3 | 3 | NULL
-- 2026-10-02 | 2 | 5 | -1
-- 2026-10-03 | 1 | 6 | -1
SELECT user_id, url, request_time
FROM (
SELECT user_id, url, request_time,
-- 同じ時刻の行が複数あると選ばれる行は固定されない。ログIDなどで順序を足す
row_number() OVER (PARTITION BY user_id ORDER BY request_time DESC) AS rn
FROM access_logs
)
WHERE rn = 1;
先頭日のdiffは比較する前日が無いのでNULLになります。0として扱いたい場合はcoalesce(pv - lag(pv) OVER (ORDER BY dt), 0)と書きます。lagが参照するのは前日ではなく1つ前の行なので、アクセスが0件の日がある実データでは、日付の一覧をsequenceとUNNESTで作ってLEFT JOINし、欠けた日を0で埋めてから計算します。
日付と時刻を書く:文字列比較とUTCの日付境界
timestamp列を文字列と比べると型エラーになります。Trino 484でWHERE request_time >= '2026-10-02'を実行するとCannot apply operator: timestamp(3) <= varchar(10)で失敗しました。比較する値を型付きのリテラルにするか、列のほうを日付に変換します。
-- NG: timestamp と varchar の比較
SELECT count(*) FROM access_logs WHERE request_time >= '2026-10-02';
-- OK: 型付きリテラルで比べる(結果 3)
SELECT count(*) FROM access_logs WHERE request_time >= TIMESTAMP '2026-10-02 00:00:00';
-- OK: 日付にそろえて比べる(結果 2)
SELECT count(*) FROM access_logs WHERE CAST(request_time AS date) = DATE '2026-10-02';
もう1つの落とし穴が時刻帯です。ログの時刻がUTCで記録されている場合、そのまま日付で集計すると日本時間の日付とずれます。サンプル表のUTCの2026-10-02 23:50は、日本時間では10月3日の8:50です。
SELECT CAST(with_timezone(request_time, 'UTC') AT TIME ZONE 'Asia/Tokyo' AS date) AS jst_date,
count(*) AS pv
FROM access_logs
GROUP BY 1
ORDER BY 1;
-- UTCの日付(dt)で数えると 10/01=3, 10/02=2, 10/03=1
-- 日本時間で数えると 10/01=2, 10/02=2, 10/03=2
6行のうち2行が日付をまたいで別の日に移りました。ここでwith_timezone(request_time, 'UTC')を挟んでいるのは、時刻帯を持たないtimestampにAT TIME ZONEを直接付けると、セッションの時刻帯で解釈されるためです。Trino 484で確かめると、request_time AT TIME ZONE 'Asia/Tokyo'はセッションがUTCなら10月3日8:50、セッションがAsia/Tokyoなら10月2日23:50と、同じSQLで結果が変わりました。with_timezoneで「この値はUTC」と明示した書き方は、どちらのセッションでも10月3日8:50を返します。日次レポートの数字が管理画面と合わないときは、まずこの変換の有無を疑ってください。文字列の日時はdate_parse('2026/10/01 09:15:00', '%Y/%m/%d %H:%i:%s')、ISO 8601形式ならfrom_iso8601_timestampで変換できます。engine version 3の変更点によると、CAST('2021-06-06-23:38:46' AS timestamp)のように日付と時刻の間がハイフンの文字列は変換できなくなっているため、間は半角スペースにします。
json_extract_scalarによるJSONの値の抽出とUNNESTによる配列の展開
1列にJSON文字列が入っているイベントログから値を取り出し、配列を1要素1行に展開する例です。json_extract_scalarは値をvarcharで返し、キーが無い場合はNULLになります。
SELECT event_id,
json_extract_scalar(payload, '$.user.id') AS user_id,
json_extract_scalar(payload, '$.user.country') AS country
FROM events;
-- 1 | u1 | JP
-- 2 | u2 | US
-- 3 | u3 | NULL
SELECT e.event_id,
json_extract_scalar(item, '$.sku') AS sku,
CAST(json_extract_scalar(item, '$.qty') AS integer) AS qty
FROM events e
CROSS JOIN UNNEST(CAST(json_extract(e.payload, '$.items') AS array(json))) AS t(item);
-- 1 | A-1 | 2
-- 1 | B-7 | 1
-- 3 | A-1 | 5
CROSS JOIN UNNESTは、配列が空の行(この例のevent_id=2)を結果から落とします。空の行も残したいときはLEFT JOIN UNNEST(...) AS t(item) ON TRUEと書きます。TrinoのSELECTリファレンスは、UNNESTのLEFT JOINで使える条件はON TRUEだけだと明記しています。要素の順番が必要ならWITH ORDINALITYを付けると、末尾に1から始まる番号の列が付きます。
ROW型(構造体)の配列は書き方が環境で分かれます。Athenaの配列のフラット化の公式ページはUNNEST(dataset.users) AS t(person)のように1つの名前で受け、person.nameで参照する例を載せています。同じ書き方をTrino 484で実行するとColumn alias list has 1 entries but 't' has 2 columns availableで失敗し、AS t(name, department)のようにフィールドの数だけ列名を並べる必要がありました。同じSQLをEMRのTrinoなど別の環境でも流す予定があるなら、上の例のようにarray(json)へ変換してから取り出すと、どちらの解釈にも依存しません。
CTASによる集計結果の保存とビューによるSQL定義の再利用
CTAS(CREATE TABLE AS SELECT)は、SELECTの結果を新しいテーブルとしてS3に書き出します。CTASの公式リファレンスによると、形式を省略するとParquetになり、Parquetの圧縮を省略するとGZIPが使われます。パーティション列はSELECTの最後に置き、external_locationには空のS3の場所を指定します。下のCTASのWITH句はAthena固有のプロパティのため、ローカルのTrinoでは列と集計だけを確認し、プロパティは未検証です。
CREATE TABLE daily_pv
WITH (
format = 'PARQUET',
write_compression = 'SNAPPY',
external_location = 's3://my-log-bucket/marts/daily_pv/',
partitioned_by = ARRAY['dt']
) AS
SELECT url, count(*) AS pv, dt
FROM access_logs
WHERE dt >= '2026-10-01'
GROUP BY url, dt;
CREATE OR REPLACE VIEW error_logs AS
SELECT dt, user_id, url, status
FROM access_logs
WHERE status >= 400;
CTASで1回に書き込めるパーティションは100までです(CTASの考慮事項)。それを超える期間を作るときは、CTASで最初の100日分を作り、残りをINSERT INTOで100パーティションずつ追加します。ビューはデータを持たず、参照のたびに元のテーブルを読むので、料金はビュー経由でも元テーブルの読み込み量で決まります。重い集計を何度も参照するならCTAS、条件の共通化だけが目的ならビュー、という使い分けになります。
書き方で料金と速度が変わるポイント
Athenaの料金は読み込んだデータ量で決まるため、同じ結果を返すクエリでも書き方で請求額が変わります。効果の大きい順に並べます。
パーティション列と参照する列の絞り込み
最も効くのは、WHEREにパーティション列の条件を必ず入れることです。dtの条件が無いクエリは全期間のファイルを読みます。次が参照する列の数で、Parquetなら出力列と条件・結合に使う列だけを読み、ほかの列は読みません。列を絞る効果は速度にも出ます。前述のAWSのチューニング記事が示した例では、全列を取るSELECT *が19.7秒、必要な3列だけで5.2秒でした(測定条件は同記事のとおりで、Parquetの読み込み量の実測ではありません)。データ形式をParquetに変え、パーティションで絞った場合の請求額の下がり方はAmazon Athenaの料金:スキャン量課金の実額とパーティション設計での削減で試算しています。
LIMITが効く場所と効かない場所
LIMITの位置は実行計画で確かめられます。Trino 484でEXPLAIN SELECT url, count(*) FROM access_logs WHERE dt = '2026-10-01' GROUP BY url LIMIT 10を実行すると、Limitは集計(Aggregate[type = FINAL])の後ろに置かれ、テーブルを読むScanFilterProjectには件数の上限が付いていませんでした。集計や並べ替えの後にLIMITを付けても、読み込み量は減らないということです。
一方で、並べ替え自体は軽くなります。AWSのチューニング記事では、約6,000万行のORDER BYが274秒、LIMIT 10000を付けると4.6秒でした。上位N件だけ欲しいときはORDER BY ... LIMITを組み合わせ、料金を抑えたいときはパーティション列で読む範囲そのものを絞ってください。
近似関数と結果の再利用
厳密な値が要らない集計は、count(DISTINCT user_id)をapprox_distinct(user_id)に置き換えると速くなります。AWSの記事によると標準誤差は2.3%で、例では7.7秒が4.6秒に縮みました。ダッシュボードのように同じクエリを繰り返す場合は、クエリ結果の再利用を有効にすると、同じワークグループで同じクエリを流したとき、指定した期間内(既定60分・最大7日)は前回の結果を返し、データを読み直しません。期間内は元データが更新されても確認しないため、古い結果が返ることがあります。更新頻度より短い期間を指定してください。大量の結果をファイルとして取り出すなら、UNLOAD文でParquet・ORC・Avro・JSON・TEXTFILEのいずれかでS3へ書き出せます。
Athenaでよく出るエラーと直し方
書き方の誤りで出やすいエラーを、原因と直し方の組で並べます。上の4行はTrino 484での実測、下の4行はengine version 3の破壊的変更に載っているAthenaのエラーです。
| エラーメッセージ(抜粋) | 原因 | 直し方 |
|---|---|---|
| backquoted identifiers are not supported | SELECTでバッククォート | "列名"に変える |
Column 'tokyo' cannot be resolved |
文字列をダブルクォート | 'tokyo'に変える |
Cannot apply operator: timestamp(3) <= varchar(10) |
timestampと文字列の比較 | TIMESTAMP '...'で書く |
Cannot cast '12a' to INT |
数値に変換できない値 | try_castでNULLにする |
| must be an aggregate expression or appear in GROUP BY clause | 入れ子の列を引用符なしでGROUP BY | "user"."name"と書く |
| There must be two or more concatenation arguments | CONCATの引数が1つ | CONCAT(str, '') |
| Value cannot be cast to timestamp | 日付と時刻の間がハイフン | 半角スペースにする |
| SERIALIZATION_ERROR … timestamp(3) | データのtimestampがミリ秒より細かい | ミリ秒精度にそろえる |
型の扱いでは、整数どうしの割り算にも注意してください。Trino 484でSELECT 5 / 2は2、5 / 2.0は2.500000、CAST(5 AS double) / 2は2.5を返しました。率を計算するときは、どちらかをdoubleに変換してから割ります。count(region)はNULLの行を数えず、count(*)は数えるため、欠損のある列で件数を比べるときも結果が変わります。
Athenaの料金:東京リージョンの単価と課金されないもの
SQLクエリの課金は、読み込んだデータ量による従量制と、処理能力を時間で確保する予約容量(Capacity Reservations)の2通りです。東京リージョンの単価はAWSの料金API(2026-09-11公開版)の値です。
| 課金の対象 | 東京リージョンの単価 | 条件 |
|---|---|---|
| SQLクエリ(従量) | 5.00 USD/TB | 1クエリの最小10MB |
| 予約容量 | 0.43 USD/DPU時 | 最小4 DPU・最短1分 |
| Apache Spark | 0.50 USD/DPU時 | ノートブックの実行時間 |
| DDL(CREATE・ALTER・DROP) | 課金なし | 従量課金の場合 |
| 失敗したクエリ | 課金なし | 従量課金の場合 |
| キャンセルしたクエリ | 読み込んだ分を課金 | キャンセル時点まで |
キャンセルの扱いはAthena FAQの「you are charged for the amount of data scanned up to the point at which you canceled the query」に基づきます。誤って全期間を読むクエリを流したときは、早く止めるほど請求が小さくなります。予約容量の最小4 DPU・最短1分は2026年2月10日の発表で始まった条件です。S3のGETリクエスト料金と結果ファイルの保存料は別にかかり、小さなファイルが大量にあるとリクエスト料金が膨らみます。最新の単価はAthenaの料金ページで確認してください。
AthenaとRedshiftの使い分け
どちらもSQLで分析するサービスですが、データの置き場所と課金の単位が違います。
| 項目 | Amazon Athena | Amazon Redshift |
|---|---|---|
| データの場所 | S3に置いたまま | 主にロード(Spectrumで参照も可) |
| 課金 | 読み込んだデータ量 | ノード時間またはRPU時間 |
| 準備 | テーブル定義のみ | クラスターかServerlessの作成 |
| 向く処理 | 不定期の集計・ログ調査 | 毎日の定型集計・BIの常時参照 |
判断の材料は、実行頻度と1回あたりの読み込み量です。月に数回の調査や、担当者が条件を変えながら探る分析なら、使っていない時間に費用がかからないAthenaが候補になります。同じ大きな集計を1日に何十回も回すBIの裏側では、読み込みのたびに課金されるAthenaより、データを取り込んで常時動かすRedshiftのほうが月額を見積もりやすくなります。どちらが安いかは、想定の読み込み量と稼働時間をそろえて両方の料金で試算して決めてください。Redshiftのノード種別と料金はAmazon Redshift(レッドシフト)の解説にまとめています。S3のデータ基盤ごと設計から相談したい場合は、データ分析基盤構築・MLOps構築支援で対応しています。
Athenaの制約と向かない使い方
公式のAthenaのクォータのうち、クエリの書き方に関係するのは次の3つです。
- DMLクエリのタイムアウトは既定30分で、引き上げ申請により最大240分まで延ばせる。
- クエリ文字列の最大長は262,144バイトで、引き上げはできない。
- 東京リージョンのアクティブなDMLクエリ数(実行中と実行待ちの合計)は、既定で1アカウント150件。
長いIN句を文字列連結で組み立てるバッチは、件数が増えると262,144バイトの上限に当たります。値のリストはS3にファイルとして置き、テーブルとしてJOINする形に変えてください。テーブルの行を更新・削除したい場合は、通常のHiveテーブルではなくIcebergテーブルにするとUPDATE・DELETE・MERGE INTOが使えます。IcebergとParquetの関係はIcebergとParquetの違いで説明しています。
2026年のAthenaの主な更新
2026年のWhat’s Newでは、予約容量の最短1分・最小4 DPU化(2月10日)と提供リージョンの追加(3月30日)、Athena SparkのAWS PrivateLink対応(4月21日)が発表されました。クエリの書き方に影響するのは、4月23日のマネージドコネクタです。DynamoDB・PostgreSQL・MySQL・Snowflakeなど12種のデータソースを、Lambdaのコネクタを自前で配置せずにAthenaのSQLから参照できるようになりました。AWSのコストをAthenaで集計する具体例としては、CUR 2.0をData Exportsで出力しAthenaで集計する手順があります。
よくある質問
Amazon Athenaの読み方は?
「アテナ」と読みます。AWSの日本語ドキュメントでもカタカナで「アテナ」と表記されています。
AthenaとTrinoはどういう関係ですか?
AthenaのSQLエンジン(engine version 3)はTrinoを基にしています。SELECTの文法と関数はTrinoのドキュメントで調べられますが、テーブル定義のDDLはHive系の文法で、Trinoのコネクタは使えないなどの違いがあります。
Athenaでビュー(VIEW)は作れますか?
作れます。CREATE OR REPLACE VIEWで定義し、参照のたびに元のテーブルを読みます。ビューへのクエリで予約語を使うときは、SELECTと同じくダブルクォートで囲みます。
Athenaの料金はいくらからかかりますか?
東京リージョンでは読み込んだデータ1TBあたり5.00 USDで、1クエリの最小課金は10MBです。10MBなら約0.00005 USDなので、少量のデータを試す段階の費用はほぼかかりません。
AthenaでCROSS JOINはどう使いますか?
配列の展開に使うCROSS JOIN UNNESTが代表的です。2つのテーブルを条件なしでCROSS JOINすると行数の掛け算になり、中間結果と計算量が膨らむため、結合条件を書けるならJOIN … ONを使います。