Guide 09. By Nicolas Dao

Athena or DuckDB?

How should I query my data lake, and what changed when AWS bought DuckDB's company?

Use both. Athena for big scans and ad-hoc questions with zero code. DuckDB for app queries, writing Parquet and local development, on the same files. Sorting files, or Iceberg, fixes the second-index problem without copying data.

The news

AWS bought DuckLabs, the company behind DuckDB. It was announced on 26 August 2026 and closed on 31 August. DuckDB stays MIT-licensed, the non-profit DuckDB Foundation keeps the IP, and MotherDuck (managed DuckDB) is still independent.

So far one integration has shipped: since 30 September 2026, Aurora PostgreSQL can query Parquet and Iceberg in S3 with DuckDB inside (CREATE EXTENSION aurora_analytics). AWS says Athena, Glue, EMR and the rest will get DuckDB "over time", with no dates.

Same lake, three engines

Firehose / Lambda ──▶ S3: Parquet (or Iceberg), catalogued in Glue
                         │
       ┌─────────────────┼──────────────────────┐
       ▼                 ▼                      ▼
    Athena            DuckDB                Aurora + aurora_analytics
 big scans, ad hoc   app queries, ETL,      live tables JOIN lake history,
 reports, BI         local dev and tests    in one SQL query (read-only)
Athena DuckDB in Lambda
Best at Scans of tens of GB to TBs, many users, zero code Many small, selective queries; writing Parquet; local dev
Cost to read 1 GB $0.005 (10 MB minimum per query) about $0.002
Cost to read 1 TB $5, in minutes about $2.20, but only if you build the fan-out across many functions
Latency Seconds, start then poll for results Sub-second when warm
Local dev None: every test query hits AWS Same SQL on a laptop against the same S3 data
Writing Parquet CTAS, at most 100 partitions per query COPY: you choose sort order, file size, partitions
Concurrency 100 active queries in Sydney (adjustable), queued Each Lambda is its own engine

Nobody should switch for cost alone: the difference is at most about 2x. The case for DuckDB is speed for small queries and developer experience.

What each looks like in code

Athena: start, poll, fetch.

const { QueryExecutionId } = await athena.send(new StartQueryExecutionCommand({
	QueryString: `SELECT dt, count(*) AS orders FROM lake.orders WHERE customer_id = '42' GROUP BY dt`,
	WorkGroup: 'primary',
}))
// poll GetQueryExecution until SUCCEEDED, then page through GetQueryResults

DuckDB in a Lambda: query and return.

import { DuckDBInstance } from '@duckdb/node-api'

const db = await DuckDBInstance.create(':memory:') // module scope: reused while warm
const conn = await db.connect()
await conn.run(`SET home_directory = '/tmp'`)                // Lambda has no HOME
await conn.run(`SET extension_directory = '/opt/duckdb_ext'`) // bundled in the layer
await conn.run(`CREATE SECRET (TYPE s3, PROVIDER credential_chain, REGION 'ap-southeast-2')`)

export const handler = async ({ customerId }) => {
	const reader = await conn.runAndReadAll(`
		SELECT dt, count(*) AS orders, sum(total) AS revenue
		FROM read_parquet('s3://lake/orders/*/*.parquet', hive_partitioning = true)
		WHERE customer_id = $1 AND dt >= '2026-01-01'
		GROUP BY dt ORDER BY dt`, [customerId])
	return reader.getRowObjectsJson()
}

The same query runs on a laptop with an SSO profile, and in CI against a few sample Parquet files with no AWS at all.

Writing Parquet with DuckDB, sorted so that both keys prune:

COPY (
	SELECT * FROM read_json('s3://lake/raw/orders/2026/10/19/*/*.gz')
	ORDER BY customer_id
) TO 's3://lake/orders' (FORMAT parquet, PARTITION_BY (dt), COMPRESSION zstd);

The second-index problem

Data partitioned by date, queried by customer, scans everything. Without copying the data:

  1. Sort each file by the second key (the COPY above). Parquet keeps min/max values per chunk; Athena and DuckDB skip chunks that can't match, and Athena bills only the bytes it reads. The limit: every file's footer is still opened, fine for thousands of files, slow for millions.
  2. Iceberg tables in S3 Tables: partition by day(ts) and bucket(16, customer_id). A query by date reads one day; a query by customer reads one-sixteenth of the files. Per-file statistics on every column, and AWS compacts and sorts (or z-orders) the files for about $0.01/GB in Sydney. Athena and DuckDB both read them.

Partition projection and Glue partition indexes only speed up planning. They aren't indexes.

Iceberg, briefly

Iceberg is a table format on top of Parquet: a metadata layer that adds partitions you can change later, schema changes, updates and deletes, time travel and per-file statistics. S3 Tables is AWS's managed version. It's where this lake should go next; I'll dig into it in a later guide.

Aurora with DuckDB inside

If the app already runs on Aurora PostgreSQL (17.11+ or 18.6+), aurora_analytics exposes lake tables as foreign tables, so one SQL query can join this month's live orders with five years of history in S3. It's read-only, costs nothing beyond Aurora compute and S3 requests, and is the first sign of where AWS takes DuckDB. On Neon, use DuckDB in a Lambda instead.

MotherDuck

Managed DuckDB, with a Sydney region, from $250 a month plus usage. Worth it for a shared warehouse that analysts or agents query all day. Its AI functions run in the US.

Want this stack in your AWS account?

Cloudless Labs is a Sydney consultancy. We migrate on-premises systems to AWS, build serverless apps on this stack, and coach teams to ship with Claude Code and Codex.

Contact Us

For general inquiries, please contact us at:

For e-learning capabilities, please contact: