Data Analytics at Scale

Query engines and analytical processing for petabyte-scale data

From OLTP to OLAP at Scale

Traditional databases excel at transactional workloads (OLTP), thousands of small, fast queries updating individual records. But when you need to analyze billions of rows to answer "how many customers purchased in Q4?" or "what's our revenue trend by region?", you need OLAP (Online Analytical Processing). Modern query engines like Presto, Apache Spark, and Druid can scan petabytes of data in seconds by distributing work across hundreds of machines. Understanding these tools is critical for building data-driven applications at scale.

OLTP vs OLAP: Understanding the Difference

OLTP (Transactional)

Online Transaction Processing, designed for day-to-day operations

  • Query Pattern: Small, fast queries (milliseconds)
  • Operations: INSERT, UPDATE, DELETE single records
  • Volume: Thousands of concurrent users
  • Data Touched: Few rows at a time
  • Examples: E-commerce checkout, bank transfers, user login
  • Databases: PostgreSQL, MySQL, Oracle

OLAP (Analytical)

Online Analytical Processing, designed for business intelligence

  • Query Pattern: Complex, long-running (seconds to minutes)
  • Operations: SELECT with aggregations (SUM, AVG, GROUP BY)
  • Volume: Dozens of analysts running queries
  • Data Touched: Millions to billions of rows
  • Examples: Sales reports, trend analysis, dashboards
  • Tools: Presto, Spark, Snowflake, BigQuery
πŸ’‘ Key Insight: OLTP databases are optimized for row-oriented storage (update one customer quickly). OLAP systems use column-oriented storage (scan millions of revenue values quickly). This fundamental difference drives architecture choices.

Column-Oriented Storage: The Secret to Speed

The single most important optimization for analytical queries is storing data by column instead of by row.

Row-Oriented Storage (Traditional)

Stored on disk:
Row 1: [id=1, name="Alice", age=30, salary=80000, dept="Eng"]
Row 2: [id=2, name="Bob", age=25, salary=60000, dept="Sales"]
Row 3: [id=3, name="Charlie", age=35, salary=90000, dept="Eng"]
...

Query: SELECT AVG(salary) FROM employees WHERE dept = 'Eng';

Problem: Must read ALL columns for ALL rows (name, age, etc.)
even though we only need salary and dept!
Performance: Reads 100% of data (wasteful)
For 1 billion rows Γ— 50 bytes = 50GB read from disk

Column-Oriented Storage (Analytical)

Stored on disk:
dept column:   ["Eng", "Sales", "Eng", "Eng", "Sales", ...]
salary column: [80000, 60000, 90000, 75000, 55000, ...]
name column:   ["Alice", "Bob", "Charlie", ...] (not needed)
age column:    [30, 25, 35, ...] (not needed)

Query: SELECT AVG(salary) FROM employees WHERE dept = 'Eng';

Optimization: Only read dept and salary columns!
Compression: "Eng" repeated 1M times compresses to few bytes
Performance: Reads only 2 columns out of 50
For 1 billion rows Γ— 8 bytes (2 columns) = 8GB read (6x faster!)
With compression: ~1GB (50x faster!)

Compression Benefits

πŸ—œοΈ Run-Length Encoding

["Eng", "Eng", "Eng"] β†’ [("Eng", 3)]
Perfect for sorted columns

πŸ“– Dictionary Encoding

"Engineering" β†’ 1, "Sales" β†’ 2
Store IDs instead of strings

πŸ”’ Bit Packing

Store age (0-100) in 7 bits instead of 32
4.5x compression

Presto (Trino): Distributed SQL Query Engine

Query petabytes across data lakes with standard SQL

Presto (now called Trino) is a distributed SQL query engine developed at Facebook. It runs queries across multiple data sources (S3, HDFS, Cassandra, MySQL) without moving data. Perfect for ad-hoc analytics and data lake exploration.

Architecture: Coordinator + Workers

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         PRESTO CLUSTER ARCHITECTURE             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                           β”‚
β”‚  β”‚   Coordinator    β”‚  (Receives query, plans)  β”‚
β”‚  β”‚  - Parse SQL     β”‚                           β”‚
β”‚  β”‚  - Optimize      β”‚                           β”‚
β”‚  β”‚  - Schedule      β”‚                           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                           β”‚
β”‚           β”‚                                     β”‚
β”‚           β”‚ Distributes tasks                   β”‚
β”‚           β–Ό                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”    β”‚
β”‚  β”‚                  β”‚                  β”‚   β”‚    β”‚
β”‚  β–Ό                  β–Ό                  β–Ό   β”‚    β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚    β”‚
β”‚ β”‚Worker 1 β”‚    β”‚Worker 2 β”‚    β”‚Worker N β”‚  β”‚    β”‚
β”‚ β”‚Scan S3  β”‚    β”‚Scan S3  β”‚    β”‚Scan S3  β”‚  β”‚    β”‚
β”‚ β”‚1M rows  β”‚    β”‚1M rows  β”‚    β”‚1M rows  β”‚  β”‚    β”‚
β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β”‚    β”‚
β”‚      β”‚              β”‚              β”‚       β”‚    β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚    β”‚
β”‚                     β”‚                      β”‚    β”‚
β”‚                     β–Ό                      β”‚    β”‚
β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”‚    β”‚
β”‚              β”‚ Aggregate    β”‚              β”‚    β”‚
β”‚              β”‚ & Return     β”‚              β”‚    β”‚
β”‚              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Parallel execution: Each worker scans partition of data
Results streamed back through coordinator

Key Features

πŸ”Œ Federated Queries

Query across S3, Postgres, Cassandra in single SQL statement

⚑ In-Memory Processing

Pipelined execution, no intermediate disk writes

πŸ“Š ANSI SQL

Standard SQL, no new language to learn

πŸš€ Low Latency

Interactive queries, seconds instead of hours

Example: Querying Data Lake (Parquet on S3)

-- Define table pointing to S3 Parquet files
CREATE TABLE sales (
  order_id BIGINT,
  customer_id BIGINT,
  product_id BIGINT,
  amount DECIMAL(10,2),
  order_date DATE
)
WITH (
  format = 'PARQUET',
  external_location = 's3://my-data-lake/sales/'
);

-- Query 1 billion rows across 100 workers
SELECT 
  DATE_TRUNC('month', order_date) AS month,
  SUM(amount) AS revenue,
  COUNT(DISTINCT customer_id) AS customers
FROM sales
WHERE order_date >= DATE '2024-01-01'
GROUP BY 1
ORDER BY 1;
Execution:
β€’ 100 workers scan S3 in parallel (10M rows each)
β€’ Column pruning: Only reads order_date, amount, customer_id
β€’ Predicate pushdown: Skips Parquet files before 2024-01-01
β€’ Query time: ~15 seconds for 1B rows

Result:
month | revenue | customers 2024-01-01 | 15,234,567 | 234,567 2024-02-01 | 16,789,012 | 245,891 2024-03-01 | 18,234,456 | 256,789

Example: Federated Query (Join Across Sources)

-- Join S3 data lake with live PostgreSQL
SELECT 
  c.name,
  c.email,
  SUM(s.amount) AS total_spent
FROM hive.default.sales s           -- S3 / Hive catalog
JOIN postgresql.public.customers c  -- Live Postgres
  ON s.customer_id = c.id
WHERE s.order_date >= DATE '2024-01-01'
GROUP BY c.name, c.email
HAVING SUM(s.amount) > 10000
ORDER BY total_spent DESC
LIMIT 100;
Execution:
β€’ Scans 1B rows from S3 in parallel
β€’ Fetches matching customers from Postgres
β€’ Performs distributed join in-memory
β€’ No data movement required!

Result: Top 100 customers by spend (seconds)

When to Use Presto

  • Ad-hoc exploratory queries on data lakes (S3, HDFS)
  • Joining data across multiple systems without ETL
  • Interactive analytics (BI tools like Tableau, Looker)
  • When you need sub-minute query latency
  • Data sits in Parquet/ORC/Avro format

Pros & Cons

βœ… Pros
  • Fast interactive queries (seconds)
  • Standard SQL, easy adoption
  • Query multiple data sources
  • No data movement needed
  • Scales to petabytes
  • Active community (Trino)
❌ Cons
  • Memory intensive (in-memory)
  • Not great for batch ETL jobs
  • Complex queries can OOM
  • No built-in fault tolerance
  • Requires cluster management
  • Slower than Spark for iterative ML

Apache Spark: Unified Analytics Engine

Batch, streaming, ML, and graph processing in one framework

Apache Spark is a distributed computing framework that processes massive datasets using in-memory computation. Unlike Hadoop MapReduce (which writes to disk between stages), Spark keeps data in memory, making it 100x faster for iterative algorithms. It's the Swiss Army knife of big data.

Architecture: Driver + Executors

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚          SPARK CLUSTER ARCHITECTURE              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                             β”‚
β”‚  β”‚  Spark Driver   β”‚  (Your code, DAG creation)  β”‚
β”‚  β”‚  - SparkContext β”‚                             β”‚
β”‚  β”‚  - DAG Schedulerβ”‚                             β”‚
β”‚  β”‚  - Task         β”‚                             β”‚
β”‚  β”‚    Scheduler    β”‚                             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                             β”‚
β”‚           β”‚                                      β”‚
β”‚           β”‚ Distributes tasks                    β”‚
β”‚           β–Ό                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”            β”‚
β”‚  β”‚              β”‚              β”‚    β”‚            β”‚
β”‚  β–Ό              β–Ό              β–Ό    β”‚            β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚ β”‚Executor 1β”‚ β”‚Executor 2β”‚ β”‚Executor Nβ”‚           β”‚
β”‚ β”‚4 cores   β”‚ β”‚4 cores   β”‚ β”‚4 cores   β”‚           β”‚
β”‚ β”‚16GB RAM  β”‚ β”‚16GB RAM  β”‚ β”‚16GB RAM  β”‚           β”‚
β”‚ β”‚          β”‚ β”‚          β”‚ β”‚          β”‚           β”‚
β”‚ β”‚ Task 1   β”‚ β”‚ Task 3   β”‚ β”‚ Task 5   β”‚           β”‚
β”‚ β”‚ Task 2   β”‚ β”‚ Task 4   β”‚ β”‚ Task 6   β”‚           β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚      β–²             β–²             β–²               β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β”‚                    β”‚                             β”‚
β”‚             Read from HDFS/S3                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
In-memory caching: Data stays in executor RAM between stages
Lazy evaluation: Builds execution plan before running

Key Features

πŸ’Ύ In-Memory Compute

Cache datasets in RAM, 100x faster than disk-based MapReduce

πŸ”„ Unified API

Batch (SQL), streaming, ML, graph, all in one framework

🐍 Multiple Languages

Python, Scala, Java, R, SQL, choose your preference

πŸ”§ Fault Tolerant

Lineage tracking, automatically recomputes lost partitions

Example: Spark SQL (DataFrame API)

from pyspark.sql import SparkSession
from pyspark.sql.functions import col, sum, count, date_trunc

# Initialize Spark
spark = SparkSession.builder \
    .appName("Sales Analysis") \
    .config("spark.executor.memory", "16g") \
    .config("spark.executor.cores", "4") \
    .getOrCreate()

# Read Parquet from S3 (billions of rows)
sales_df = spark.read.parquet("s3://data-lake/sales/")

# Perform aggregation (distributed across cluster)
monthly_revenue = sales_df \
    .filter(col("order_date") >= "2024-01-01") \
    .groupBy(date_trunc("month", "order_date").alias("month")) \
    .agg(
        sum("amount").alias("revenue"),
        count("customer_id").alias("orders")
    ) \
    .orderBy("month")

# Execute and show results
monthly_revenue.show()
Execution:
β€’ Lazy evaluation: Builds execution plan first
β€’ Catalyst optimizer: Pushes filters, prunes columns
β€’ Tungsten execution: Generates optimized bytecode
β€’ Executes in parallel across all executors

Result:
+-------------------+-----------+--------+
|month              |revenue    |orders  |
+-------------------+-----------+--------+
|2024-01-01 00:00:00|15234567.89|123456  |
|2024-02-01 00:00:00|16789012.34|134567  |
|2024-03-01 00:00:00|18234456.78|145678  |
+-------------------+-----------+--------+

Example: Spark Streaming (Real-Time)

# Read streaming data from Kafka
stream_df = spark.readStream \
    .format("kafka") \
    .option("kafka.bootstrap.servers", "localhost:9092") \
    .option("subscribe", "clickstream") \
    .load()

# Parse JSON and aggregate in 5-minute windows
from pyspark.sql.functions import window, from_json
from pyspark.sql.types import StructType, StructField, StringType, LongType, TimestampType

schema = StructType([
    StructField("timestamp", TimestampType()),
    StructField("page_url", StringType()),
    StructField("user_id", LongType())
])

clicks_per_page = stream_df \
    .selectExpr("CAST(value AS STRING) as json") \
    .select(from_json("json", schema).alias("data")) \
    .select("data.*") \
    .groupBy(
        window("timestamp", "5 minutes"),
        "page_url"
    ) \
    .count() \
    .orderBy("window", "count", ascending=False)

# Write results to dashboard (continuously)
query = clicks_per_page.writeStream \
    .format("memory") \
    .queryName("top_pages") \
    .outputMode("complete") \
    .start()
Execution:
β€’ Micro-batch processing: 5-minute tumbling windows
β€’ Stateful aggregation: Maintains window state
β€’ Fault-tolerant: Checkpointing to HDFS/S3
β€’ Updates continuously as new data arrives

Result: Real-time dashboard of popular pages

Example: Machine Learning (MLlib)

from pyspark.ml.feature import VectorAssembler
from pyspark.ml.regression import LinearRegression

# Load training data (millions of rows)
data = spark.read.parquet("s3://ml-data/housing/")

# Prepare features
assembler = VectorAssembler(
    inputCols=["sqft", "bedrooms", "bathrooms", "age"],
    outputCol="features"
)
training_data = assembler.transform(data)

# Split into training and test sets
train_data, test_data = training_data.randomSplit([0.8, 0.2], seed=42)

# Train model (distributed)
lr = LinearRegression(
    featuresCol="features",
    labelCol="price",
    maxIter=10
)
model = lr.fit(train_data)

# Make predictions
predictions = model.transform(test_data)
predictions.select("features", "price", "prediction").show()
Execution:
β€’ Distributed training across cluster
β€’ Gradient descent parallelized
β€’ Model broadcasted to executors
β€’ Predictions computed in parallel

Result: Trained model + predictions on test set

When to Use Spark

  • Batch ETL jobs processing terabytes daily
  • Real-time streaming analytics (Kafka, Kinesis)
  • Machine learning at scale (MLlib, TensorFlow)
  • Iterative algorithms (PageRank, K-means)
  • Complex multi-stage data pipelines
  • When you need fault tolerance for long jobs

Pros & Cons

βœ… Pros
  • 100x faster than MapReduce
  • Unified API (batch + streaming + ML)
  • Fault tolerant (lineage tracking)
  • Rich ecosystem (Spark SQL, MLlib)
  • Works with existing Hadoop data
  • Multiple language support
❌ Cons
  • Memory intensive (requires tuning)
  • Complex configuration (100+ params)
  • Steeper learning curve than SQL
  • Not great for sub-second latency
  • GC pauses with large heaps
  • Slower than Presto for ad-hoc queries

Apache Druid: Real-Time Analytics Database

Sub-second queries on streaming and historical data

Apache Druid is designed for real-time analytics on event streams. It ingests data continuously, pre-aggregates it, and serves queries in milliseconds. Perfect for dashboards, anomaly detection, and user-facing analytics where speed matters.

Architecture: Real-Time + Historical

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             DRUID ARCHITECTURE                         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                        β”‚
β”‚  Ingestion Layer:                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                        β”‚
β”‚  β”‚  Kafka   │─────▢│ Real-timeβ”‚  (Indexing Service)    β”‚
β”‚  β”‚ Stream   β”‚      β”‚  Nodes   β”‚  Ingest + Index        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜                        β”‚
β”‚                          β”‚                             β”‚
β”‚                          β–Ό (persist)                   β”‚
β”‚  Storage Layer:    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚Deep      β”‚                        β”‚
β”‚  β”‚Historical│◀─────│Storage   β”‚  (S3/HDFS)             β”‚
β”‚  β”‚Nodes     β”‚      β”‚(Segments)β”‚  Column-oriented       β”‚
β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                        β”‚
β”‚       β”‚                                                β”‚
β”‚  Query Layer:                                          β”‚
β”‚       β–Ό                                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                        β”‚
β”‚  β”‚ Broker   │─────▢│  Query   β”‚                        β”‚
β”‚  β”‚ Nodes    β”‚      β”‚ Results  β”‚  (Merge + Return)      β”‚
β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                        β”‚
β”‚       β–²                                                β”‚
β”‚       β”‚                                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”                                          β”‚
β”‚  β”‚ Client   β”‚  SELECT ... WHERE timestamp > now()-1h   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Hot data: Real-time nodes (recent data)
Cold data: Historical nodes (deep storage)
Query: Broker merges results from both

Key Features

⚑ Sub-Second Queries

Bitmap indexes + columnar storage = millisecond responses

πŸ”„ Real-Time Ingestion

Stream from Kafka, data queryable within seconds

πŸ“Š Roll-Up Aggregation

Pre-aggregate at ingestion, store summaries not raw events

🎯 Approximate Algorithms

HyperLogLog for COUNT(DISTINCT), 99% accurate, 1% memory

Example: Ingestion Spec (Roll-Up)

{
  "type": "kafka",
  "dataSchema": {
    "dataSource": "clickstream",
    "timestampSpec": { "column": "timestamp", "format": "iso" },
    "dimensionsSpec": {
      "dimensions": ["user_id", "page_url", "country", "device"]
    },
    "metricsSpec": [
      { "type": "count", "name": "count" },
      { "type": "longSum", "name": "session_duration", "fieldName": "duration" },
      { "type": "hyperUnique", "name": "unique_users", "fieldName": "user_id" }
    ],
    "granularitySpec": {
      "segmentGranularity": "hour",  // 1 segment per hour
      "queryGranularity": "minute"   // Roll up to minute
    }
  }
}
Roll-Up Result:
Input: 1M raw events per minute
After roll-up: ~10K rows per minute (100x compression)
Stores: aggregated metrics by (timestamp, user_id, page, country, device)

Example: Real-Time Dashboard Query

SELECT 
  TIME_FLOOR(__time, 'PT5M') AS time_bucket,
  country,
  SUM(count) AS page_views,
  APPROX_COUNT_DISTINCT_DS_HLL(unique_users) AS users
FROM clickstream
WHERE __time >= CURRENT_TIMESTAMP - INTERVAL '1' HOUR
GROUP BY 1, 2
ORDER BY 1 DESC, 3 DESC
LIMIT 100;
Execution:
β€’ Scans pre-aggregated segments (not raw events)
β€’ Bitmap indexes filter by time instantly
β€’ HyperLogLog gives approximate unique users
β€’ Query time: ~50ms for 1 hour of data

Result:
time_bucket         |country|page_views|users
2024-03-15 14:55:00 |US     |1,234,567 |45,678
2024-03-15 14:50:00 |US     |1,198,234 |44,123
2024-03-15 14:55:00 |UK     |567,890   |23,456

Example: Approximate COUNT DISTINCT

-- Exact count (slow - scans all events)
SELECT COUNT(DISTINCT user_id) FROM clickstream;
-- Scans: 1 billion rows, Takes: 30 seconds

-- Approximate count (fast - uses HyperLogLog sketch)
SELECT APPROX_COUNT_DISTINCT_DS_HLL(unique_users) 
FROM clickstream;
-- Scans: pre-aggregated segments, Takes: 100ms
-- Accuracy: Β±1-2% error
Result:
Exact: 45,678,912 users (30s)
Approximate: 45,234,567 users (100ms)
Error: 0.97%, acceptable for dashboards!

When to Use Druid

  • User-facing analytics dashboards (need sub-second)
  • Real-time monitoring and alerting
  • Clickstream and event analytics
  • Network telemetry and APM
  • Time-series data with high cardinality
  • When approximate algorithms are acceptable

Pros & Cons

βœ… Pros
  • Extremely fast (sub-second queries)
  • Real-time ingestion (Kafka)
  • Automatic roll-up (compression)
  • High concurrency (1000+ QPS)
  • Scales to petabytes
  • Built-in approximate algorithms
❌ Cons
  • Complex architecture (many components)
  • No joins (denormalize at ingestion)
  • Updates/deletes are expensive
  • Requires careful dimension design
  • High operational overhead
  • Steep learning curve

Query Engine Comparison

FeaturePresto/TrinoApache SparkApache Druid
Primary Use CaseAd-hoc SQL queriesBatch ETL + MLReal-time dashboards
Query LatencySecondsSeconds to minutesMilliseconds
Data SourcesS3, HDFS, DBs (federated)S3, HDFS, KafkaKafka, S3 (pre-indexed)
Real-Time Streaming❌ Noβœ… Yes (micro-batch)βœ… Yes (true streaming)
Fault ToleranceLimited (in-memory)βœ… Strong (lineage)βœ… Strong (segments)
Machine Learning❌ Noβœ… MLlib built-in❌ No
Concurrency100s of queries10s of jobs1000s QPS
Setup ComplexityMediumMedium-HighHigh

Best Practices for Analytics at Scale

Never store analytics data in CSV or JSON. Always use columnar formats like Parquet or ORC.

βœ… Parquet Benefits
  • 10-100x compression
  • Column pruning (read only needed columns)
  • Predicate pushdown (skip entire files)
  • Schema evolution support
  • Universal tool support
❌ CSV Problems
  • No compression (10x larger)
  • Must read entire file
  • No schema (parsing errors)
  • Slow to parse (CPU intensive)
  • No predicate pushdown

Partition by columns that are frequently used in WHERE clauses to enable partition pruning.

-- Bad: No partitioning
s3://data-lake/sales/all_data.parquet (1TB file)
Query: WHERE date = '2024-01-15'
Scans: Entire 1TB file

-- Good: Partition by date
s3://data-lake/sales/year=2024/month=01/day=15/*.parquet
Query: WHERE date = '2024-01-15'
Scans: Only files in /day=15/ (~3GB)
⚠️ Warning: Don't over-partition! Too many small files (less than 100MB) hurt performance. Aim for 128MB-1GB per file.

Different engines excel at different workloads. Choose based on your access patterns.

Access PatternBest ToolWhy
Ad-hoc exploration (analysts)PrestoFast, standard SQL, no setup
Daily ETL pipelinesSparkFault tolerant, handles failures
Real-time dashboardsDruidSub-second latency
Machine learningSparkMLlib integration
Joining multiple sourcesPrestoFederated queries

For data accessed repeatedly, caching can provide 10-100x speedup.

# Spark: Cache hot datasets in memory
df = spark.read.parquet("s3://data/orders/")
df_filtered = df.filter(col("date") >= "2024-01-01")
df_filtered.cache()  # Keep in executor memory

# First query: reads from S3 (slow)
df_filtered.groupBy("region").sum("revenue").show()

# Subsequent queries: reads from memory (fast!)
df_filtered.groupBy("product").sum("revenue").show()
df_filtered.groupBy("customer_segment").sum("revenue").show()
Result: First query: 30s, Subsequent: 2s (15x faster)

For dashboards and monitoring, approximate results are often good enough and 100x faster.

HyperLogLog (COUNT DISTINCT)

Memory: ~12KB vs full set in RAM
Accuracy: Β±2% error
Speed: 100x faster

APPROX_COUNT_DISTINCT(user_id)
T-Digest (PERCENTILE)

Memory: Fixed size sketch
Accuracy: Very high
Speed: 50x faster

APPROX_PERCENTILE(latency, 0.95)

Poor resource allocation is the #1 cause of slow queries. Monitor and adjust.

Key Spark Configurations:
  • spark.executor.memory, Too low = OOM, too high = GC pauses
  • spark.executor.cores, More cores = more parallelism
  • spark.sql.shuffle.partitions, Default 200, increase for big data
  • spark.memory.fraction, 60% memory for execution vs storage
πŸ’‘ Rule of Thumb: For Spark, aim for 128-256MB per partition. If processing 1TB, use ~4000-8000 partitions.

Common Pitfalls to Avoid

❌ Data Skew (Hot Partitions)

Problem: One partition has 90% of data, others idle

Solution: Use salting, add random prefix to skewed keys. Example: Instead of GROUP BY user_id, use GROUP BY CONCAT(user_id, '_', RAND(1-10))

❌ Reading Too Much Data

Problem: SELECT * from billions of rows

Solution: Always use WHERE clauses with partition keys. Use LIMIT for exploration. Project only needed columns (SELECT specific_cols, not *).

❌ Too Many Small Files

Problem: 1 million files of 1KB each (metadata overhead)

Solution: Compact files regularly. Use coalesce() or repartition() to merge small files into 128MB-1GB files.

❌ Not Using Broadcast Joins

Problem: Shuffling 1TB table to join with 1MB lookup table

Solution: Broadcast small tables (<100MB) to all executors. Spark: broadcast(small_df). Avoids expensive shuffle.

❌ Ignoring Query Plans

Problem: Running slow queries without understanding why

Solution: Always EXPLAIN your queries. Look for full table scans, missing partition pruning, or expensive shuffles.

❌ Memory Configuration Issues

Problem: OutOfMemory errors or excessive GC pauses

Solution: Tune executor memory and cores. Increase parallelism (more, smaller tasks). Use persist() with MEMORY_AND_DISK for large datasets.

Real-World Architecture Example

E-Commerce Analytics Platform

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   DATA FLOW ARCHITECTURE                   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                            β”‚
β”‚  Ingestion Layer:                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
β”‚  β”‚App Logs  │─────▢│  Kafka   │─────▢│  Spark   β”‚          β”‚
β”‚  β”‚Events    β”‚      β”‚ Topics   β”‚      β”‚Streaming β”‚          β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜          β”‚
β”‚                                           β”‚                β”‚
β”‚                                           β–Ό                β”‚
β”‚  Storage Layer (Data Lake):                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚  β”‚  S3 / HDFS (Partitioned Parquet)                β”‚       β”‚
β”‚  β”‚  /events/year=2024/month=03/day=15/*.parquet    β”‚       β”‚
β”‚  β”‚  - Raw events (keep 90 days)                    β”‚       β”‚
β”‚  β”‚  - Aggregated (keep 2 years)                    β”‚       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β”‚             β”‚                                              β”‚
β”‚  Processing Layer:                                         β”‚
β”‚             β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚             β–Ό                β–Ό                 β–Ό           β”‚
β”‚        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚        β”‚ Spark  β”‚      β”‚ Presto  β”‚      β”‚  Druid   β”‚       β”‚
β”‚        β”‚Batch   β”‚      β”‚Ad-hoc   β”‚      β”‚Real-time β”‚       β”‚
β”‚        β”‚ETL     β”‚      β”‚Queries  β”‚      β”‚Dashboard β”‚       β”‚
β”‚        β””β”€β”€β”€β”€β”¬β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜       β”‚
β”‚             β”‚               β”‚                β”‚             β”‚
β”‚  Serving Layer:             β”‚                β”‚             β”‚
β”‚             β–Ό               β–Ό                β–Ό             β”‚
β”‚        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚        β”‚ Data    β”‚     β”‚ Jupyter β”‚      β”‚  Grafana β”‚       β”‚
β”‚        β”‚Warehouseβ”‚     β”‚Notebooksβ”‚      β”‚Dashboard β”‚       β”‚
β”‚        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Layer Responsibilities:

Kafka: Collects 1M events/sec from web/mobile apps

Spark Streaming: Writes Parquet to S3 every 5 minutes

Spark Batch: Nightly ETL aggregations (revenue by product)

Presto: Data scientists run ad-hoc queries on S3

Druid: Powers real-time executive dashboard (sub-second)

Managed Services vs Self-Hosted

Running these systems yourself requires significant DevOps expertise. Consider managed services.

TechnologyAWSGoogle CloudAzure
Presto/TrinoAmazon Athena--
SparkEMR, GlueDataprocHDInsight, Databricks
Druid---
General OLAPRedshiftBigQuerySynapse Analytics
βœ… Managed Services
  • No cluster management
  • Auto-scaling built-in
  • Pay per query (serverless)
  • Automatic updates/patches
  • Built-in monitoring
❌ Self-Hosted
  • Full control over config
  • Lower cost at huge scale
  • No vendor lock-in
  • But requires expertise
  • 24/7 on-call needed

Key Takeaways

  • Column-oriented storage is fundamental, use Parquet/ORC, not CSV
  • Choose the right tool, Presto for ad-hoc, Spark for ETL/ML, Druid for dashboards
  • Partition intelligently, align with query patterns, avoid too many small files
  • Monitor and tune, query plans, resource allocation, and data skew
  • Use approximations, HyperLogLog, T-Digest when exact isn't needed
  • Consider managed services, unless you have dedicated ops team