Big Data & Analytics

Cataloging, querying, and warehousing data at scale with Glue, Athena, Redshift, EMR, and QuickSight

Introduction

Lesson 17 covered how data gets onto AWS in real time: Kinesis Data Streams for ingestion, Firehose for near-real-time archival to S3. This lesson picks up where that data lands. Once events, logs, or exports sit in S3, how do you describe their shape, query them without standing up a database, warehouse them for recurring BI workloads, and hand the results to people who don't write SQL?

That is the job of AWS's analytics platform: the Glue Data Catalog and Crawlers describe your data once, Athena answers ad-hoc questions directly against S3, Redshift powers recurring dashboards at warehouse scale, EMR steps in when you need full Spark or Hadoop control, and QuickSight turns query results into shareable dashboards.

1. The AWS Analytics & Data Lake Landscape

S3 (Lesson 5) is the foundation: cheap, durable, and infinitely scalable storage for raw and curated files. The "data lake" pattern builds five more services on top of it, each solving one part of the journey from raw files to business answers:

AWS Glue

Describes your data once (Data Catalog), discovers its shape automatically (Crawlers), and transforms it with serverless Spark (ETL Jobs).

Amazon Athena

Serverless SQL directly against S3 files. No cluster, no loading step, pay only for the data each query scans.

Amazon Redshift

A managed columnar data warehouse for recurring, high-concurrency analytical workloads, with Spectrum reaching back into the lake.

Amazon EMR & QuickSight

EMR runs managed Hadoop/Spark clusters for heavy custom processing; QuickSight turns Athena and Redshift results into shareable dashboards.

Serverless Data Lake Pipeline

S3Raw DataS3CRWGlue CrawlerCATData CatalogGUEGlue ETL JobSparkS3Curated DataParquetATHAthenaRSRedshiftSpectrumQSQuickSightscan & inferpopulateschemametastoretransformquerySpectrum querydashboardsdashboards

Figure 1: Raw S3 data is cataloged once and then queried by Athena, transformed by Glue, warehoused by Redshift, and visualized by QuickSight, all sharing one schema.

2. AWS Glue: Data Catalog, Crawlers & ETL Jobs

Glue is three closely related capabilities under one name: a place to store table definitions, a way to generate those definitions automatically, and a serverless engine to transform the data those definitions describe.

Data Catalog

A centralized, Hive-metastore-compatible metadata repository. You define a table's schema, location, and partitions once, and Athena, Redshift Spectrum, and EMR all read from it. No service owns its own copy of the schema.

Crawlers

Scheduled jobs that scan S3 prefixes (or JDBC sources), infer column types and partition structure, and create or update Catalog tables automatically. Point a crawler at a prefix and it keeps the schema current as new files land.

ETL Jobs

Serverless Apache Spark (or lightweight Python-shell) jobs, billed per DPU-hour. Glue Studio offers a visual, drag-and-drop editor, and job bookmarks let a job pick up only the new files since its last run.

Pros
  • No servers or clusters to provision or patch
  • Pay only for crawler runtime and ETL job DPU-hours
  • One schema definition shared across the whole platform
Cons
  • Spark job cold starts add latency to short transforms
  • Debugging distributed Spark failures can be opaque
  • Crawlers can misclassify columns on messy or mixed-format data
When to reach for Glue

You need a shared schema registry that other query engines can read, and/or scheduled, low-code transforms that turn raw files into clean, partitioned, columnar data ready for analysis.

CDK: define a Glue database to hold table metadata, then attach a crawler that scans the curated S3 prefix on an hourly schedule and keeps the table definition (including partitions) up to date as new Parquet files arrive.

glue_database = glue.CfnDatabase(self, "AnalyticsDatabase",
    catalog_id=self.account,
    database_input=glue.CfnDatabase.DatabaseInputProperty(name="analytics_db"),
)

crawler = glue.CfnCrawler(self, "CuratedDataCrawler",
    role=crawler_role.role_arn,
    database_name=glue_database.ref,
    targets=glue.CfnCrawler.TargetsProperty(
        s3_targets=[glue.CfnCrawler.S3TargetProperty(
            path=f"s3://{curated_bucket.bucket_name}/curated/orders/",
        )],
    ),
    schedule=glue.CfnCrawler.ScheduleProperty(schedule_expression="cron(0 * * * ? *)"),
)

3. Amazon Athena: Serverless SQL on S3

Athena is a serverless query engine, built on Trino, the open-source engine that grew out of the original Presto project, that runs standard SQL directly against files sitting in S3. There is no cluster to launch and no loading step: point Athena at a Glue Catalog table and start querying within seconds.

Pricing & Performance
  • Billed at roughly $5 per TB scanned, regardless of how complex the query is
  • Partitioning lets Athena skip whole prefixes that can't match the query
  • Columnar formats (Parquet, ORC) let it read only the columns a query touches

Together, partitioning and columnar storage are the two biggest levers for cutting both Athena's cost and its latency.

Catalog & Workgroups
  • Athena reads table definitions straight from the Glue Data Catalog, the same metastore Section 2 populated
  • Workgroups isolate teams, set per-query data scan limits, and define where results are written
Pros
  • Zero infrastructure, query within seconds of table creation
  • Pay only for what each query scans, nothing when idle
  • Standard SQL, works with existing BI tools via JDBC/ODBC
Cons
  • Cost scales with bytes scanned, not query complexity, an unindexed full-table scan can be expensive
  • Query latency (seconds) is not built for high-concurrency, sub-second BI dashboards
When to reach for Athena

Occasional or exploratory SQL over data that already lives in S3: log analysis, one-off reports, validating a hypothesis before building a pipeline around it.

SQL: register a partitioned Parquet table in the Glue Catalog, then run a query whose WHERE clause matches the partition columns, so Athena prunes every file outside that single day before it scans a single byte.

CREATE EXTERNAL TABLE analytics_db.orders (
    order_id      string,
    customer_id   string,
    total_amount  double,
    status        string
)
PARTITIONED BY (year string, month string, day string)
STORED AS PARQUET
LOCATION 's3://my-curated-bucket/curated/orders/';

-- Partition pruning: Athena scans only the matching day's Parquet files,
-- not the entire table, which is what keeps the per-TB-scanned bill low
SELECT customer_id, SUM(total_amount) AS daily_total
FROM analytics_db.orders
WHERE year = '2026' AND month = '06' AND day = '05'
GROUP BY customer_id
ORDER BY daily_total DESC
LIMIT 10;

4. Amazon Redshift: Data Warehousing at Scale

Where Athena is built for occasional queries against files, Redshift is built for recurring, complex analytical workloads against structured data, the kind a BI team runs every morning, with dozens of analysts querying concurrently.

Architecture & Modes
  • Columnar storage + MPP: data is stored by column and queries run in parallel across nodes, ideal for large aggregations and joins
  • Provisioned clusters: you choose node type and count, predictable cost, you manage scaling
  • Redshift Serverless: pay per RPU-second, scales down automatically, no cluster to size up front
Redshift Spectrum

Spectrum lets a Redshift cluster query data sitting in S3 directly, through the same Glue Data Catalog that Athena uses, without loading it into the warehouse first. It is the bridge between the warehouse and the data lake: join "hot" loaded tables with "cold" lake data in one SQL statement.

Pros
  • Predictable, fast performance for repeated, complex queries
  • Handles many concurrent analysts without degrading
  • Rich SQL and tight integration with BI tools and QuickSight
Cons
  • Requires schema design, distribution keys, and a loading pipeline
  • Provisioned clusters cost money even while idle (Serverless mitigates this)
  • More operational surface area than a fully serverless option
When to reach for Redshift

Recurring dashboards, scheduled reports, and complex joins across curated, structured data, run by many concurrent users who expect consistent performance.

