Distributed SQL Query Engines

Presto/Trino, Impala, Athena: Fast SQL queries on massive datasets without moving data

SQL at the Speed of Thought on Petabyte-Scale Data

When analysts need answers in seconds, not hours, distributed SQL query engines deliver. Unlike Spark SQL (optimized for batch ETL), engines like Presto/Trino, Impala, and Athena are purpose-built for interactive analytics, ad-hoc queries that return results in seconds on petabyte-scale data lakes. They achieve this through MPP (massively parallel processing) architectures that distribute queries across hundreds of workers, query optimization that prunes unnecessary data, and in-memory processing that avoids disk I/O bottlenecks. This lesson explores how these engines work, when to use each one, and how query federation enables joining data across PostgreSQL, S3, MongoDB, and Kafka in a single SQL query.

What are Distributed SQL Query Engines?

Distributed SQL engines execute ANSI SQL queries on data stored across distributed systems (S3, HDFS, databases) without requiring data movement or loading into proprietary formats.

Traditional Database vs Distributed SQL Engine:

TRADITIONAL DATABASE:
┌─────────────────────────────────────┐
│   Database Server (single machine)  │
│   • Data stored locally             │
│   • Query processed on one machine  │
│   • Vertical scaling only           │
│   • Limited to TBs                  │
└─────────────────────────────────────┘


DISTRIBUTED SQL ENGINE (Presto/Trino/Athena):
┌─────────────────────────────────────────────────────┐
│                 Coordinator                         │
│  • Parses SQL                                       │
│  • Plans query execution                            │
│  • Coordinates workers                              │
└───────────────┬─────────────────────────────────────┘
                │
    ┌───────────┴────────────┬──────────────┐
    ▼                        ▼              ▼
┌──────────┐          ┌──────────┐    ┌──────────┐
│ Worker 1 │          │ Worker 2 │    │ Worker N │
│ • Process│          │ • Process│    │ • Process│
│   query  │          │   query  │    │   query  │
│ • Read   │          │ • Read   │    │ • Read   │
│   data   │          │   data   │    │   data   │
└────┬─────┘          └────┬─────┘    └────┬─────┘
     │                     │               │
     └─────────┬───────────┴───────────────┘
               ▼
┌──────────────────────────────────────────┐
│         Data Sources                     │
│  S3, HDFS, Databases, Kafka, etc.        │
└──────────────────────────────────────────┘

KEY DIFFERENCE: Data stays in place, query goes to data
Distributed engines scale by adding workers, not moving data

Key Characteristics

⚡ Low Latency

Seconds to minutes for interactive queries (vs hours for batch)

📊 MPP Architecture

Massively parallel processing across hundreds of workers

🔗 Query Federation

Join data across multiple sources (S3, PostgreSQL, MySQL, etc.)

💾 In-Memory

Pipelined execution without writing intermediate results to disk

📈 Horizontal Scaling

Add workers to handle more concurrent queries or larger datasets

🔍 ANSI SQL

Standard SQL syntax, analysts don't need to learn new languages

Presto/Trino: The Facebook SQL Engine

Presto (now Trino) was created at Facebook to enable interactive SQL queries on their 300+ PB data warehouse. It's designed for ad-hoc analytics, not ETL.

Trino Architecture Deep Dive

Trino Query Execution:

┌────────────────────────────────────────────────────┐
│                  COORDINATOR                       │
│                                                    │
│  1. SQL Parser → Abstract Syntax Tree (AST)        │
│  2. Analyzer → Validate & resolve references       │
│  3. Planner → Generate optimized execution plan    │
│  4. Scheduler → Assign tasks to workers            │
│  5. Coordinator → Aggregate final results          │
└───────────────┬────────────────────────────────────┘
                │
        ┌───────┴────────┬──────────────┐
        ▼                ▼              ▼
    ┌───────┐        ┌───────┐      ┌───────┐
    │Worker1│        │Worker2│      │WorkerN│
    │       │        │       │      │       │
    │Split 1│        │Split 2│      │Split N│
    │(1GB)  │        │(1GB)  │      │(1GB)  │
    └───┬───┘        └───┬───┘      └───┬───┘
        │                │              │
        ▼                ▼              ▼
    ┌────────────────────────────────────┐
    │         Data Sources               │
    │  Hive, S3, PostgreSQL, MySQL, etc. │
    └────────────────────────────────────┘

