Most Redshift warehouses we are asked to review in 2026 are no longer self-contained. Some of the data lives in Redshift-managed storage, and a growing share lives in Apache Iceberg tables on S3 that are written by Spark, Flink, EMR, Athena, or a third-party tool. The question we get is always the same: can Redshift read that Iceberg data directly, and is it fast enough to join against the warehouse tables?
The answer is yes, through an external schema backed by the AWS Glue Data Catalog, with Lake Formation handling permissions. This tutorial walks the whole path end to end, including the newer Amazon S3 Tables case, where S3 itself manages the Iceberg metadata and compaction for you.
What you need before you start
- An RA3 provisioned cluster or a Redshift Serverless workgroup. Iceberg reads are not available on DC2.
- Iceberg tables registered in the Glue Data Catalog — either a general-purpose S3 bucket with an Iceberg warehouse path, or an S3 Tables (table bucket) namespace.
- Permission to create an IAM role and to make Lake Formation grants. This is the step that stalls most projects, so get the security owner involved on day one rather than day five.
Two terms worth separating, because they are often conflated:
- Redshift Spectrum external tables — the classic pattern. You point an external schema at a Glue database and query Parquet, ORC, JSON, CSV, and Iceberg objects on S3.
- S3 Tables — an S3 bucket type that stores Iceberg tables natively and performs its own compaction and snapshot maintenance. You still query it from Redshift through an external schema; the difference is that nobody on your team owns the small-files problem.
Step 1: create the IAM role Redshift will assume
Redshift needs a role it can assume to reach Glue, Lake Formation, and S3. Trust policy:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "redshift.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}
Permissions policy, kept deliberately narrow:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"glue:GetDatabase", "glue:GetDatabases",
"glue:GetTable", "glue:GetTables",
"glue:GetPartition", "glue:GetPartitions"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["lakeformation:GetDataAccess"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::analytics-lake-prod",
"arn:aws:s3:::analytics-lake-prod/*"
]
}
]
}
For S3 Tables buckets, add the s3tables:GetTableData, s3tables:GetTableMetadataLocation, s3tables:GetNamespace, and s3tables:ListTables actions scoped to the table bucket ARN instead of the general-purpose bucket statement. Attach the role to the cluster or workgroup, then note its ARN.
Step 2: register the S3 Tables bucket with the catalog
Skip this step if your Iceberg tables are already Glue-registered. For a new S3 Tables bucket, create the bucket and namespace, then enable catalog integration once per account and Region:
aws s3tables create-table-bucket --name analytics-tables --region us-east-1
aws s3tables create-namespace \
--table-bucket-arn arn:aws:s3tables:us-east-1:111111111111:bucket/analytics-tables \
--namespace events
After integration, the table bucket appears in Glue as a federated catalog whose identifier looks like 111111111111:s3tablescatalog/analytics-tables. That identifier is what you reference from Redshift, so record it exactly.
Step 3: make the Lake Formation grants
With Lake Formation in the path, IAM alone is not enough. Grant the Redshift role explicit table access:
aws lakeformation grant-permissions \
--principal DataLakePrincipalIdentifier=arn:aws:iam::111111111111:role/RedshiftLakehouseRole \
--resource '{"Table":{"CatalogId":"111111111111:s3tablescatalog/analytics-tables","DatabaseName":"events","TableWildcard":{}}}' \
--permissions SELECT DESCRIBE
Also grant DESCRIBE on the database itself, or the external schema will resolve to zero tables and give you a confusing empty result rather than an error.
Step 4: create the external schema in Redshift
For Iceberg tables in a standard Glue database:
CREATE EXTERNAL SCHEMA lake_events
FROM DATA CATALOG
DATABASE 'events'
IAM_ROLE 'arn:aws:iam::111111111111:role/RedshiftLakehouseRole'
CATALOG_ID '111111111111';
For an S3 Tables namespace, the only change is the catalog identifier:
CREATE EXTERNAL SCHEMA s3t_events
FROM DATA CATALOG
DATABASE 'events'
IAM_ROLE 'arn:aws:iam::111111111111:role/RedshiftLakehouseRole'
CATALOG_ID '111111111111:s3tablescatalog/analytics-tables';
Confirm what Redshift can see:
SELECT schemaname, tablename, location, input_format
FROM SVV_EXTERNAL_TABLES
WHERE schemaname IN ('lake_events', 's3t_events');
SELECT columnname, external_type
FROM SVV_EXTERNAL_COLUMNS
WHERE schemaname = 's3t_events' AND tablename = 'page_views'
ORDER BY columnnum;
If SVV_EXTERNAL_TABLES is empty, the problem is almost always the Lake Formation DESCRIBE grant or a mistyped CATALOG_ID, not Redshift.
Step 5: query, and join against warehouse tables
External Iceberg tables behave like any other relation in SQL:
SELECT event_date, COUNT(*) AS views
FROM s3t_events.page_views
WHERE event_date >= DATEADD(day, -7, CURRENT_DATE)
GROUP BY 1
ORDER BY 1;
The interesting query is the hybrid one — lake-scale event data joined to curated dimensions in Redshift storage:
SELECT c.segment,
COUNT(DISTINCT v.session_id) AS sessions,
SUM(o.order_total) AS revenue
FROM s3t_events.page_views v
JOIN analytics.customers c ON c.customer_id = v.customer_id
LEFT JOIN analytics.orders o ON o.session_id = v.session_id
WHERE v.event_date >= DATE '2026-01-01'
GROUP BY 1
ORDER BY revenue DESC;
Redshift pushes projections and predicates down to the Iceberg scan and uses Iceberg partition and file statistics to prune. Everything above the scan — joins, aggregation, window functions — happens in the Redshift compute layer.
Step 6: tune it before anyone calls it slow
Four things account for nearly every disappointing Iceberg query we are asked to look at.
1. Always filter the partition column. An Iceberg table partitioned by event_date prunes only if event_date appears in the WHERE clause. Filtering event_timestamp instead forces a full scan. Check the damage:
SELECT query, external_table_name,
s3_scanned_rows, s3_scanned_bytes,
avg_request_parallelism
FROM SVL_S3QUERY_SUMMARY
WHERE query = pg_last_query_id();
When s3_scanned_bytes is orders of magnitude larger than what the answer needs, pruning failed.
2. Fix small files. Thousands of few-hundred-KB Parquet files kill scan throughput. S3 Tables compacts automatically; a self-managed Iceberg warehouse does not, so schedule rewrite_data_files on the writer side. Target files in the 128 MB–512 MB range.
3. Materialize the hot slice. If a lake table is joined in dozens of dashboard queries, stop scanning S3 for each one:
CREATE MATERIALIZED VIEW analytics.mv_page_views_30d AS
SELECT event_date, customer_id, session_id, page_path
FROM s3t_events.page_views
WHERE event_date >= DATEADD(day, -30, CURRENT_DATE);
External-table materialized views require a full refresh rather than an incremental one, so refresh on a schedule that matches how often the lake table lands.
4. Watch the cost meter. On provisioned clusters, external scans are billed per terabyte scanned through Spectrum. On Serverless they consume RPU-seconds. Either way, an unpruned scan is a line item, not just a slow query. Track it:
SELECT TRUNC(starttime) AS day,
SUM(s3_scanned_bytes) / POWER(1024, 4) AS tb_scanned
FROM SVL_S3QUERY_SUMMARY
WHERE starttime >= DATEADD(day, -30, GETDATE())
GROUP BY 1
ORDER BY 1;
When Iceberg is the wrong answer
External Iceberg tables are excellent for high-volume, append-heavy, infrequently queried history — clickstream, IoT telemetry, raw logs, regulatory archives — and for data that other engines must also read. They are a poor fit for small dimensions joined in every query, for sub-second BI, and for anything needing row-level updates from Redshift, since external tables are read-only.
The architecture that holds up in production is boring on purpose: curated dimensions and recent facts in Redshift-managed storage, long-tail history in Iceberg, one external schema bridging them, and a materialized view wherever a lake table becomes hot.
Getting help
If you are standing up a Redshift lakehouse — deciding what stays in managed storage, migrating a Spectrum estate to Iceberg or S3 Tables, or untangling Lake Formation grants that nobody wants to own — our senior Redshift consultants do this work daily, on direct engagements and as a subcontracting partner. Get in touch and describe your current layout; we will tell you plainly whether Iceberg helps.