Skip to main content

Apache Spark: Distributed Data Processing & PySpark Guide

Apache Spark is the leading unified analytics engine for large-scale data processing, combining batch processing, streaming, SQL, machine learning, and graph computation in a single framework. Process petabytes across thousands of nodes with in-memory speed.

✓ In-memory processing ✓ Unified batch + streaming ✓ Catalyst optimizer ✓ Delta Lake / Iceberg support

What is Apache Spark?

Apache Spark is an open-source, distributed computing framework originally developed at UC Berkeley's AMPLab in 2009. It provides a unified API for batch processing, streaming, SQL queries, machine learning (MLlib), and graph processing (GraphX). Spark's core innovation is in-memory computation — intermediate results are kept in RAM across transformation steps rather than written to disk like Hadoop MapReduce, making it 10–100x faster for iterative workloads.

Spark can run on a local machine, a standalone cluster, YARN, Mesos, or Kubernetes. Managed services like Databricks, Amazon EMR, Google Dataproc, and Azure HDInsight run Spark at scale without cluster management overhead. PySpark is the Python API and the most widely used interface — it provides the full DataFrame, Spark SQL, and Streaming API with Python syntax.

PySpark DataFrame Quickstart

# pip install pyspark
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, sum as spark_sum, avg, count

# Initialize SparkSession
spark = SparkSession.builder     .appName("Sales Analysis")     .config("spark.sql.shuffle.partitions", "200")     .getOrCreate()

# Read data (Parquet, CSV, JSON, Delta, JDBC...)
df = spark.read.parquet("s3://my-bucket/sales/")

# DataFrame transformations (lazy — nothing executes yet)
result = (
    df
    .filter(col("status") == "completed")
    .filter(col("amount") > 0)
    .withColumn("amount_usd", col("amount") / 100)
    .groupBy("region", "product_category")
    .agg(
        spark_sum("amount_usd").alias("total_revenue"),
        avg("amount_usd").alias("avg_order_value"),
        count("order_id").alias("order_count"),
    )
    .orderBy(col("total_revenue").desc())
)

# Action triggers actual computation
result.show(20)
result.write.mode("overwrite").parquet("s3://my-bucket/output/sales_summary/")

spark.stop()

Spark SQL & Broadcast Joins

# Spark SQL — register temp view and run SQL
from pyspark.sql import SparkSession
from pyspark.sql.functions import broadcast

spark = SparkSession.builder.appName("SparkSQL").getOrCreate()

orders = spark.read.parquet("s3://bucket/orders/")
customers = spark.read.parquet("s3://bucket/customers/")

# Register as temp views
orders.createOrReplaceTempView("orders")
customers.createOrReplaceTempView("customers")

# Run standard SQL
result = spark.sql("""
    SELECT
        c.country,
        DATE_TRUNC('month', o.created_at) AS month,
        COUNT(o.order_id)                 AS orders,
        SUM(o.amount_usd)                 AS revenue
    FROM orders o
    JOIN customers c ON o.user_id = c.user_id
    WHERE o.status = 'completed'
    GROUP BY 1, 2
    ORDER BY 2 DESC, 3 DESC
""")
result.show()

# Broadcast join optimization for small dimension tables
result2 = orders.join(
    broadcast(customers),   # sends small table to all executors
    on="user_id",
    how="left"
)

Performance Optimization

# Spark performance optimization techniques
from pyspark.sql import SparkSession
from pyspark.sql.functions import col

spark = SparkSession.builder.appName("Optimization").getOrCreate()

df = spark.read.parquet("s3://bucket/events/")

# 1. Caching — keeps DataFrame in memory across multiple actions
df.cache()                     # MEMORY_AND_DISK by default
df.persist()                   # same as cache()
df.unpersist()                 # free memory when done

# 2. Repartitioning
df_balanced = df.repartition(200)                       # round-robin
df_by_key   = df.repartition(50, col("user_id"))        # hash partition
df_small    = df.coalesce(10)                           # reduce (no shuffle)

# 3. Predicate pushdown — Spark pushes filters to data source
df_filtered = spark.read     .option("pushDownPredicate", "true")     .parquet("s3://bucket/events/")     .filter(col("event_date") == "2024-01-01")