KEY CONCEPTS:
• Splits: Data chunks (typically 1GB) processed in parallel
• Stages: Pipeline of operations (scan → filter → aggregate)
• Pipelining: Data flows through operators without disk writes
• Cost-based optimization: Chooses optimal join order
Coordinator plans, workers execute in parallel on data splits

Querying Trino with Python

from trino.dbapi import connect
import pandas as pd

# Connect to Trino
conn = connect(
    host='trino.example.com',
    port=8080,
    user='analyst',
    catalog='hive',  # Which data source
    schema='default'  # Which database/schema
)

cursor = conn.cursor()

# ============ SIMPLE QUERY ============
query = """
    SELECT
        customer_id,
        COUNT(*) as order_count,
        SUM(amount) as total_spent
    FROM sales
    WHERE sale_date >= DATE '2024-01-01'
    GROUP BY customer_id
    ORDER BY total_spent DESC
    LIMIT 10
"""

cursor.execute(query)
results = cursor.fetchall()

# Convert to DataFrame
df = pd.DataFrame(results, columns=[desc[0] for desc in cursor.description])
print(df)

"""
+-------------+-------------+-------------+
| customer_id | order_count | total_spent |
+-------------+-------------+-------------+
|        1234 |          45 |     12500.0 |
|        5678 |          38 |     11200.0 |
|        9012 |          42 |     10800.0 |
|         ... |             |             |
+-------------+-------------+-------------+

Query completed in 3.2 seconds
Scanned 1.2 TB, processed 450M rows
"""

# ============ FEDERATED QUERY (multiple sources) ============
# Join S3 data with PostgreSQL customer info
federated_query = """
    SELECT
        s.sale_id,
        s.amount,
        c.name,
        c.email
    FROM hive.default.sales s              -- Data on S3
    JOIN postgresql.public.customers c     -- Data in PostgreSQL
        ON s.customer_id = c.customer_id
    WHERE s.sale_date >= CURRENT_DATE - INTERVAL '7' DAY
"""

cursor.execute(federated_query)
results = cursor.fetchall()

"""
Result: Joins 100GB S3 data with 10GB PostgreSQL data
Completed in 12 seconds

Execution plan:
1. Scan S3 sales (filtered by date)
2. Scan PostgreSQL customers
3. Hash join on customer_id
4. Return results
"""

# ============ COMPLEX ANALYTICS ============
# Window functions, CTEs, aggregations
complex_query = """
    WITH daily_sales AS (
        SELECT
            DATE(sale_date) as day,
            SUM(amount) as daily_total
        FROM sales
        WHERE sale_date >= DATE '2024-01-01'
        GROUP BY DATE(sale_date)
    ),
    moving_avg AS (
        SELECT
            day,
            daily_total,
            AVG(daily_total) OVER (
                ORDER BY day
                ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
            ) as seven_day_avg
        FROM daily_sales
    )
    SELECT *
    FROM moving_avg
    WHERE daily_total > seven_day_avg * 1.2  -- 20% above average
    ORDER BY day DESC
"""

cursor.execute(complex_query)
anomalies = cursor.fetchall()

print(f"Found {len(anomalies)} anomalous days (sales 20% above 7-day average)")

conn.close()
Result:
Simple aggregation: 3.2 seconds on 1.2 TB
Federated join: 12 seconds joining S3 + PostgreSQL
Complex analytics with window functions: Fast, interactive queries

Query Optimization Techniques

# ============ PARTITION PRUNING ============
# BAD: Scans entire table (10 TB)
SELECT * FROM sales WHERE customer_id = 1234

# GOOD: Filters by partition column first (10 GB)
SELECT * FROM sales
WHERE year = 2024
  AND month = 1
  AND customer_id = 1234

# Result: 1000x less data scanned


# ============ PREDICATE PUSHDOWN ============
# Trino pushes filters to data source (avoid reading unnecessary data)
SELECT * FROM postgresql.public.users
WHERE created_at >= '2024-01-01'

# Execution:
# 1. Trino sends filter to PostgreSQL
# 2. PostgreSQL applies filter before returning data
# 3. Only filtered rows transferred to Trino


# ============ JOIN ORDERING ============
# Trino's cost-based optimizer reorders joins

# You write:
SELECT *
FROM large_table   -- 1 TB
JOIN small_table   -- 1 GB
  ON large_table.id = small_table.id

# Trino executes:
# 1. Broadcast small_table to all workers (cheap)
# 2. Hash join with large_table (no shuffle)
# Result: Much faster than shuffling 1 TB table


# ============ COLUMN PRUNING ============
# BAD: Reads all columns from Parquet
SELECT customer_id FROM sales

# GOOD: Parquet is columnar, Trino only reads customer_id column
# Result: Read 100 MB instead of 10 GB


# ============ EXPLAIN PLAN ============
# View query execution plan
EXPLAIN SELECT * FROM sales WHERE year = 2024

"""
Fragment 0 [COORDINATOR]
    Output: customer_id, amount, sale_date
    └─ Exchange (gather)
        └─ Fragment 1 [SOURCE]
            └─ TableScan[hive.default.sales]
                Estimates: {rows: 1000000 (100MB), cpu: 100M, memory: 0B, network: 0B}
                Partition filters: (year = 2024)
                Columns: customer_id, amount, sale_date
"""

# Shows:
# - How data flows through stages
# - Estimated rows/memory/CPU
# - Which filters applied
# - Join strategies used
Result:
Partition pruning: 1000x speedup
Predicate pushdown: Filters applied at source
Smart join ordering: Avoids expensive shuffles
Column pruning: Reads only necessary columns

Apache Impala: Cloudera's SQL Engine

Apache Impala is a native MPP SQL engine for Hadoop, designed for low-latency queries on data stored in HDFS and HBase. Unlike Hive (which uses MapReduce), Impala executes queries directly without translation overhead.

Impala vs Hive

FeatureApache HiveApache Impala
Execution EngineMapReduce / Tez / SparkNative MPP engine (C++)
LatencyMinutes to hoursSeconds to minutes
Use CaseBatch ETL, large scansInteractive analytics
Startup OverheadHigh (launches containers)Low (daemon always running)
Memory UsageSpills to diskIn-memory (can fail on OOM)
Fault ToleranceHigh (retries tasks)Low (query fails on node failure)

Querying Impala with Python

from impala.dbapi import connect
import pandas as pd

# Connect to Impala
conn = connect(
    host='impala-coordinator.example.com',
    port=21050,
    database='default'
)

cursor = conn.cursor()

# ============ REFRESH METADATA ============
# Impala caches metadata; refresh after new data arrives
cursor.execute("INVALIDATE METADATA sales")
cursor.execute("REFRESH sales")

# ============ QUERY EXECUTION ============
query = """
    SELECT
        product_category,
        COUNT(DISTINCT customer_id) as unique_customers,
        SUM(amount) as revenue
    FROM sales
    WHERE year = 2024 AND month = 1
    GROUP BY product_category
    ORDER BY revenue DESC
"""

cursor.execute(query)
results = cursor.fetchall()

df = pd.DataFrame(results, columns=[desc[0] for desc in cursor.description])
print(df)

"""
+------------------+------------------+------------+
| product_category | unique_customers |    revenue |
+------------------+------------------+------------+
|      Electronics |             2500 | 1250000.00 |
|         Clothing |             1800 |  850000.00 |
|             Home |             1200 |  620000.00 |
+------------------+------------------+------------+

Query completed in 2.1 seconds
Scanned 50 GB across 200 partitions
"""

# ============ QUERY PROFILE ============
# Get detailed execution stats
cursor.execute("PROFILE")
profile = cursor.fetchall()

# Profile shows:
# - Time spent in each operator
# - Rows processed per node
# - Memory usage
# - Network I/O

conn.close()
Result: Interactive query on 50 GB completes in 2.1 seconds

AWS Athena: Serverless Presto on S3

AWS Athena is a fully managed, serverless query service based on Trino (Presto). It requires zero infrastructure, just point it at S3 data and start querying with standard SQL.

Athena Architecture & Pricing

Athena Architecture:

┌─────────────────────────────────────┐
│      User (SQL Query via UI/API)    │
└─────────────────┬───────────────────┘
                  │
┌─────────────────▼───────────────────┐
│         AWS Athena Service          │
│  • Serverless (no infra to manage)  │
│  • Auto-scaling workers             │
│  • Based on Trino/Presto            │
└─────────────────┬───────────────────┘
                  │
        ┌─────────┴─────────┐
        ▼                   ▼
┌──────────────┐    ┌──────────────┐
│  S3 Bucket   │    │ Glue Catalog │
│  (Data Lake) │    │  (Metadata)  │
└──────────────┘    └──────────────┘

PRICING MODEL:
• $5 per TB scanned (us-east-1)
• No charge for DDL (CREATE TABLE, etc.)
• No charge for failed queries
• No infrastructure costs

COST OPTIMIZATION:
• Partition data: Reduce scanned data by 100x
• Use Parquet/ORC: 5-10x less scanned vs JSON/CSV
• Compress data: Further reduce scan size
• SELECT only needed columns: Columnar formats read only specified columns

Example Cost:
Query 1 TB JSON → $5
Query 100 GB Parquet (partitioned) → $0.50 (10x savings)
Athena charges per TB scanned; optimization reduces costs 10-100x

Querying Athena with Python (PyAthena)

import boto3
from pyathena import connect
import pandas as pd

# ============ METHOD 1: PyAthena (DBAPI) ============
conn = connect(
    s3_staging_dir='s3://my-bucket/athena-results/',
    region_name='us-east-1'
)

cursor = conn.cursor()

query = """
    SELECT
        product_id,
        SUM(quantity) as total_sold,
        SUM(amount) as total_revenue
    FROM sales
    WHERE year = 2024
      AND month = 1
    GROUP BY product_id
    HAVING total_revenue > 10000
    ORDER BY total_revenue DESC
    LIMIT 100
"""

# Execute query (asynchronous by default)
cursor.execute(query)

# Fetch results
results = cursor.fetchall()
df = pd.DataFrame(results, columns=[desc[0] for desc in cursor.description])

print(df)
"""
+------------+------------+---------------+
| product_id | total_sold | total_revenue |
+------------+------------+---------------+
|       1001 |        450 |      125000.0 |
|       2003 |        380 |      110000.0 |
|       5012 |        290 |       95000.0 |
+------------+------------+---------------+

Query statistics:
• Data scanned: 12.5 GB
• Execution time: 4.2 seconds
• Cost: ~$0.06
"""

# ============ METHOD 2: Pandas Integration ============
# Query directly to DataFrame
df = pd.read_sql(query, conn)

# ============ METHOD 3: Boto3 (AWS SDK) ============
# More control over execution
athena_client = boto3.client('athena', region_name='us-east-1')

# Start query execution
response = athena_client.start_query_execution(
    QueryString=query,
    QueryExecutionContext={'Database': 'my_database'},
    ResultConfiguration={
        'OutputLocation': 's3://my-bucket/athena-results/'
    }
)

query_execution_id = response['QueryExecutionId']

# Wait for query to complete
import time
while True:
    result = athena_client.get_query_execution(QueryExecutionId=query_execution_id)
    state = result['QueryExecution']['Status']['State']

    if state in ['SUCCEEDED', 'FAILED', 'CANCELLED']:
        break

    time.sleep(1)

# Get execution statistics
stats = result['QueryExecution']['Statistics']
print(f"Data scanned: {stats['DataScannedInBytes'] / 1e9:.2f} GB")
print(f"Execution time: {stats['TotalExecutionTimeInMillis'] / 1000:.2f} seconds")
print(f"Cost: ${stats['DataScannedInBytes'] / 1e12 * 5:.4f}")

# Read results from S3 (parse the S3 URL)
result_location = result['QueryExecution']['ResultConfiguration']['OutputLocation']
bucket = result_location.split('/')[2]
key = '/'.join(result_location.split('/')[3:])
obj = boto3.client('s3').get_object(Bucket=bucket, Key=key)
df = pd.read_csv(obj['Body'])
Result:
Query 12.5 GB in 4.2 seconds
Cost: $0.06
Zero infrastructure management

Advanced Athena Features

# ============ PARTITIONING (Critical for cost optimization) ============
# Create partitioned table
CREATE EXTERNAL TABLE sales_partitioned (
    order_id STRING,
    customer_id STRING,
    amount DOUBLE
)
PARTITIONED BY (year INT, month INT, day INT)
STORED AS PARQUET
LOCATION 's3://my-bucket/sales/'

# Add partitions (after loading data)
MSCK REPAIR TABLE sales_partitioned

# Query with partition filter (scans only relevant files)
SELECT * FROM sales_partitioned
WHERE year = 2024 AND month = 1 AND day = 15
-- Scans: 1 day = 100 GB
-- Without partition filter: 100 TB (1000x more!)


# ============ CTAS (CREATE TABLE AS SELECT) ============
# Convert JSON to optimized Parquet
CREATE TABLE sales_optimized
WITH (
    format = 'PARQUET',
    parquet_compression = 'SNAPPY',
    partitioned_by = ARRAY['year', 'month'],
    bucketed_by = ARRAY['customer_id'],
    bucket_count = 100
)
AS
SELECT *
FROM sales_json
WHERE year >= 2024

# Result: 10x smaller, 10x faster queries, 100x cost savings


# ============ FEDERATED QUERIES ============
# Query across S3, RDS, DynamoDB in single query
SELECT
    s.order_id,
    s.amount,
    c.name,
    d.loyalty_points
FROM "hive".default.sales s                -- S3 data
JOIN "postgres".public.customers c         -- RDS PostgreSQL
    ON s.customer_id = c.customer_id
LEFT JOIN "dynamodb".default.loyalty d     -- DynamoDB
    ON s.customer_id = d.customer_id
WHERE s.year = 2024


# ============ QUERY RESULT REUSE ============
# Athena caches results for 7 days
# Re-running identical query = instant results, $0 cost


# ============ WORKGROUPS (Cost control & governance) ============
# Create workgroup with limits
aws athena create-work-group --name analytics-team \
    --configuration "EnforceWorkGroupConfiguration=true,\
                     BytesScannedCutoffPerQuery=1000000000000"  # 1 TB limit

# Queries exceeding limit are automatically cancelled
Result:
Partitioning: 1000x less data scanned
Parquet conversion: 10x smaller, 10x faster
Query federation: Join S3 + RDS + DynamoDB
Workgroups: Enforce cost limits per team

Query Federation: Join Data Across Sources

Query federation allows joining data from multiple sources (databases, data lakes, APIs) in a single SQL query without ETL or data movement.

Traditional Approach (ETL):
1. Extract data from PostgreSQL → S3
2. Extract data from MySQL → S3
3. Extract data from MongoDB → S3
4. Wait hours for ETL
5. Query joined data in S3

Problems: Data staleness, ETL complexity, storage duplication


Federated Query Approach:
1. Query all sources directly
2. Join in distributed engine
3. Results in seconds
4. Always fresh data

┌─────────────────────────────────────────┐
│         Trino/Athena Coordinator        │
└────┬─────────┬─────────┬──────────┬─────┘
     │         │         │          │
     ▼         ▼         ▼          ▼
┌─────────┐ ┌───────┐ ┌──────┐ ┌──────────┐
│   S3    │ │  RDS  │ │MySQL │ │ DynamoDB │
│ (Sales) │ │(Users)│ │(Prod)│ │ (Events) │
└─────────┘ └───────┘ └──────┘ └──────────┘

Single SQL joins data from all sources!
Federation eliminates ETL and enables real-time joins across sources

Example: Complex Federated Query

