C’è un pezzo di folklore di Spark che fa “Scala è più veloce di PySpark”. Come la maggior parte del folklore, è circa al 30% vero e al 70% ricordato male con sicurezza dal 2016. La risposta onesta nel 2026 è più o meno: PySpark e Scala Spark sono ugualmente veloci per il lavoro che la maggior parte delle persone fa davvero, e PySpark diventa lento solo in situazioni specifiche, ben capite, e sempre più evitabili.
Per sapere quale è quale, devi capire come PySpark parla con la JVM. L’architettura è genuinamente semplice una volta che la vedi, e una volta che la vedi le regole di performance si scrivono da sole.
Spark è un progetto Scala
Spark è scritto in Scala. Gira sulla JVM. L’ottimizzatore Catalyst è codice JVM. Il motore di esecuzione Tungsten (la cosa che fa whole-stage code generation, gestione della memoria off-heap, operazioni vettorizzate) è codice JVM. Ogni executor è un processo JVM. Anche i lettori di file (Parquet, ORC, Avro) sono librerie JVM.
Non c’è Python da nessuna parte in questo stack. A Spark non potrebbe importare di meno di Python.
Quindi come fa PySpark a esistere?
Py4J e il ponte
PySpark è un sottile strato Python che sta sopra l’API Scala e ci parla attraverso un ponte chiamato Py4J. Py4J è una libreria, più vecchia di Spark, originariamente un ponte generico Java/Python, che permette a un processo Python di chiamare metodi su oggetti JVM tramite un socket locale.
Quando il tuo codice PySpark fa così:
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = SparkSession.builder.appName("runehold").getOrCreate()
df = spark.read.parquet("s3://runehold/orders/")
result = (
df.filter(F.col("country") == "IT")
.groupBy("product_id")
.agg(F.sum("amount").alias("total"))
)
result.write.parquet("s3://runehold/outputs/it_totals/")
…ecco cosa sta davvero accadendo sotto il cofano:
SparkSession.builder...getOrCreate()avvia una JVM (se non ce n’è già una) e crea un oggetto JavaSparkSessional suo interno. Py4J restituisce un oggetto proxy Python che ne tiene un riferimento.spark.read.parquet(...)è un metodo Python su quel proxy che, internamente, chiama il metodo JavaDataFrameReader.parquet(...). Il risultato è unDataset<Row>Java nella JVM. Py4J restituisce un altro proxy Python.df.filter(F.col("country") == "IT")costruisce un’espressioneColumnJava, un albero di oggetti JVM che rappresenta il confronto, e chiama ilDataset.filter(Column)Java. Un altro proxy.groupBy,agg,sum,alias: tutti uguali. Ognuno costruisce un albero di espressioni JVM e ritorna un proxyDatasetJVM.result.write.parquet(...)innesca una action. Il driver passa il DAG (interamente JVM-side) a Catalyst, il piano viene ottimizzato, gli executor lo eseguono, e i file Parquet appaiono in S3.
Nota cosa non è successo: i dati non sono passati attraverso Python. Nessuna delle righe nei file Parquet ha mai toccato un processo Python. Il processo Python del driver ha costruito una descrizione di cosa fare, la descrizione è stata tradotta in espressioni JVM tramite Py4J, e la JVM ha fatto tutto il lavoro vero.
È per questo che PySpark con DataFrame e Scala Spark con DataFrame hanno essenzialmente la stessa velocità. Entrambi stanno programmando lo stesso motore JVM. Il lato Python sta solo inviando istruzioni.
L’overhead di Py4J è reale ma minuscolo: qualche microsecondo per chiamata API per inviare il messaggio e ricevere il proxy. Confrontato con un job che processa miliardi di righe su decine di executor, il chiacchiericcio di Py4J è errore di arrotondamento.
Dove Python paga davvero una tassa
Ora le situazioni in cui PySpark è genuinamente più lento di Scala. Ce ne sono tre, tutte correlate, e tutte coinvolgono dati che devono lasciare la JVM ed entrare in un processo Python.
UDF Python
Una UDF Python è una normale funzione Python che registri con Spark e chiami all’interno di un’operazione DataFrame:
from pyspark.sql.functions import udf
from pyspark.sql.types import StringType
@udf(returnType=StringType())
def normalise_country(s):
if s is None:
return None
return s.strip().upper()
df.withColumn("country_norm", normalise_country(F.col("country")))
Sembra innocente. Non lo è.
Ecco cosa succede davvero a runtime, per ogni riga, su ogni executor:
- La JVM dell’executor ha la riga in memoria JVM.
- Per eseguire la tua funzione Python, la JVM ha bisogno di Python. Spark avvia un processo Python worker accanto all’executor (uno per task in esecuzione in parallelo).
- Per ogni riga, la JVM serializza il valore della colonna (usando Pickle), lo invia su un socket locale al Python worker, il Python worker lo deserializza, esegue la tua funzione, serializza il risultato, lo rispedisce, e la JVM lo deserializza.
- Ripeti per ogni riga. Un milione di righe = un milione di andate e ritorni di serialise/socket/deserialise.
Questa è la famosa tassa di serializzazione JVM-Python, e su un dataset non banale è brutale. Un job che impiega 30 secondi con funzioni native può impiegare 30 minuti con una UDF Python che fa la trasformazione equivalente. Ho visto personalmente rallentamenti di 50x. I team di produzione hanno storie dell’orrore.
La soluzione nel 90% dei casi è non scrivere proprio una UDF Python. Il modulo pyspark.sql.functions di Spark ha centinaia di operazioni built-in: upper, trim, regexp_replace, when, coalesce, date_format, concat_ws, from_json, qualunque cosa. Girano nella JVM. Sono completamente visibili a Catalyst. Sono gratis.
L’esempio di sopra potrebbe essere riscritto come:
df.withColumn("country_norm", F.upper(F.trim(F.col("country"))))
Stesso risultato, centinaia di volte più veloce. Controlla sempre pyspark.sql.functions prima di tendere la mano verso @udf.
pandas_udf (basate su Arrow)
A volte hai genuinamente bisogno di Python: c’è una libreria solo-pandas, un modello sklearn che vuoi scorare, un pezzo di logica troppo intricato per essere espresso in funzioni native. Per quei casi, PySpark ha pandas_udf, conosciuta anche come UDF Vettorizzata.
from pyspark.sql.functions import pandas_udf
from pyspark.sql.types import DoubleType
import pandas as pd
@pandas_udf(DoubleType())
def score_orders(amounts: pd.Series) -> pd.Series:
return amounts * 1.22 + 5.0 # qualunque sia la tua logica reale
df.withColumn("scored", score_orders(F.col("amount")))
Due cose sono diverse. Primo, la tua funzione prende e restituisce una pandas.Series (o DataFrame), non valori singoli. Secondo, il ponte tra la JVM e Python usa Apache Arrow, un formato in-memory colonnare che entrambi i lati possono leggere direttamente senza serializzazione per riga.
La differenza di performance è enorme. Una pandas_udf che processa un milione di righe gira grossomodo da 10x a 100x più veloce dell’equivalente @udf semplice. Il costo è prevalentemente il costo di eseguire Python, non il costo del ponte.
La regola: se devi eseguire Python sulle righe, usa prima pandas_udf. La @udf semplice è il fallback quando non puoi. La maggior parte delle codebase PySpark moderne non usa mai UDF semplici per niente.
Operazioni RDD in PySpark
Il terzo posto in cui Python paga una tassa è quando usi l’API RDD in PySpark. Ogni operazione RDD è una lambda Python che deve effettivamente girare in un processo Python, con la stessa danza di serialise-su-socket di una UDF semplice. È per questo che la lezione precedente ha detto “usa i DataFrame” tre volte.
Gli RDD PySpark sono grossomodo un ordine di grandezza più lenti degli RDD Scala per lo stesso lavoro, perché il costo di serializzazione è per riga e non evitabile. Se hai deciso di usare gli RDD e ti serve performance, quella è una ragione vera per considerare Scala. Al di fuori di quel caso, la penalità Python è trascurabile.
Aneddoto: quale linguaggio scelgono i team veri?
In ogni team di data engineering con cui ho lavorato o parlato negli ultimi cinque anni, la scelta del linguaggio è andata sempre nello stesso modo:
- Il team conosce già Python. Pandas, NumPy, scikit-learn, requests, l’intero ecosistema.
- I data scientist conoscono già Python. Vogliono condividere notebook con il team di engineering.
- Il team della piattaforma vuole un linguaggio unico per analytics, ML, e pipeline.
- PySpark è abbastanza veloce.
Quindi il team sceglie PySpark. Non ci sono errori di compilazione Scala. Non c’è sbt. Non ci sono case class. Le nuove assunzioni leggono la codebase il primo giorno. I data scientist contribuiscono feature senza imparare un nuovo linguaggio.
Quando vince Scala? Quasi esclusivamente in tre casi:
- Legacy. Una codebase di era 2015 scritta contro l’API Scala. Riscriverla non vale la pena.
- Sviluppo di librerie. Se stai scrivendo una libreria che viaggia attraverso molte applicazioni Spark, una sorgente dati custom, una regola di ottimizzazione custom, un’estensione Catalyst, Scala è l’API nativa e te la passerai meglio.
- UDF custom pesanti che non entrano in
pandas_udf. Una piccola minoranza di job. Se il tuo hot path è una trasformazione riga per riga che ha genuinamente bisogno di velocità JVM e non può essere espressa in funzioni native o in pandas_udf basato su Arrow, scrivi una UDF Scala. La UDF Scala è un JAR che registri da PySpark, e il tuo codice Python la chiama. Non devi scrivere l’intera pipeline in Scala, solo il kernel caldo.
Non ho visto un progetto analytics greenfield scegliere Scala Spark al posto di PySpark da circa il 2020. Gli stessi esempi di Databricks sono per lo più PySpark. La maggior parte delle piattaforme Spark gestite va di default a PySpark. Il campo Scala è reale, professionale, e in calo.
Un manuale pratico per la performance di PySpark
Attaccali al muro:
- Leggi formati colonnari (Parquet, Delta, Iceberg). Spark spinge filtri e column pruning dentro il file scan; la JVM non legge mai quello che non ti serve.
- Resta nell’API DataFrame. Non scendere agli RDD a meno che tu non sappia articolare il perché.
- Usa
pyspark.sql.functionsper le trasformazioni. Ce ne sono centinaia. Leggi la pagina del modulo una volta a trimestre. - Niente
@udfsemplice. Se ne tendi la mano verso una, fermati e controlla sepyspark.sql.functionsha l’equivalente. Quasi sempre ce l’ha. - Se hai bisogno di Python sulle righe, usa
pandas_udf. Basata su Arrow, vettorizzata, 10-100x più veloce delle UDF semplici. - Profila la parte lenta con la Spark UI. Se hai seguito 1-5 e un job è ancora lento, il collo di bottiglia è il layout dei dati (skew, partizionamento, dimensione dei file) e non Python.
- Tendi la mano a Scala solo quando il rapporto costo-beneficio è chiaro. Una regola Catalyst custom, una UDF Scala calda che chiami da Python, o una migrazione legacy. Non “perché Scala è più veloce”.
Il titolo: PySpark nel 2026 è il default giusto per quasi tutti. Il divario di performance che esisteva una volta è stato in larga parte chiuso da Catalyst, Tungsten, e UDF basate su Arrow. Il vantaggio dell’ecosistema Python è solo cresciuto. L’argomento di produttività del team è schiacciante.
Fine del Modulo 1. Ora hai il modello mentale: cos’è Spark, perché esiste, l’architettura (driver, executor, cluster manager), le tre API (RDD, DataFrame, Dataset), e come PySpark si relaziona a Scala sotto. Dalla lezione 7 installiamo PySpark, configuriamo un ambiente locale, e iniziamo a scrivere codice.
References
- Apache Spark — PySpark Overview: https://spark.apache.org/docs/latest/api/python/index.html
- Apache Spark — Python User-Defined Functions: https://spark.apache.org/docs/latest/api/python/user_guide/sql/python_udf.html
- Apache Spark — Pandas API on Spark and Arrow Optimization: https://spark.apache.org/docs/latest/api/python/user_guide/sql/arrow_pandas.html
- Py4J — Python-to-Java bridge: https://www.py4j.org/
- Apache Arrow — Columnar in-memory format: https://arrow.apache.org/
Retrieved 2026-05-01.