Lift the lakehouse: Snowflake or BigQuery on top of your Iceberg tables — or stay local with DuckDB.
Data Engineering Module 4 Modern LakehouseDuckDB queries Parquet. L10 adds Iceberg: schemas, snapshots, ACID. The local laptop is now a real lakehouse.
Same lakehouse, scale-up engine. Snowflake or BigQuery reads the same Iceberg metadata. No copy. No load. New compute, same data.
Both Snowflake (CREATE ICEBERG TABLE ... CATALOG = ... BASE_LOCATION = ...) and BigQuery (CREATE EXTERNAL TABLE ... OPTIONS (format='ICEBERG', uris=[...])) can register an existing Iceberg table without copying data.
You have a free-tier Snowflake or BigQuery account. The lab walks through registering the Iceberg table, creating daily_counts, and running the materialize-vs-query benchmark on warehouse hardware.
You don’t. The lab substitutes warehouse/duckdb_local.py everywhere — same materialize_daily_counts() surface, same benchmark, just running locally. Concepts are identical; the credit-card billing is not.
| Pricing axis | Snowflake | BigQuery | DuckDB local |
|---|---|---|---|
| Compute | Credits/hour for warehouse size | Bytes scanned per query (on-demand) or slot-hours (reserved) | None (your CPU) |
| Storage | External = your S3 cost only | External = your GCS cost only | Local disk |
| Egress | Bucket egress to compute region | Bucket egress to compute region | N/A |
External Iceberg tables move you from “paying for warehouse storage” to “paying only for your own bucket” — often a 10–50× storage cost reduction.
Every query reads the Iceberg metadata + scans Parquet through the warehouse engine. Storage costs are minimal; query costs scale with data scanned. Best for ad-hoc exploration.
Pre-compute the result into warehouse-managed storage. Query costs collapse. Storage costs rise. Freshness lag opens. Best for aggregates queried many times.
Rule of thumb: materialise aggregates queried many times; query Iceberg directly for ad-hoc exploration and one-shot dashboards.
CREATE EXTERNAL VOLUME iceberg_vol
STORAGE_LOCATIONS = (
(NAME = 'my_s3', STORAGE_PROVIDER = 'S3',
STORAGE_BASE_URL = 's3://bucket/warehouse/',
STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::...'));
CREATE CATALOG INTEGRATION iceberg_cat
CATALOG_SOURCE = OBJECT_STORE
TABLE_FORMAT = ICEBERG
ENABLED = TRUE;
CREATE ICEBERG TABLE events
EXTERNAL_VOLUME = 'iceberg_vol'
CATALOG = 'iceberg_cat'
BASE_LOCATION = 'warehouse/events';
Three DDL statements. From this point the table behaves like any Snowflake table for SELECT.
CREATE OR REPLACE EXTERNAL TABLE my_dataset.events
WITH CONNECTION my_conn
OPTIONS (
format = 'ICEBERG',
uris = ['gs://bucket/warehouse/events/metadata/v*.metadata.json']
);
One DDL. BigQuery reads the most-recent v<N>.metadata.json matching the glob, then the snapshots it references.
Then the materialised aggregate is the same SQL as Snowflake:
CREATE OR REPLACE VIEW daily_counts AS
SELECT event_date, type, COUNT(*) AS n
FROM my_dataset.events
GROUP BY event_date, type;
from warehouse.duckdb_local import materialize_daily_counts
materialize_daily_counts()
# duckdb_local: daily_counts materialised, N rows.
import duckdb
con = duckdb.connect("warehouse/duckdb_local.duckdb")
con.execute("SELECT * FROM daily_counts LIMIT 5").fetchall()
Same daily_counts shape as Snowflake / BigQuery. Same materialize_vs_query.py benchmark. No cloud account.
SELECT * (no projection, no partition filter) scans every Parquet file. On 1 TB that’s one query that exhausts a significant chunk of your monthly budget.