# 4. Explain plan — inspect physical plan
df_filtered.explain(mode="formatted")

# 5. Adaptive Query Execution (AQE) — Spark 3.x auto-tuning
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")

Spark vs Apache Flink vs Dask

Feature Apache Spark Apache Flink Dask
Primary use case Batch + streaming unified Low-latency streaming Scale-out Pandas/NumPy
Streaming model Micro-batch (or ContinuousProcessing) True event-by-event Batch only
Python API PySpark (full-featured) PyFlink (growing) Native Python
Managed cloud Databricks, EMR, Dataproc Kinesis, Confluent Cloud Coiled, self-hosted
SQL support Excellent (Spark SQL) Good (Flink SQL) Limited
Learning curve Medium High Low (Pandas-like)

Best Practices

Frequently Asked Questions

What is the difference between RDDs, DataFrames, and Datasets in Spark?▼

RDDs (Resilient Distributed Datasets) are the low-level API — collections of objects distributed across the cluster, processed with functional transformations (map, filter, reduce). They give full control but lack optimization. DataFrames are distributed collections of rows with named, typed columns — similar to a database table. Spark's Catalyst optimizer can optimize DataFrame queries automatically. Datasets are type-safe DataFrames available in Scala and Java (Python does not have them). For Python, always use DataFrames — they are faster than RDDs and fully optimized by Catalyst and Tungsten.

What does 'lazy evaluation' mean in Spark?▼

In Spark, transformations (filter, groupBy, join, select) are lazy — they build up a logical execution plan but do not actually run any computation. Execution only happens when an action is called (show, collect, write, count). This design allows Spark's Catalyst optimizer to examine the full transformation plan before execution and apply optimizations like predicate pushdown, projection pruning, and join reordering. It also means you can chain many transformations with no performance cost until you trigger an action.

What causes Spark shuffles and why are they expensive?▼

A shuffle occurs when data must be redistributed across partitions — typically during groupBy, join, distinct, orderBy, and repartition. Shuffled data is written to disk, transferred over the network to target executors, then read back. Shuffles are expensive because they involve disk I/O, network I/O, and serialization. Minimize shuffles by: broadcasting small tables in joins, filtering data before joining, using partition-aware operations, and setting spark.sql.shuffle.partitions to an appropriate value (default 200 is often too high for small datasets).

How does Spark's in-memory processing work and when does it spill to disk?▼

Spark stores intermediate data in executor JVM memory. Each executor has an execution memory pool (for shuffles and aggregations) and a storage memory pool (for cached RDDs/DataFrames). With Unified Memory Management (Spark 1.6+), these pools dynamically share memory. When execution memory is exhausted during a shuffle or sort, Spark spills data to local disk. Spill is far slower than in-memory but prevents OOM errors. Tune executor memory (--executor-memory), cores per executor, and the memory.fraction settings to minimize spill on large jobs.

What is Adaptive Query Execution (AQE) in Spark 3?▼

Adaptive Query Execution (AQE), introduced in Spark 3.0, allows Spark to re-optimize the query plan at runtime based on actual statistics collected during execution. It handles three main scenarios automatically: coalescing post-shuffle partitions (merging small partitions to reduce task overhead), converting sort-merge joins to broadcast joins when a table turns out to be small enough, and skew join optimization (splitting oversized partitions). Enable AQE with spark.sql.adaptive.enabled=true — it is enabled by default in Spark 3.2+.

When should I choose Spark over Pandas or Dask?▼

Use Pandas when your data fits comfortably in a single machine's RAM (typically under 20–50 GB) — it is simpler, faster to develop with, and has a richer ecosystem for exploratory analysis. Use Dask when you want Pandas-like syntax but need to process data that is too large for one machine without the operational complexity of a Spark cluster. Use Spark when you have genuinely large datasets (hundreds of GB to petabytes), need to integrate with a data lake or warehouse (Delta Lake, Iceberg, Hive), require SQL support alongside DataFrame operations, or are on a managed cluster (Databricks, EMR, Dataproc).

Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$9/mo after trial · Cancel anytime

Want the full toolkit?

Get DevPlaybook Pro

Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →