AWS Athena Cost Optimization: Cut Query Bills 90% with Partitioning, Parquet, and Iceberg (2026)

Cut AWS Athena bills 80-95% with partitioning, Snappy Parquet, and Iceberg tables. Runnable DDL, current pricing, partition projection, Capacity Reservations breakeven math, and workgroup guardrails to stop runaway analyst queries.

AWS Athena Cost Optimization Guide 2026

Updated: June 24, 2026

AWS Athena cost optimization is the practice of reducing the data scanned per query, because Athena bills $5 per TB scanned in most regions, so partitioning, converting raw CSV/JSON to compressed Parquet, and adopting Apache Iceberg tables routinely cut bills by 80–95% without changing a single SQL statement. I've personally migrated three petabyte-scale lakes off raw text formats over the past year, and honestly, the same pattern keeps showing up: the query is fine, the layout is the problem. This guide walks through the exact moves that move the needle, with runnable DDL, current pricing, and the breakeven math on Athena Capacity Reservations.

  • Athena per-query pricing is $5.00 per TB scanned (us-east-1, 2026), rounded up to the nearest MB with a 10 MB minimum per query, so every gigabyte you skip is real money saved.
  • Converting CSV/JSON to Snappy-compressed Parquet typically cuts scan volume 80–95% because Athena only reads the columns and row groups your WHERE clause touches.
  • Partition projection eliminates the Glue ALTER TABLE ADD PARTITION overhead and avoids hitting the 1M-partition Glue limit on high-cardinality date tables.
  • Apache Iceberg tables on Athena enable hidden partitioning, partition evolution, and row-level MERGE/DELETE, and they reduce scan volume further via column-level metadata pruning.
  • Athena Capacity Reservations (DPU-based pricing) break even around 200–400 TB scanned per month per workgroup, depending on query concurrency patterns.
  • Workgroup data-scan limits and per-query bytes-scanned cutoffs are the single best guardrail against runaway analyst queries.

How Athena pricing actually works in 2026

Athena has two pricing modes, and you need to understand both before you optimize anything. The default (and what 95% of accounts I audit are still using) is per-query pricing: $5.00 per TB of data scanned in us-east-1, billed per query, rounded up to the nearest megabyte with a 10 MB floor. DDL statements (CREATE TABLE, MSCK REPAIR TABLE, etc.) are free. Failed queries are free. Cancelled queries are charged for whatever they scanned before cancellation. The second mode is Capacity Reservations, which prices Athena like a provisioned engine: you reserve DPUs (Data Processing Units) and pay $0.30 per DPU-hour, with a 24-DPU minimum and a 1-hour minimum commitment.

"Data scanned" is not the same as "data in S3". With columnar formats and proper partitioning, Athena reads only the bytes it needs. A query like SELECT user_id FROM events WHERE event_date = '2026-06-23' against a 100 TB Parquet table partitioned by date might scan less than 200 MB, costing one-tenth of a cent. The same query against gzipped JSON in a flat prefix will scan the full 100 TB and cost $500. Read the official Athena pricing page carefully, because federated queries (RDS, DynamoDB, etc.) are billed differently per DPU-second, not per byte.

Measure first: find your most expensive queries

Don't optimize what you haven't measured. Athena writes query metadata to aws.athena.queries which surfaces in CloudWatch metrics, but the cleanest source is the Athena query history itself, which you can query with the Athena API or, better, pipe to S3 and query with Athena. Set up an audit workgroup that writes results back to S3, then run this query daily:

-- Top 20 most expensive queries in the last 7 days
SELECT
  query_execution_id,
  query,
  data_scanned_in_bytes / 1099511627776.0 AS tb_scanned,
  (data_scanned_in_bytes / 1099511627776.0) * 5.00 AS estimated_cost_usd,
  engine_execution_time_in_millis / 1000.0 AS seconds,
  workgroup,
  submission_date_time
FROM athena_query_audit.query_executions
WHERE submission_date_time >= current_date - interval '7' day
  AND state = 'SUCCEEDED'
ORDER BY data_scanned_in_bytes DESC
LIMIT 20;

What you almost always find: 80% of spend comes from fewer than 20 query templates, usually dashboard refresh jobs or a few analyst notebooks doing SELECT * against raw event tables. Those are your targets. I tag every query I optimize with a comment header (-- optimized: 2026-06-24, JR) so I can prove the before/after when it shows up in the next month's bill review. If you want a broader take on auditing runaway AWS spend, my AWS bill spike triage playbook covers the cross-service version of this exercise.

Partition data by your query pattern

Partitioning is the single highest-impact change you can make. Athena only reads the partition folders that match your WHERE clause, so a table partitioned by event_date turns a "scan the whole table" query into a "scan one day" query. The rule is simple: partition on the columns your filters use most often, almost always a date or hour column, and usually one tenant/region column. Don't over-partition, because every partition is at minimum one S3 object, and Glue caps you at one million partitions per table without using partition projection.

-- Bad: flat layout, every query scans everything
s3://my-bucket/events/event-2026-06-24-001.json.gz

-- Good: Hive-style partition keys in the path
s3://my-bucket/events/year=2026/month=06/day=24/events-001.parquet

-- DDL for the partitioned external table
CREATE EXTERNAL TABLE events (
  user_id STRING,
  event_type STRING,
  payload STRUCT<page: STRING, referrer: STRING>,
  ingested_at TIMESTAMP
)
PARTITIONED BY (year INT, month INT, day INT)
STORED AS PARQUET
LOCATION 's3://my-bucket/events/'
TBLPROPERTIES ('parquet.compress'='SNAPPY');

Pick partition granularity that matches your query cadence. Daily partitions are right for most analytics. Hourly partitions only make sense if your dashboards filter by hour AND you have enough volume that hourly chunks are still tens of megabytes each. Tiny partitions (under 128 MB) hurt because Athena's planner cost grows with partition count and S3 LIST calls add up. The official Athena performance tuning guide recommends targeting 128 MB to 1 GB per partition file.

Convert raw text to compressed Parquet

If your data is sitting in CSV, JSON, or even gzipped JSON, you're paying for every byte of every column you don't need. Parquet stores data column-by-column with per-column compression and row-group statistics. When you run SELECT user_id, event_type FROM events WHERE event_date = '2026-06-23', Athena reads two columns out of however many you have, skips row groups that don't match the predicate, and decompresses only what it returns. In my last migration, switching from gzipped JSON to Snappy Parquet cut average scan-per-query from 14 GB to 380 MB, a 97% reduction.

You don't need a separate Spark cluster. Athena's CREATE TABLE AS SELECT (CTAS) handles the conversion in-engine. Here's the pattern I use for one-shot backfills:

-- One-shot conversion: raw JSON to partitioned Parquet
CREATE TABLE events_parquet
WITH (
  format = 'PARQUET',
  parquet_compression = 'SNAPPY',
  partitioned_by = ARRAY['year', 'month', 'day'],
  external_location = 's3://my-bucket/events-parquet/'
) AS
SELECT
  user_id,
  event_type,
  payload,
  ingested_at,
  year(ingested_at) AS year,
  month(ingested_at) AS month,
  day(ingested_at) AS day
FROM events_raw
WHERE ingested_at >= TIMESTAMP '2026-01-01';

For ongoing ingest, switch your producers (Firehose, Kinesis Data Streams + Lambda, or Glue jobs) to write Parquet directly. Firehose has built-in JSON-to-Parquet conversion that runs at $0.018/GB processed, usually cheaper than scanning the raw data once. Snappy is the right default compression: faster decode than gzip, smaller files than uncompressed, and Athena/Spark/Presto all handle it natively. Use Zstd only for cold archive partitions where read frequency is low. Its better compression ratio is wasted if you're paying for CPU to decompress on every query.

Skip Glue partition management with partition projection

Once you have date partitions, the next operational headache is keeping the Glue Data Catalog in sync. Every new day means running MSCK REPAIR TABLE or ALTER TABLE ADD PARTITION, and on high-cardinality tables you hit the 1 million partition cap surprisingly fast. Partition projection lets Athena compute partition values from the WHERE clause without consulting Glue at all. No catalog writes, no nightly REPAIR jobs, and the partition count is effectively unbounded.

-- Enable partition projection for a date-partitioned table
ALTER TABLE events SET TBLPROPERTIES (
  'projection.enabled' = 'true',
  'projection.year.type' = 'integer',
  'projection.year.range' = '2024,2030',
  'projection.month.type' = 'integer',
  'projection.month.range' = '1,12',
  'projection.month.digits' = '2',
  'projection.day.type' = 'integer',
  'projection.day.range' = '1,31',
  'projection.day.digits' = '2',
  'storage.location.template' =
    's3://my-bucket/events/year=${year}/month=${month}/day=${day}/'
);

After that, queries like WHERE year=2026 AND month=6 AND day=23 resolve the partition directly from the template, and Athena never calls Glue. This shaves 1–3 seconds off planning time for every query and removes Glue from your hot path entirely. The tradeoff: projection only works when your S3 layout follows a strictly predictable template, so it doesn't suit tables where partitions are sparse or randomly named.

Use Iceberg tables for hidden partitioning and MERGE

Apache Iceberg is now a first-class table format in Athena (engine v3), and it solves several Hive-table pain points at once: hidden partitioning (queries don't need to know the partition column), partition evolution (you can switch from daily to hourly without rewriting old data), row-level UPDATE/DELETE/MERGE, and per-column statistics that prune even more aggressively than Parquet row groups alone. For mutable tables (CDC pipelines, GDPR deletion, slowly-changing dimensions), Iceberg is the only sensible choice in 2026.

-- Create an Iceberg table managed by Glue
CREATE TABLE events_iceberg (
  user_id STRING,
  event_type STRING,
  event_time TIMESTAMP,
  payload STRING
)
PARTITIONED BY (day(event_time))   -- hidden partitioning
LOCATION 's3://my-bucket/events-iceberg/'
TBLPROPERTIES (
  'table_type' = 'ICEBERG',
  'format' = 'PARQUET',
  'write_compression' = 'SNAPPY'
);

-- GDPR deletion: previously impossible without rewriting the whole partition
DELETE FROM events_iceberg WHERE user_id = 'user-12345';

-- CDC merge from a staging table
MERGE INTO events_iceberg t USING staging_events s
  ON t.user_id = s.user_id AND t.event_time = s.event_time
  WHEN MATCHED THEN UPDATE SET payload = s.payload
  WHEN NOT MATCHED THEN INSERT VALUES (s.user_id, s.event_type, s.event_time, s.payload);

The cost angle: Iceberg's metadata layer tracks min/max values per file per column, so a query filtering WHERE user_id = 'X' can skip entire data files before reading them, something Hive tables on Parquet can't do. In my own pipeline, switching from Hive Parquet to Iceberg cut scan-per-query another 30–60% on filtered lookups. Just budget for the Glue compaction job; un-compacted Iceberg tables accumulate tiny delete files that erode the savings. See the Iceberg documentation for the full table-maintenance reference.

When Athena Capacity Reservations beat per-query pricing

Once you've done the work above and your per-query bill is still high (usually because of query concurrency rather than scan volume), it's worth pricing out Capacity Reservations. The math: a 24-DPU reservation runs $0.30 × 24 × 730 hours = $5,256 per month, which equals 1,051 TB at the per-query rate. But you're not buying scan capacity, you're buying concurrency: 24 DPUs comfortably handle ~12 concurrent typical queries, and reserved capacity has no scan-volume cap.

DimensionPer-query ($5/TB)Capacity Reservations
Pricing modelPay per TB scanned$0.30/DPU-hour, 24 DPU minimum
Minimum spend$0 (no commitment)$5,256/mo (24 DPU × 730 hr)
ConcurrencySoft quota (~20 active)Hard, predictable per DPU
Cost predictabilityVariable, query-dependentFixed monthly
PerformanceBest-effort, queued at peakGuaranteed, no queuing
Best fitSporadic analyst queriesBI dashboards, scheduled ETL
Breakevenn/a~1 PB scanned/month

My rule of thumb: if a single workgroup is scanning more than 400 TB per month AND has concurrency complaints from BI tools, reserve. If you've got lots of small ad-hoc queries from analysts, stay per-query. You can mix the two (put dashboards on a reserved workgroup and ad-hoc on per-query), which is what I recommend for most accounts above $20K/month in Athena spend. For broader commitment-based discount strategy across services, my guide to AWS Compute Savings Plans coverage covers the same coverage/utilization tradeoff you should apply here.

Workgroup guardrails that stop runaway bills

Even with all the above, an analyst can still write SELECT * FROM 100tb_table and burn $500 in 30 seconds. Workgroup-level controls are your safety net. Every Athena workgroup supports a per-query data-scan limit (queries that would exceed it are cancelled before execution) and a per-workgroup data-scan limit (a daily/weekly/monthly cap that suspends the workgroup when hit). Configure both via Terraform:

resource "aws_athena_workgroup" "analytics" {
  name = "analytics"

  configuration {
    enforce_workgroup_configuration    = true
    publish_cloudwatch_metrics_enabled = true
    bytes_scanned_cutoff_per_query     = 10737418240  # 10 GB per query

    result_configuration {
      output_location = "s3://my-athena-results/analytics/"
      encryption_configuration {
        encryption_option = "SSE_S3"
      }
    }
  }
}

# Daily data-scan alarm (workgroup-wide)
resource "aws_cloudwatch_metric_alarm" "athena_scan_daily" {
  alarm_name          = "athena-analytics-daily-scan-1tb"
  metric_name         = "ProcessedBytes"
  namespace           = "AWS/Athena"
  statistic           = "Sum"
  period              = 86400
  evaluation_periods  = 1
  threshold           = 1099511627776  # 1 TB
  comparison_operator = "GreaterThanThreshold"
  dimensions          = { WorkGroup = aws_athena_workgroup.analytics.name }
  alarm_actions       = [aws_sns_topic.finops_alerts.arn]
}

I set per-query limits aggressively: 10 GB for ad-hoc workgroups, 100 GB for ETL workgroups, with a documented override process. The cancelled-query message is annoying for analysts the first time, but it's a forcing function. They learn to add a partition predicate. Pair these with the anomaly detection patterns I covered for cross-cloud spend, and you've got both per-query and per-day guardrails in place. For a broader view of database-layer cost optimization across services, see my cloud database cost optimization guide.

Frequently Asked Questions

How is AWS Athena priced in 2026?

Athena per-query pricing is $5.00 per TB of data scanned in most US regions, billed per query, rounded up to the nearest MB with a 10 MB minimum. DDL statements and failed queries are free. The alternative, Capacity Reservations, runs $0.30 per DPU-hour with a 24-DPU minimum, costing about $5,256/month for a reserved workgroup.

Is Parquet always cheaper than CSV in Athena?

Almost always, yes. Parquet stores data by column with per-column compression and row-group statistics, so Athena reads only the columns and chunks your WHERE clause needs. For typical analytics queries, switching from CSV or JSON to Snappy-compressed Parquet cuts scan volume 80–95%. The only exception is full-row exports of every column, where the savings shrink to whatever compression gives you (~3–5x).

What's the difference between partition projection and partition discovery?

Partition discovery requires you to register each partition in the Glue Data Catalog with ALTER TABLE ADD PARTITION or MSCK REPAIR TABLE, and is capped at 1 million partitions per table. Partition projection computes partition values from a template at query time without consulting Glue, so there's no catalog write, no REPAIR job, and no partition cap. Use projection for any table where partitions follow a predictable date or numeric pattern.

Should I use Iceberg or Hive tables on Athena?

Use Iceberg for any mutable table (CDC pipelines, GDPR-deletable user data, slowly-changing dimensions) because Hive tables can't do row-level updates or deletes without rewriting whole partitions. Iceberg also prunes data files using per-column metadata, often cutting scan-per-query another 30–60% on filtered lookups. For append-only logs that never change, plain Hive tables on Parquet are simpler and have lower metadata overhead.

When do Athena Capacity Reservations save money?

Reservations break even around 1 PB scanned per month on a 24-DPU minimum, but the real reason to switch is concurrency: BI dashboards and scheduled ETL benefit from guaranteed slots and no per-query scan billing. Stay on per-query pricing if your workload is sporadic analyst queries totaling less than ~400 TB/month. Many large accounts run a hybrid: reserved for dashboards, per-query for ad-hoc.

Does SELECT COUNT(*) scan the whole table in Athena?

On Parquet and Iceberg tables, no, because Athena reads row counts from file or table metadata and the query usually scans under 10 MB (the per-query minimum). On CSV/JSON tables it scans the whole table because text formats have no metadata. Same for SELECT MAX(col) and similar aggregates: free on columnar formats with min/max statistics, full scan on text.

Jordan Reeves
About the Author Jordan Reeves

FinOps practitioner who's cut seven-figure cloud bills more than once. Believes most cost overruns are an architecture problem in disguise.