Cloud allgemein

Apache Spark Tutorial 2026: PySpark-Pipelines von Null bis Produktion

Praktisches Apache Spark Tutorial mit PySpark-DataFrames, Transformations, Joins und einer kompletten Pipeline — inklusive Performance-Fallen, die jeden erwischen.

Harbinger Team25. März 20268 Min. LesezeitAktualisiert 14.5.2026
  • apache-spark
  • pyspark
  • data-engineering
  • spark-tutorial
  • big-data
  • etl
  • data-pipelines
Inhaltsverzeichnis23 Abschnitte

Apache Spark Tutorial 2026: PySpark-Pipelines von Null bis Produktion

Du hast einen Datensatz, der nicht mehr in pandas passt. Vielleicht 50 Millionen Zeilen, vielleicht 500 Millionen. Dein Laptop-Lüfter schreit, der Kernel crashed, und du fragst dich, ob es einen besseren Weg gibt. Gibt es — und der heißt Apache Spark.

Dieses Apache Spark Tutorial führt dich von „Ich hab schon mal von Spark gehört" zu lauffähigen Production-PySpark-Jobs. Kein Fluff, nur Code, der funktioniert.

TL;DR

  • Spark ist: Distributed Computing Engine — Daten parallel über mehrere Cores oder Cluster-Nodes.
  • Lazy Evaluation: Transformations bauen einen Plan, Actions (show, count, write) executen.
  • Performance-Killer Nr. 1: Python-UDFs statt eingebauter Functions — 10-100× langsamer.
  • Schema explizit definieren: inferSchema=True ist nur für Exploration, nicht Production.
  • Broadcast-Joins: kleine Table < 100 MB? Immer broadcast() — größter Single-Win.

Was ist Apache Spark?

Apache Spark ist eine Distributed Computing Engine, die große Datasets über einen Cluster von Maschinen prozessiert. Statt eine Transformation auf einem CPU-Core laufen zu lassen, splittet Spark die Arbeit über Dutzende oder Hunderte Cores parallel.

Die Key-Konzepte:

KonzeptWas es bedeutetWarum es zählt
DriverProzess, der dein Main-Programm fährtKoordiniert die Arbeit
ExecutorsWorker-Prozesse auf Cluster-NodesVerarbeiten Daten tatsächlich
PartitionsChunks deiner DatenErmöglichen paralleles Processing
Lazy EvaluationTransformations laufen nicht sofortSpark optimiert den vollen Plan vor Execution
DAGDirected Acyclic Graph von OperationsSparks Execution-Plan

Spark unterstützt Python (PySpark), Scala, Java und R. Dieses Tutorial nutzt PySpark — das, was die meisten Data-Engineers zuerst greifen.

Spark-Environment aufsetzen

Local Development (Single Machine)

Schnellster Weg: PySpark via pip installieren.

# Python 3.8+ nötig
pip install pyspark==3.5.1

# Installation verifizieren
pyspark --version

Deine erste SparkSession

Jedes Spark-Programm startet mit einer SparkSession — dein Entry-Point zu allem Spark.

# PySpark
from pyspark.sql import SparkSession

spark = (
    SparkSession.builder
    .appName("my-first-spark-job")
    .master("local[*]")  # alle verfügbaren Cores lokal nutzen
    .config("spark.sql.shuffle.partitions", 8)  # für Local-Dev reduzieren
    .getOrCreate()
)

print(f"Spark version: {spark.version}")
print(f"Spark UI: http://localhost:4040")

Der local[*]-Master bedeutet „alles auf dieser Maschine, alle CPU-Cores". In Production zeigst du auf einen Cluster-Manager wie YARN oder Kubernetes.

Häufiger Fehler: spark.sql.shuffle.partitions auf Default (200) während lokaler Entwicklung lassen. Bei kleinen Datasets bedeuten 200 Partitions 200 winzige Tasks mit mehr Overhead als echter Arbeit. Lokal auf 2× deine Core-Anzahl setzen.

Apache Spark DataFrames: Die zentrale Abstraktion

Wenn du pandas oder SQL kennst, fühlen sich Spark-DataFrames vertraut an. Der Unterschied: sie sind über einen Cluster verteilt.

DataFrames erstellen

# PySpark — DataFrames aus verschiedenen Quellen

# Aus einer Liste von Tupeln
data = [
    ("Berlin", "DE", 3_645_000, 891.7),
    ("Munich", "DE", 1_472_000, 310.7),
    ("Hamburg", "DE", 1_841_000, 755.2),
    ("Paris", "FR", 2_161_000, 105.4),
    ("Lyon", "FR", 516_000, 47.9),
]
columns = ["city", "country", "population", "area_km2"]

df = spark.createDataFrame(data, columns)
df.show()

# Aus CSV (in Praxis am häufigsten)
df_csv = (
    spark.read
    .option("header", True)
    .option("inferSchema", True)
    .csv("/data/cities.csv")
)

# Aus Parquet (preferred für Spark — columnar, komprimiert)
df_parquet = spark.read.parquet("/data/cities.parquet")

# Aus JSON
df_json = spark.read.json("/data/cities.json")

Schema-Definition — Verlass dich nicht auf Inference

Schema-Inference ist bequem zum Erkunden, aber gefährlich in Production. Schema immer explizit definieren.

# PySpark — explizite Schema-Definition
from pyspark.sql.types import (
    StructType, StructField, StringType, LongType, DoubleType
)

schema = StructType([
    StructField("city", StringType(), nullable=False),
    StructField("country", StringType(), nullable=False),
    StructField("population", LongType(), nullable=True),
    StructField("area_km2", DoubleType(), nullable=True),
])

df = spark.read.schema(schema).csv("/data/cities.csv", header=True)
df.printSchema()

Häufiger Fehler: inferSchema=True in Production-Pipelines. Es liest die gesamte Datei einmal nur zum Type-Raten, verdoppelt deine I/O-Kosten und rät manchmal falsch (PLZ als Integer, IDs als Double).

Transformations: Wo die echte Arbeit passiert

Spark-Transformations sind lazy — sie bauen einen Plan, executen aber nicht, bis du eine Action callst (.show(), .count(), oder .write).

Wichtige DataFrame-Operations

# PySpark — Core-Transformations
from pyspark.sql import functions as F

# Rows filtern
german_cities = df.filter(F.col("country") == "DE")

# Computed-Columns hinzufügen
df_with_density = df.withColumn(
    "density_per_km2",
    F.round(F.col("population") / F.col("area_km2"), 1)
)

# Select und Rename
df_clean = (
    df.select(
        F.col("city"),
        F.col("country"),
        F.col("population"),
        F.col("area_km2").alias("area_sqkm"),
    )
)

# Aggregation
country_stats = (
    df.groupBy("country")
    .agg(
        F.sum("population").alias("total_population"),
        F.count("city").alias("num_cities"),
        F.round(F.avg("area_km2"), 1).alias("avg_area_km2"),
    )
)
country_stats.show()

# Sort
df_sorted = df.orderBy(F.col("population").desc())

Spark SQL — was sich natürlich anfühlt

DataFrame-API und SQL mischen geht problemlos. DataFrame als Temp-View registrieren und mit SQL querien.

# PySpark + Spark SQL
df.createOrReplaceTempView("cities")

result = spark.sql("""
    SELECT
        country,
        COUNT(*) AS num_cities,
        SUM(population) AS total_pop,
        ROUND(AVG(population / area_km2), 1) AS avg_density
    FROM cities
    GROUP BY country
    HAVING SUM(population) > 1000000
    ORDER BY total_pop DESC
""")
result.show()

Meine Sicht: SQL für Ad-hoc-Exploration und komplexe Joins. DataFrame-API für Pipelines, wo Komponierbarkeit und Testing zählen. Beide compilen zum gleichen Execution-Plan.

Joins in Apache Spark

Joins sind, wo Sparks Distributed-Nature interessant wird — und wo Performance-Probleme typischerweise starten.

# PySpark — Joins
countries = spark.createDataFrame([
    ("DE", "Germany", "Europe"),
    ("FR", "France", "Europe"),
    ("US", "United States", "North America"),
], ["code", "name", "continent"])

# Inner Join (Default)
enriched = df.join(countries, df.country == countries.code, "inner")

# Left Join — alle Cities behalten, auch ohne Country-Match
enriched_left = df.join(countries, df.country == countries.code, "left")

# Broadcast Join — Spark schickt kleine Table an alle Executors
from pyspark.sql.functions import broadcast

enriched_fast = df.join(
    broadcast(countries),
    df.country == countries.code,
    "inner"
)

Broadcast-Joins: Die größte einzelne Spark-Optimierung

Wenn eine Seite eines Joins klein ist (unter ~100 MB), nutze broadcast(). Spark schickt die kleine Table zu jedem Executor und vermeidet ein teures Shuffle der großen Table.

Join-TypWann nutzenVorsicht
Shuffle-Join (Default)Beide Tables großTeuer — shuffled beide Seiten
Broadcast-JoinEine Table < 100 MBKeine großen Tables broadcasten — OOM-Risiko
Sort-Merge-JoinBeide groß, pre-sortedGut für wiederholte Joins auf gleichem Key

Häufiger Fehler: Zwei große Tables joinen, ohne zu checken, ob eine Seite klein genug zum Broadcasten ist. Mit df.count() und Size-Schätzung prüfen, bevor du Shuffle annimmst.

Eine echte Pipeline bauen

Alles zusammen. Hier eine Pipeline, die Roh-Event-Daten liest, säubert, aggregiert und das Ergebnis schreibt — Pattern, das du täglich nutzen wirst.

# PySpark — Komplette Pipeline
from pyspark.sql import SparkSession, functions as F
from pyspark.sql.types import (
    StructType, StructField, StringType, TimestampType, DoubleType, LongType
)

spark = (
    SparkSession.builder
    .appName("event-aggregation-pipeline")
    .config("spark.sql.shuffle.partitions", 16)
    .getOrCreate()
)

# 1. Schema explizit definieren
event_schema = StructType([
    StructField("event_id", StringType(), False),
    StructField("user_id", StringType(), False),
    StructField("event_type", StringType(), False),
    StructField("event_ts", TimestampType(), False),
    StructField("amount", DoubleType(), True),
    StructField("country", StringType(), True),
])

# 2. Rohdaten lesen
raw_events = (
    spark.read
    .schema(event_schema)
    .parquet("s3a://data-lake/raw/events/")
)

# 3. Säubern: Nulls entfernen, Invalid filtern, deduplizieren
cleaned = (
    raw_events
    .filter(F.col("event_type").isNotNull())
    .filter(F.col("amount") >= 0)
    .dropDuplicates(["event_id"])
    .withColumn("event_date", F.to_date("event_ts"))
)

# 4. Aggregieren: tägliches Revenue pro Land
daily_revenue = (
    cleaned
    .filter(F.col("event_type") == "purchase")
    .groupBy("event_date", "country")
    .agg(
        F.sum("amount").alias("total_revenue"),
        F.countDistinct("user_id").alias("unique_buyers"),
        F.count("event_id").alias("num_transactions"),
    )
    .withColumn(
        "avg_order_value",
        F.round(F.col("total_revenue") / F.col("num_transactions"), 2)
    )
)

# 5. Partitioniert schreiben
(
    daily_revenue
    .repartition("event_date")
    .write
    .mode("overwrite")
    .partitionBy("event_date")
    .parquet("s3a://data-lake/curated/daily_revenue/")
)

print(f"Wrote {daily_revenue.count()} aggregated rows")
spark.stop()

Diese Pipeline folgt einem Pattern, das du überall sehen wirst:

  1. Read mit explizitem Schema
  2. Clean (filter, deduplicate, cast)
  3. Transform (aggregate, join, compute)
  4. Write mit Partitioning

Performance-Fallen, die jeden Anfänger erwischen

1. Große DataFrames zum Driver collecten

# Schlecht: zieht ALLE Daten zum Driver — crashed bei großen Datasets
all_rows = df.collect()

# Gut: Sample nehmen
sample = df.limit(100).collect()

# Gut: .show() für Inspection
df.show(20, truncate=False)

2. Python-UDFs nutzen wo eingebaute Functions existieren

# Schlecht: Python-UDF — serialisiert Daten nach Python, killt Performance
from pyspark.sql.functions import udf
from pyspark.sql.types import StringType

@udf(StringType())
def upper_udf(s):
    return s.upper() if s else None

df.withColumn("city_upper", upper_udf("city"))

# Gut: eingebaute Function — läuft in JVM, 10-100× schneller
df.withColumn("city_upper", F.upper("city"))

3. Nicht repartitionieren vor Writes

