Inhaltsverzeichnis23 Abschnitte
- TL;DR
- Was ist Apache Spark?
- Spark-Environment aufsetzen
- Local Development (Single Machine)
- Deine erste SparkSession
- Apache Spark DataFrames: Die zentrale Abstraktion
- DataFrames erstellen
- Schema-Definition — Verlass dich nicht auf Inference
- Transformations: Wo die echte Arbeit passiert
- Wichtige DataFrame-Operations
- Spark SQL — was sich natürlich anfühlt
- Joins in Apache Spark
- Broadcast-Joins: Die größte einzelne Spark-Optimierung
- Eine echte Pipeline bauen
- Performance-Fallen, die jeden Anfänger erwischen
- 1. Große DataFrames zum Driver collecten
- 2. Python-UDFs nutzen wo eingebaute Functions existieren
- 3. Nicht repartitionieren vor Writes
- 4. Alles cachen
- Wann du Spark NICHT nutzen solltest
- FAQ
- Nächste Schritte
- Weiterlesen
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=Trueist 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:
| Konzept | Was es bedeutet | Warum es zählt |
|---|---|---|
| Driver | Prozess, der dein Main-Programm fährt | Koordiniert die Arbeit |
| Executors | Worker-Prozesse auf Cluster-Nodes | Verarbeiten Daten tatsächlich |
| Partitions | Chunks deiner Daten | Ermöglichen paralleles Processing |
| Lazy Evaluation | Transformations laufen nicht sofort | Spark optimiert den vollen Plan vor Execution |
| DAG | Directed Acyclic Graph von Operations | Sparks 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.partitionsauf 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=Truein 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-Typ | Wann nutzen | Vorsicht |
|---|---|---|
| Shuffle-Join (Default) | Beide Tables groß | Teuer — shuffled beide Seiten |
| Broadcast-Join | Eine Table < 100 MB | Keine großen Tables broadcasten — OOM-Risiko |
| Sort-Merge-Join | Beide groß, pre-sorted | Gut 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:
- Read mit explizitem Schema
- Clean (filter, deduplicate, cast)
- Transform (aggregate, join, compute)
- 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:
- Partitioning-Strategie lernen — wie du partitionierst, bestimmt Pipeline-Performance
- Spark Structured Streaming erkunden — gleiche DataFrame-API, aber für Streaming-Daten
- 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
- Was ist dbt und warum Data-Engineers es nutzen
- Data-Lakehouse-Architektur erklärt
- REST-API-Datenpipeline-Guide
Stand: 14. Mai 2026. Spark 3.5.x als Referenz. Versionen ändern sich — kritische Konfigs in der offiziellen Spark-Doku verifizieren.
Geschrieben von
Harbinger Team
Cloud-, Data- und AI-Engineer in DACH. Schreibt seit 2018 über infrastrukturkritische 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.