SQL: register the Glue Catalog database as an external schema so Redshift can read S3 data directly, then run a query that joins a native, loaded table with that external table, blending warehouse and data-lake data without an ETL step in between.

-- Registers the same Glue Data Catalog database that Athena queries,
-- so Redshift can "see" the curated S3 tables without copying any data
CREATE EXTERNAL SCHEMA spectrum_orders
FROM DATA CATALOG
DATABASE 'analytics_db'
IAM_ROLE 'arn:aws:iam::123456789012:role/RedshiftSpectrumRole'
REGION 'us-east-1';

-- Federated query: joins a native (loaded) Redshift table with an
-- external (S3-backed) Spectrum table in a single statement
SELECT c.segment, SUM(o.total_amount) AS revenue
FROM customers c
JOIN spectrum_orders.orders o ON o.customer_id = c.customer_id
WHERE o.year = '2026'
GROUP BY c.segment
ORDER BY revenue DESC;

5. Rounding Out the Platform: EMR & QuickSight

Glue, Athena, and Redshift cover cataloging, ad-hoc querying, and warehousing. Two more services complete the picture: one for when serverless processing isn't enough, and one for turning query results into something a business user can actually read.

Amazon EMR

Managed Hadoop, Spark, Hive, and Presto clusters. Reach for EMR over serverless Glue when a job needs full framework control, custom libraries, very large or long-running processing, or you're porting existing Hadoop-ecosystem code. The trade-off is the same one Lesson 17 raised for Managed Apache Flink: you now own cluster sizing, patching, and scaling, capability in exchange for operational overhead.

Amazon QuickSight

A serverless BI and visualization service: the "last mile" that turns Athena and Redshift query results into interactive dashboards for people who don't write SQL. Its SPICE in-memory engine caches query results so repeated views of the same dashboard load fast, and pricing is per session rather than per always-on seat.

A note on governance: AWS Lake Formation

As more services share one Data Catalog, controlling who can see which tables, columns, or rows becomes its own problem. AWS Lake Formation sits on top of the Glue Data Catalog and centralizes those permissions, including column-level and row-level security and cross-account sharing. It's a deep topic in its own right (covered in the dedicated Big Data Fundamentals course); the takeaway here is simply to know it exists once your data lake grows past a handful of trusted consumers.

6. Choosing the Right Tool

FeatureAWS GlueAmazon AthenaAmazon RedshiftAmazon EMR
RoleCatalog + serverless ETLServerless ad-hoc SQL on S3Managed data warehouseManaged Hadoop/Spark clusters
Pricing modelPer DPU-hourPer TB scannedPer node-hour, or per RPU (Serverless)Per instance-hour
Management overheadNone (serverless)None (serverless)Low to moderate, or none w/ ServerlessHigh (cluster sizing/tuning)
Best forSchema registry + scheduled ETLExploratory/occasional SQLRecurring BI, high concurrencyCustom/large-scale Spark & Hadoop jobs
Decision Guide

Use Glue when:

  • You need one shared schema multiple query engines can read
  • Transforms are scheduled, low-code, and serverless-friendly

Use Athena when:

  • You need quick answers over data that's already in S3
  • Setup time matters more than predictable per-query cost

Use Redshift when:

  • Many analysts run recurring, complex analytical queries
  • You need predictable performance at consistent concurrency

Use EMR when:

  • A job needs the full Spark/Hadoop ecosystem or custom code
  • Workloads are very large, bespoke, or long-running

7. Code Example: Data Lake Pipeline Pieces

Bonus: Full Working Project

The complete deployable data lake from this lesson is available as a GitLab repository. Deploy it and watch raw JSON land in S3, get cataloged and curated into partitioned Parquet automatically, then run the comparison query and see the partitioning-and-columnar-formats cost story in real bytes-scanned numbers.