# Schlecht: 200 winzige Files (eine pro Default-Shuffle-Partition)
df.write.parquet("/output/")

# Gut: Output-File-Count kontrollieren
df.repartition(4).write.parquet("/output/")

# Gut: coalesce (kein Shuffle, aber ungleichmäßige Partition-Sizes)
df.coalesce(4).write.parquet("/output/")

4. Alles cachen

# Schlecht: Dataset cachen, das du nur einmal nutzt — verschwendet Memory
df.cache()
result = df.groupBy("country").count()

# Gut: nur cachen, wenn du DataFrame mehrfach wiederverwendest
cleaned = raw.filter(...).cache()
report_a = cleaned.groupBy("country").count()
report_b = cleaned.groupBy("event_type").count()
cleaned.unpersist()  # Release wenn fertig

Wann du Spark NICHT nutzen solltest

Wichtig — Spark ist nicht immer das richtige Tool:

  • Dataset unter 10 GB? DuckDB oder pandas nutzen. Sparks Startup-Overhead lohnt sich für kleine Daten nicht.
  • Realtime, Event-by-Event? Apache Flink oder Kafka Streams erwägen. Spark Structured Streaming funktioniert für Micro-Batch, aber kein echtes Event-at-a-Time.
  • Einfache SQL-Queries auf Files? DuckDB liest Parquet direkt ohne Cluster. Zum interaktiven Erkunden lässt dich Harbinger Explorer Daten im Browser via DuckDB WASM querien — kein Cluster, kein JVM-Overhead, einfach Files reinziehen und SQL schreiben.
  • One-Off-Exploration? Ein Jupyter-Notebook mit pandas oder Polars ist schneller aufgesetzt.

Spark glänzt, wenn deine Daten zu groß für eine Maschine sind, du eine battle-tested Distributed-Engine brauchst, oder du Pipelines baust, die täglich in Production laufen.

FAQ

Spark vs DuckDB — wann was? DuckDB unter 100 GB auf einer Maschine, in-Process. Spark, wenn du mehrere Worker brauchst oder Daten nicht auf eine Box passen.

Was kostet ein Spark-Cluster? Databricks ab ~150 € pro Job (Single-Node-Compute, eu-central-1) oder ~0,15 € pro DBU. EMR ähnlich. Self-hosted auf Hetzner/eigener K8s deutlich günstiger, aber Ops-Aufwand.

PySpark vs Scala Spark — was lernen? PySpark für 95% der Data-Engineering-Aufgaben. Scala nur, wenn du Custom-Sources oder Performance-kritische UDFs schreibst.

Wie debugge ich Performance-Probleme? Spark UI (Port 4040): Stages anschauen, Shuffle-Read/Write checken, Skew identifizieren. Top-Verdächtige: ungleichmäßige Partitions oder Python-UDFs.

Nächste Schritte

Du hast die Foundation: SparkSession, DataFrames, Transformations, Joins, komplette Pipeline. Von hier:

  1. Partitioning-Strategie lernen — wie du partitionierst, bestimmt Pipeline-Performance
  2. Spark Structured Streaming erkunden — gleiche DataFrame-API, aber für Streaming-Daten
  3. Mit der Spark UI üben — DAG-Visualisierung und Stage-Metriken verstehen trennt Anfänger von Erfahrenen

Der beste Weg, Spark zu lernen: nimm ein Dataset, das zu groß für pandas ist, und bau eine Pipeline neu, die du schon mal geschrieben hast. Du siehst sofort, wo Sparks Modell anders ist und wo es excellent.

Weiterlesen

Stand: 14. Mai 2026. Spark 3.5.x als Referenz. Versionen ändern sich — kritische Konfigs in der offiziellen Spark-Doku verifizieren.

H

Geschrieben von

Harbinger Team

Cloud-, Data- und AI-Engineer in DACH. Schreibt seit 2018 über infrastruktur­kritische Tech-Entscheidungen — keine Marketing- Folien, sondern echte Trade-offs aus Production-Workloads.

Hat dir das geholfen?

Jede Woche ein neuer Artikel über DACH-Cloud, Data und AI — direkt in dein Postfach. Kein Spam, kein Marketing-Sprech.

Kein Spam. 1-Klick-Abmeldung. Datenschutz bei Loops.so.