Există o piesă de folclor Spark care zice „Scala e mai rapid decât PySpark.” Ca majoritatea pieselor de folclor, e cam 30% adevărată și 70% prost amintită cu încredere din 2016. Răspunsul onest în 2026 e mai degrabă: PySpark și Scala Spark sunt egal de rapide pentru munca pe care o face efectiv majoritatea oamenilor, iar PySpark devine lent doar în situații specifice, bine înțelese, din ce în ce mai evitabile.
Ca să știi care e care, trebuie să înțelegi cum vorbește PySpark cu JVM-ul. Arhitectura e cu adevărat simplă odată ce o vezi, și odată ce o vezi, regulile de performanță se scriu singure.
Spark e un proiect Scala
Spark e scris în Scala. Rulează pe JVM. Catalyst optimizer-ul e cod JVM. Engine-ul de execuție Tungsten, chestia care face whole-stage code generation, off-heap memory management, operații vectorizate, e cod JVM. Fiecare executor e un proces JVM. Chiar și cititorii de fișiere (Parquet, ORC, Avro) sunt biblioteci JVM.
Nu există Python nicăieri în stack-ul ăsta. Spark-ului îi e total indiferent de Python.
Atunci cum există PySpark?
Py4J și puntea
PySpark este un strat subțire Python care stă deasupra API-ului Scala și vorbește cu el printr-o punte numită Py4J. Py4J e o bibliotecă, mai veche decât Spark, original o punte generică Java/Python, care lasă un proces Python să apeleze metode pe obiecte JVM printr-un socket local.
Când codul tău PySpark face asta:
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/")
…iată ce se întâmplă efectiv sub capotă:
SparkSession.builder...getOrCreate()pornește un JVM (dacă nu e deja unul) și creează un obiect JavaSparkSessionîn el. Py4J returnează un obiect proxy Python care ține o referință la el.spark.read.parquet(...)e o metodă Python pe acel proxy care, intern, apelează metoda JavaDataFrameReader.parquet(...). Rezultatul este un JavaDataset<Row>în JVM. Py4J returnează un alt proxy Python.df.filter(F.col("country") == "IT")construiește o expresie JavaColumn, un arbore de obiecte JVM care reprezintă comparația, și apelează JavaDataset.filter(Column). Alt proxy.groupBy,agg,sum,alias, toate la fel. Fiecare construiește un arbore de expresii JVM și returnează un proxy JVMDataset.result.write.parquet(...)declanșează un action. Driverul predă DAG-ul (în întregime pe partea JVM) către Catalyst, planul e optimizat, executorii îl rulează și fișiere Parquet apar în S3.
Observă ce nu s-a întâmplat: datele nu au curs prin Python. Niciun rând din fișierele Parquet nu a atins vreodată un proces Python. Procesul Python al driverului a construit o descriere a ce e de făcut, descrierea a fost tradusă în expresii JVM prin Py4J, iar JVM-ul a făcut toată munca efectivă.
De aceea DataFrame PySpark și DataFrame Scala Spark sunt în esență la aceeași viteză. Ambele programează același engine JVM. Partea Python doar trimite instrucțiuni.
Overhead-ul Py4J e real, dar minuscul, câteva microsecunde per apel API ca să trimită mesajul și să primească proxy-ul. Comparat cu un job care procesează miliarde de rânduri pe zeci de executori, sporovăiala Py4J e eroare de rotunjire.
Unde plătește Python efectiv un impozit
Acum, situațiile în care PySpark e cu adevărat mai lent decât Scala. Sunt trei, toate înrudite, toate implicând date care trebuie să iasă din JVM și să intre într-un proces Python.
Python UDF-uri
Un Python UDF e o funcție Python obișnuită pe care o înregistrezi cu Spark și o apelezi în interiorul unei operații 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")))
Asta pare nevinovat. Nu este.
Iată ce se întâmplă efectiv la runtime, pentru fiecare rând, pe fiecare executor:
- JVM-ul executorului are rândul în memoria JVM.
- Ca să ruleze funcția ta Python, JVM-ul are nevoie de Python. Spark pornește un Python worker process alături de executor (unul per task care rulează în paralel).
- Pentru fiecare rând, JVM-ul serializează valoarea coloanei (folosind Pickle), o trimite printr-un socket local către Python worker, Python worker-ul o deserializează, rulează funcția ta, serializează rezultatul, îl trimite înapoi, și JVM-ul îl deserializează.
- Repetă pentru fiecare rând. Un milion de rânduri egal un milion de round-trip-uri de serialize/socket/deserialize.
Ăsta e celebrul impozit de serializare JVM-Python, și pe un dataset non-trivial e brutal. Un job care durează 30 de secunde cu funcții native poate dura 30 de minute cu un Python UDF care face transformarea echivalentă. Am văzut personal slowdown-uri de 50x. Echipele de producție au povești de groază.
Soluția în 90% din cazuri e să nu scrii un Python UDF în primul rând. Modulul pyspark.sql.functions din Spark are sute de operații built-in, upper, trim, regexp_replace, when, coalesce, date_format, concat_ws, from_json, ce vrei tu. Rulează în JVM. Sunt complet vizibile pentru Catalyst. Sunt gratis.
Exemplul de mai sus ar putea fi rescris ca:
df.withColumn("country_norm", F.upper(F.trim(F.col("country"))))
Același rezultat, de sute de ori mai rapid. Verifică mereu pyspark.sql.functions înainte să întinzi mâna după @udf.
pandas_udf (bazat pe Arrow)
Uneori chiar ai nevoie de Python, există o bibliotecă doar pentru pandas, un model sklearn pe care vrei să-l scorezi, o bucată de logică prea complicată ca să fie exprimată în funcții native. Pentru astfel de cazuri, PySpark are pandas_udf, cunoscut și ca Vectorised UDF.
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 # whatever your real logic is
df.withColumn("scored", score_orders(F.col("amount")))
Două lucruri sunt diferite. Primul, funcția ta ia și returnează un pandas.Series (sau DataFrame), nu valori singulare. Al doilea, puntea dintre JVM și Python folosește Apache Arrow, un format columnar in-memory pe care ambele părți îl pot citi direct fără serializare per-rând.
Diferența de performanță e enormă. Un pandas_udf care procesează un milion de rânduri rulează aproximativ de 10x până la 100x mai rapid decât echivalentul @udf simplu. Costul e în mare parte costul rulării Python, nu costul punții.
Regula: dacă trebuie să rulezi Python pe rânduri, folosește mai întâi pandas_udf. @udf simplu e fallback-ul când nu poți. Majoritatea codebase-urilor PySpark moderne nu folosesc niciodată UDF-uri simple.
Operații RDD în PySpark
Al treilea loc unde Python plătește un impozit e când folosești API-ul RDD în PySpark. Fiecare operație RDD e un lambda Python care trebuie efectiv să se execute într-un proces Python, cu același dans serialize-prin-socket ca un UDF simplu. De aceea lecția anterioară a zis „folosește DataFrame-uri” de trei ori.
PySpark RDD-urile sunt aproximativ cu un ordin de mărime mai lente decât Scala RDD-urile pentru aceeași muncă, fiindcă costul de serializare e per rând și inevitabil. Dacă ai decis să folosești RDD-uri și ai nevoie de performanță, ăsta e un motiv real să iei în considerare Scala. În afara acelui caz, penalizarea Python e neglijabilă.
Anecdotă: ce limbă aleg echipele reale?
În fiecare echipă de data engineering cu care am lucrat sau cu care am vorbit în ultimii cinci ani, alegerea de limbă a mers la fel:
- Echipa știe deja Python. Pandas, NumPy, scikit-learn, requests, întreg ecosistemul.
- Data scientists știu deja Python. Vor să împartă notebook-uri cu echipa de engineering.
- Echipa de platformă vrea o singură limbă pentru analytics, ML și pipeline-uri.
- PySpark e suficient de rapid.
Așa că echipa alege PySpark. Nu există erori de compilare Scala. Nu există sbt. Nu există case classes. Noii angajați citesc codebase-ul în prima zi. Data scientists contribuie cu features fără să învețe o limbă nouă.
Când câștigă Scala? Aproape exclusiv în trei cazuri:
- Legacy. Un codebase din era 2015 scris pe API-ul Scala. Rescrierea nu merită.
- Dezvoltarea de biblioteci. Dacă scrii o bibliotecă care va fi livrată în multe aplicații Spark, un data source custom, o regulă custom de optimizer, o extensie Catalyst, Scala e API-ul nativ și o vei avea mai bine.
- UDF-uri custom grele care nu încap în
pandas_udf. O minoritate mică de job-uri. Dacă hot path-ul tău e o transformare rând-cu-rând care chiar are nevoie de viteza JVM și nu poate fi exprimată în funcții native sau pandas_udf bazat pe Arrow, scrii un UDF Scala. UDF-ul Scala e un JAR pe care îl înregistrezi din PySpark, iar codul tău Python îl apelează. Nu trebuie să scrii tot pipeline-ul în Scala, doar kernel-ul fierbinte.
Nu am văzut un proiect analitic greenfield să aleagă Scala Spark peste PySpark de prin 2020 încoace. Exemplele proprii ale Databricks sunt în mare parte PySpark. Majoritatea platformelor Spark gestionate au PySpark ca implicit. Tabăra Scala e reală, profesionistă, și în scădere.
Un manual practic pentru performanța PySpark
Lipește-le pe perete:
- Citește formate columnar (Parquet, Delta, Iceberg). Spark împinge filtre și column pruning în file scan; JVM-ul nu citește niciodată ce nu îți trebuie.
- Rămâi în API-ul DataFrame. Nu coborî la RDD-uri decât dacă poți articula de ce.
- Folosește
pyspark.sql.functionspentru transformări. Sunt sute. Citește pagina modulului o dată pe trimestru. - Fără
@udfsimplu. Dacă întinzi mâna după el, oprește-te și verifică dacăpyspark.sql.functionsare echivalentul. Aproape întotdeauna are. - Dacă ai nevoie de Python pe rânduri, folosește
pandas_udf. Bazat pe Arrow, vectorizat, de 10 până la 100x mai rapid decât UDF simplu. - Profilează partea lentă cu Spark UI. Dacă ai urmat 1-5 și un job e tot lent, bottleneck-ul e layout-ul de date (skew, partiționare, dimensiunea fișierului) și nu Python.
- Întinde mâna după Scala doar când raportul cost-beneficiu e clar. O regulă custom Catalyst, un UDF Scala fierbinte pe care îl apelezi din Python sau o migrare de legacy. Nu „pentru că Scala e mai rapid.”
Concluzia: PySpark în 2026 e implicit-ul corect pentru aproape toată lumea. Diferența de performanță care exista odinioară a fost în mare parte închisă de Catalyst, Tungsten și UDF-urile bazate pe Arrow. Avantajul ecosistemului Python a crescut doar. Argumentul productivității echipei e copleșitor.
Sfârșitul Modulului 1. Ai acum modelul mental: ce e Spark, de ce există, arhitectura (driver, executors, cluster manager), cele trei API-uri (RDD, DataFrame, Dataset) și cum se raportează PySpark la Scala dedesubt. Din lecția 7 instalăm PySpark, configurăm un mediu local și începem să scriem cod.
Referințe
- 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/
Consultat 2026-05-01.