# Athena with federated data sources (via Data Source Connectors)
SELECT
    -- From S3 data lake (100 GB)
    s.order_id,
    s.order_date,
    s.amount,

    -- From PostgreSQL RDS (10 GB)
    c.customer_name,
    c.email,
    c.segment,

    -- From MySQL on-prem (5 GB)
    p.product_name,
    p.category,

    -- From DynamoDB (real-time)
    l.loyalty_tier,
    l.points_balance,

    -- From MongoDB (user activity)
    m.last_login,
    m.page_views

FROM "s3_catalog".sales.orders s

-- Join with PostgreSQL
INNER JOIN "postgres_catalog".public.customers c
    ON s.customer_id = c.customer_id

-- Join with MySQL
INNER JOIN "mysql_catalog".products.product_info p
    ON s.product_id = p.product_id

-- Join with DynamoDB
LEFT JOIN "dynamo_catalog".default.loyalty_program l
    ON s.customer_id = l.customer_id

-- Join with MongoDB
LEFT JOIN "mongo_catalog".analytics.user_activity m
    ON s.customer_id = m.customer_id

WHERE s.order_date >= CURRENT_DATE - INTERVAL '30' DAY
  AND c.segment = 'premium'
  AND s.amount > 100

ORDER BY s.order_date DESC
LIMIT 1000

-- Result: Joins 5 different sources in single query
-- Execution time: ~15 seconds
-- No ETL required, always fresh data
Result:
Joins S3 + PostgreSQL + MySQL + DynamoDB + MongoDB in 15 seconds
No ETL or data movement required
Real-time results from all sources

When to Use: Distributed SQL vs Spark SQL

Distributed SQL engines and Spark SQL have different strengths. Choose based on use case.

CriteriaDistributed SQL (Trino/Athena)Spark SQL
Use CaseInteractive analytics, ad-hoc queriesBatch ETL, large-scale processing
LatencySeconds to minutesMinutes to hours
ConcurrencyHigh (100s of concurrent users)Low (10s of concurrent jobs)
Fault ToleranceLow (query fails on node failure)High (retries failed tasks)
MemoryIn-memory only (fails on OOM)Spills to disk on OOM
Query SizeSmall to medium (GB to TB)Large (TB to PB)
Complex LogicSQL onlySQL + Python/Scala/Java
ML IntegrationLimitedExcellent (MLlib)
Best ForBI dashboards, analyst queries, reportingETL pipelines, ML training, complex transformations

Decision Matrix

Use Distributed SQL if:
  • Analysts need interactive queries (seconds)
  • 100+ concurrent users querying
  • BI dashboards need fast refresh
  • Ad-hoc exploration of data
  • Query federation across sources
  • Serverless (Athena) simplicity desired
Use Spark SQL if:
  • ETL pipelines transforming TBs daily
  • Complex Python/Scala logic required
  • ML model training on big data
  • Fault tolerance critical (long jobs)
  • Streaming + batch in same framework
  • Budget for cluster management
💡 Best Practice: Use both! Spark SQL for ETL/processing, distributed SQL (Trino/Athena) for analytics/BI. Many organizations run Spark jobs nightly to transform data, then use Athena/Trino for analyst queries during the day. This hybrid approach leverages strengths of each engine.

Key Takeaways

  • Distributed SQL engines enable interactive queries on big data
  • Trino/Presto: Open-source, multi-source, powerful optimizer
  • Impala: Native MPP for Hadoop, very fast on HDFS
  • Athena: Serverless Trino on S3, zero infrastructure
  • Query federation joins data across sources without ETL
  • Optimization: Partition pruning, predicate pushdown, column pruning
  • Use for: BI, dashboards, analyst queries (not ETL)
  • Complement Spark: Spark for ETL, SQL engines for analytics
Remember: Distributed SQL engines are optimized for interactive analytics, not batch processing. Use Athena for simplicity and zero ops. Use Trino for multi-engine flexibility and cost control. Use Impala if heavily invested in Hadoop. Combine with Spark for complete analytics stack: Spark transforms data overnight, SQL engines power dashboards during the day. Optimize aggressively (partitions, Parquet, compression) for 10-100x speedups and cost savings.