The project includes:

  • DataLakeStack - CDK stack: S3 bucket with raw and curated zones, Glue database with two crawlers (raw_orders, curated_orders), a Python Shell ETL job, and EventBridge rules wired to a trigger Lambda for a fully event-driven pipeline, no schedules, no manual start-crawler/start-job-run
  • curate_orders.py - Python Shell job: reads newly-landed raw NDJSON, cleans and reshapes it, and writes partitioned, compressed Parquet back to the curated zone
  • pipeline_trigger/handler.py - Lambda: starts the matching crawler or ETL job the moment new data lands in its watched S3 prefix, idempotently collapsing bursts of events into a single run
  • generate_orders.py - seeds synthetic NDJSON order events into the raw zone, backfilling several year=/month=/day= partitions
  • compare_queries.py - runs the same Athena aggregation query against raw_orders (JSON) and curated_orders (Parquet) and prints bytes scanned side by side, the "aha" moment that makes partitioning and columnar formats' cost savings visible in real numbers

The diagram below traces the exact components and relations wired up in this lesson's deployable showcase project (above referenced): a single S3 bucket with raw and curated zones, an event-driven Glue Catalog/Crawler/ETL chain, and Athena querying the result. The first two panels below generalize that same shape into reusable pieces you can adapt to your own data: a CDK stack that wires up the catalog and crawler, and the Athena DDL and queries that read from it. The third panel, Redshift Spectrum, stays purely illustrative: the showcase project deliberately doesn't deploy a Redshift cluster (it bills by the hour, the opposite of this lesson's "stay in the cents" goal), but the pattern is worth knowing for when a warehouse needs to reach into the same lake. All three follow AWS best practices and this course's CDK conventions.

glue-athena-data-lake: Components & Relations

S3DataLakeBucketraw/ & curated/ zonesEVBEventBridge RulesObject CreatedλPipelineTriggerFnCRWRawOrdersCrawlerCATData Catalograw_orders, curated_ordersGUEOrdersCuratorJobPython Shell ETLCRWCuratedOrdersCrawlerATHAthenacompare_queries.pyObject CreatedinvokeStartCrawlerStartJobRunStartCrawlerraw_orderswrites Parquetcurated_ordersmetastore

Figure 2: One Object-Created event fans out through PipelineTriggerFn to catalog raw JSON and curate it into Parquet in parallel; the curated Parquet landing re-fires the same EventBridge -> Lambda chain to catalog curated_orders, no schedules or manual start-crawler/start-job-run anywhere in the loop.

These three panels expand the inline snippets above into the project's actual CDK definition and the considerations that shape it: the CDK stack mirrors data_lake_stack.py piece for piece, the Athena panel weighs a hand-declared, projection-backed table against the crawler-inferred one the stack already creates, and the Redshift Spectrum panel stays illustrative, a deliberate cost call-out rather than something the showcase deploys.

import os

import aws_cdk as cdk
from aws_cdk import (
    aws_events as events,
    aws_events_targets as targets,
    aws_glue as glue,
    aws_iam as iam,
    aws_lambda as lambda_,
    aws_s3 as s3,
    aws_s3_assets as s3_assets,
)
from constructs import Construct

GLUE_JOBS_DIR = os.path.join(os.path.dirname(__file__), "..", "glue_jobs")
LAMBDAS_DIR = os.path.join(os.path.dirname(__file__), "..", "lambdas")


class DataLakeStack(cdk.Stack):
    def __init__(self, scope: Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # Single bucket, two zones by prefix: raw/ (JSON, as-landed) and
        # curated/ (Parquet, partitioned). DESTROY + auto_delete_objects means
        # `cdk destroy` leaves nothing behind to clean up by hand.
        bucket = s3.Bucket(
            self, "DataLakeBucket",
            removal_policy=cdk.RemovalPolicy.DESTROY,
            auto_delete_objects=True,
            # Lets EventBridge see this bucket's "Object Created" events.
            event_bridge_enabled=True,
        )

        # Shared metadata: one database, two tables (raw_orders,
        # curated_orders), the same "define it once" idea the lesson teaches.
        database = glue.CfnDatabase(
            self, "AnalyticsDatabase",
            catalog_id=self.account,
            database_input=glue.CfnDatabase.DatabaseInputProperty(
                name="ecommerce_analytics",
                description="Raw and curated order tables for the data lake showcase",
            ),
        )

        # Crawlers: both assume the same minimal role, scoped to read the
        # bucket and manage tables in this database.
        crawler_role = iam.Role(self, "CrawlerRole",
            assumed_by=iam.ServicePrincipal("glue.amazonaws.com"),
            managed_policies=[
                iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSGlueServiceRole"),
            ],
        )

        bucket.grant_read(crawler_role)

        raw_crawler = glue.CfnCrawler(self, "RawOrdersCrawler",
            name="raw-orders-crawler",
            role=crawler_role.role_arn,
            database_name=database.ref,
            targets=glue.CfnCrawler.TargetsProperty(
                s3_targets=[glue.CfnCrawler.S3TargetProperty(
                    path=f"s3://{bucket.bucket_name}/raw/orders/",
                )],
            ),
            # Crawler names the table after the last path segment ("orders"),
            # so table_prefix="raw_" yields "raw_orders"
            table_prefix="raw_",
            schema_change_policy=glue.CfnCrawler.SchemaChangePolicyProperty(
                update_behavior="UPDATE_IN_DATABASE",
                delete_behavior="LOG",
            ),
        )

        raw_crawler.node.add_dependency(bucket)

        curated_crawler = glue.CfnCrawler(self, "CuratedOrdersCrawler",
            name="curated-orders-crawler",
            role=crawler_role.role_arn,
            database_name=database.ref,
            targets=glue.CfnCrawler.TargetsProperty(
                s3_targets=[glue.CfnCrawler.S3TargetProperty(
                    path=f"s3://{bucket.bucket_name}/curated/orders/",
                )],
            ),
            table_prefix="curated_",
            schema_change_policy=glue.CfnCrawler.SchemaChangePolicyProperty(
                update_behavior="UPDATE_IN_DATABASE",
                delete_behavior="LOG",
            ),
        )

        curated_crawler.node.add_dependency(bucket)

        # Near-real-time pipeline: rather than running crawlers/jobs on a
        # schedule (polling) or by hand, EventBridge rules fire the moment a
        # new object lands in `raw/orders/` or `curated/orders/`, and a small
        # Lambda starts the matching crawler or job, every stage typically
        # refreshes within minutes of new data arriving, at effectively no
        # extra cost. See PipelineTriggerFn's targets below for the full chain.
        pipeline_trigger_fn = lambda_.Function(self, "PipelineTriggerFn",
            runtime=lambda_.Runtime.PYTHON_3_12,
            handler="handler.handler",
            code=lambda_.Code.from_asset(os.path.join(LAMBDAS_DIR, "pipeline_trigger")),
            timeout=cdk.Duration.seconds(30),
        )

        pipeline_trigger_fn.add_to_role_policy(
            iam.PolicyStatement(
                actions=["glue:StartCrawler"],
                resources=[
                    self.format_arn(service="glue", resource="crawler", resource_name=raw_crawler.ref),
                    self.format_arn(service="glue", resource="crawler", resource_name=curated_crawler.ref),
                ],
            )
        )

        # New raw JSON triggers two things in parallel: catalog it (raw_crawler)
        # and curate it (curator_job, added once it exists further below)
        raw_landing_rule = events.Rule(self, "RawOrderLandingRule",
            event_pattern=events.EventPattern(
                source=["aws.s3"],
                detail_type=["Object Created"],
                detail={
                    "bucket": {"name": [bucket.bucket_name]},
                    "object": {"key": [{"prefix": "raw/orders/"}]},
                },
            ),
        )

        raw_landing_rule.add_target(
            targets.LambdaFunction(pipeline_trigger_fn,
                event=events.RuleTargetInput.from_object({"crawlerName": raw_crawler.ref}),
            )
        )

        curated_landing_rule = events.Rule(self, "CuratedOrderLandingRule",
            event_pattern=events.EventPattern(
                source=["aws.s3"],
                detail_type=["Object Created"],
                detail={
                    "bucket": {"name": [bucket.bucket_name]},
                    "object": {"key": [{"prefix": "curated/orders/"}]},
                },
            ),
        )

        curated_landing_rule.add_target(
            targets.LambdaFunction(pipeline_trigger_fn,
                event=events.RuleTargetInput.from_object({"crawlerName": curated_crawler.ref}),
            )
        )

        # ETL: a Python Shell job (NOT Spark/EMR). At this data scale a
        # Spark job would spend most of its billed minute on cluster startup;
        # Python Shell runs at a fraction of a DPU and finishes in seconds,
        # the cheapest "real" Glue ETL option, and the right one here.
        job_role = iam.Role(self, "JobRole",
            assumed_by=iam.ServicePrincipal("glue.amazonaws.com"),
            managed_policies=[
                iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSGlueServiceRole"),
            ],
        )

        bucket.grant_read_write(job_role)

        job_script_asset = s3_assets.Asset(self, "CurateOrdersScriptAsset",
            path=os.path.join(GLUE_JOBS_DIR, "curate_orders.py"),
        )

        job_script_asset.grant_read(job_role)

        curator_job = glue.CfnJob(self, "OrdersCuratorJob",
            name="orders-curator-job",
            role=job_role.role_arn,
            command=glue.CfnJob.JobCommandProperty(
                name="pythonshell",
                python_version="3.9",
                script_location=job_script_asset.s3_object_url,
            ),
            default_arguments={
                "--TempDir": f"s3://{bucket.bucket_name}/glue-temp/",
                "--job-bookmark-option": "job-bookmark-disable",
                "--library-set": "analytics",
                "--BUCKET_NAME": bucket.bucket_name,
                "--DATABASE_NAME": database.ref,
            },
            glue_version="3.0",
            max_capacity=0.0625,
            max_retries=0,
            timeout=10,
        )

        curator_job.node.add_dependency(job_script_asset)

        # Completes the raw-landing chain: the same new-JSON event that starts
        # raw_crawler also starts curator_job, so curation runs in parallel
        # with cataloging rather than waiting on it (curate_orders.py reads
        # straight from S3, it has no dependency on the raw_orders table).
        pipeline_trigger_fn.add_to_role_policy(
            iam.PolicyStatement(
                actions=["glue:StartJobRun"],
                resources=[self.format_arn(service="glue", resource="job", resource_name=curator_job.ref)],
            )
        )

        raw_landing_rule.add_target(
            targets.LambdaFunction(pipeline_trigger_fn,
                event=events.RuleTargetInput.from_object({"jobName": curator_job.ref}),
            )
        )

        cdk.CfnOutput(self, "BucketName",
            value=bucket.bucket_name,
            description="Data lake bucket, pass to generate_orders.py",
        )

        cdk.CfnOutput(self, "DatabaseName",
            value=database.ref,
            description="Glue Data Catalog database, pass to compare_queries.py",
        )

        cdk.CfnOutput(self, "RawOrdersCrawlerName",
            value=raw_crawler.ref,
            description="Crawler that catalogs the raw/orders/ JSON zone into raw_orders",
        )

        cdk.CfnOutput(self, "CuratedOrdersCrawlerName",
            value=curated_crawler.ref,
            description="Crawler that catalogs the curated/orders/ Parquet zone into curated_orders",
        )

        cdk.CfnOutput(self, "PipelineTriggerFunctionName",
            value=pipeline_trigger_fn.function_name,
            description="Lambda that starts the matching crawler/job the moment a new object lands in its watched prefix",
        )

        cdk.CfnOutput(self, "OrdersCuratorJobName",
            value=curator_job.ref,
            description="Python Shell job that converts raw JSON orders into partitioned, curated Parquet",
        )

Why hand-write a table the CDK stack's crawler could infer for you? Because, for a table whose shape you already know, you can skip the crawler altogether. A crawler costs runtime (Glue bills a 10-minute minimum per run) and only updates the Catalog when it runs, so a brand-new partition stays invisible to Athena until the next crawl. The DDL below declares the shape once, up front, and partition projection lets Athena compute partition locations straight from the S3 key pattern, no crawler, no schedule, almost no upkeep, and a query against today's data works the instant today's files land.

-- Partition projection: Athena computes partition locations from the S3
-- key pattern itself, no crawler run (or "MSCK REPAIR TABLE") needed after
-- every new day of data lands
CREATE EXTERNAL TABLE analytics_db.orders (
    order_id      string,
    customer_id   string,
    total_amount  double,
    status        string
)
PARTITIONED BY (year string, month string, day string)
STORED AS PARQUET
LOCATION 's3://my-curated-bucket/curated/orders/'
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'
);

-- Cost-aware query: the WHERE clause prunes partitions, so Athena scans
-- only the matching day's Parquet files rather than the whole table
SELECT
    customer_id,
    COUNT(*)          AS order_count,
    SUM(total_amount) AS daily_total
FROM analytics_db.orders
WHERE year = 2026 AND month = 6 AND day = 5
  AND status = 'COMPLETED'
GROUP BY customer_id
ORDER BY daily_total DESC
LIMIT 10;

This panel registers the same Glue Data Catalog database as an external schema inside Redshift, then runs a single query that joins a native Redshift table with that S3-backed external table, so a warehouse can reach into the lake without copying any data into it. It's shown purely as illustration of how the pieces this lesson teaches connect to a warehouse: the showcase project deliberately doesn't deploy a Redshift cluster, since it bills by the hour, the opposite of the project's "stay in the cents" goal.

-- The IAM role attached to the Redshift cluster needs glue:GetTable,
-- glue:GetDatabase, and s3:GetObject on the curated bucket (scope this
-- down from AWSGlueConsoleFullAccess with a custom policy in production)

-- Register the Glue Data Catalog database as an external schema: Redshift
-- now "sees" the same tables Athena queries, with zero data movement
CREATE EXTERNAL SCHEMA spectrum_orders
FROM DATA CATALOG
DATABASE 'analytics_db'
IAM_ROLE 'arn:aws:iam::123456789012:role/RedshiftSpectrumRole'
REGION 'us-east-1';

-- Federated query: joins a native Redshift table (loaded, distributed,
-- fast) with an external Spectrum table (S3-backed, no load step) in
-- one statement, blending warehouse and data-lake data transparently
SELECT
    c.segment,
    DATE_TRUNC('month', o.order_date) AS month,
    SUM(o.total_amount)               AS revenue
FROM customers c
JOIN spectrum_orders.orders o
    ON o.customer_id = c.customer_id
WHERE o.year = 2026
GROUP BY c.segment, DATE_TRUNC('month', o.order_date)
ORDER BY month, revenue DESC;

Key Takeaways

  • The Glue Data Catalog is the shared metadata layer - define a table's schema once and Athena, Redshift Spectrum, and EMR can all query the same S3 data without copying it
  • Crawlers automate discovery, ETL Jobs automate transformation - both are serverless and billed by usage, turning schema management and data cleanup into scheduled, code-light tasks
  • Athena and Redshift solve different problems - Athena is pay-per-scan ad-hoc SQL with zero setup, Redshift is a provisioned or serverless warehouse for recurring, high-concurrency analytics, and Spectrum lets Redshift reach directly into the lake
  • Choose EMR over Glue when you need framework control - full Spark/Hadoop access and custom code in exchange for owning cluster management, the same trade-off Lesson 17 made between Lambda windows and Managed Flink
  • Partitioning and columnar formats are the biggest cost lever - Parquet or ORC plus well-chosen partitions cut scan time and dollars across Athena, Redshift Spectrum, and Glue alike