Trois API pour le même moteur, mais un seul optimiseur les comprend. Pourquoi le DataFrame gagne presque toujours, et les trois cas où le RDD reste indispensable.
Vous avez écrit votre premier word count en RDD. L’étape suivante, presque tout le monde la fait mal : on continue en RDD par habitude, alors que Spark propose depuis longtemps une API plus rapide, plus lisible, et — surtout — connue de son optimiseur. Le RDD n’est pas déprécié, mais l’utiliser par défaut coûte du temps de calcul et de la sueur au débogage.
On regarde la même tâche écrite deux fois, on compare ce qui se passe sous la surface, et on donne la seule règle simple à retenir.
Compter les occurrences de prénoms dans un fichier.
En RDD :
val counts = sc
.textFile("etudiants.txt")
.flatMap(_.split(" "))
.map(word => (word, 1))
.reduceByKey(_ + _)En DataFrame :
import org.apache.spark.sql.functions._
val counts = spark.read.text("etudiants.txt")
.select(explode(split($"value", " ")).as("prenom"))
.groupBy("prenom")
.count()Le résultat est identique. Ce qui change n’apparaît pas dans le code : c’est ce que Spark peut voir de vos intentions.
| Aspect | RDD | DataFrame | Dataset[T] |
|---|---|---|---|
| Représentation | Objets JVM opaques | Table avec schéma | Table avec schéma et type |
| Immuable | Oui | Oui | Oui |
| Tolérance aux pannes | Oui, par la lignée | Oui | Oui |
| Schéma de colonnes | Non | Oui | Oui |
| Types vérifiés à la compilation | Oui | Non | Oui |
| Optimiseur Catalyst | Aucun | Complet | Complet, sauf sur les lambdas |
| Génération de code | Non | Whole-stage codegen | Oui |
| Sérialisation | Java/Kryo, lente | Tungsten binaire | Tungsten via encodeur |
| Filtres poussés vers la source | Non | Oui (PushedFilters) | Oui |
| Niveau d’abstraction | Bas | Haut | Haut |
| Disponible en Python | Oui | Oui | Non — JVM seulement |
Deux lignes méritent d’être lues ensemble, parce qu’elles expliquent tout le reste.
Le niveau d’abstraction. C’est la différence de fond, et elle se formule en une phrase : avec un RDD vous décrivez comment traiter, avec un DataFrame vous décrivez ce que vous voulez obtenir.
RDD → « Voici exactement comment je veux travailler. »
DataFrame → « Voici le résultat voulu. Spark, choisis comment. »
Dataset → « Pareil, mais avec mes objets Scala typés. »L’optimiseur. Une map en RDD est une boîte noire : Spark ne peut pas savoir si vous filtrez, si vous réécrivez une colonne, si vous jetez les autres. Il exécute vos étapes telles quelles. En DataFrame, il lit l’expression, la simplifie, la réordonne, la fusionne avec les étapes voisines, et pousse vers la source ce qui peut l’être.
Ces deux propriétés sont liées : c’est précisément parce que vous cédez le comment que Spark peut l’optimiser. Et c’est pour cela que le RDD coûte plus cher à l’exécution — plus de CPU, plus de mémoire, plus de données lues, donc une facture cloud plus élevée sur les mêmes données.
Sur le compte de prénoms, Spark fait :
Sur un fichier de 3 lignes, la différence est invisible. Sur un lac de données de 500 Go, elle se compte en facteurs 5 à 20. Ce n’est pas un chiffre marketing : c’est la conséquence directe du codegen et du format binaire, mesurée par les benchmarks internes de Databricks depuis 2016.
Trois cas de figure — trois seulement.
mapPartitions, zipPartitions, partitionBy avec un partitioner custom. Les DataFrames s’en approchent avec repartition et coalesce, mais pour du traitement par partition avec état, le RDD reste plus direct.Dans tous les autres cas — c’est-à-dire 90 % de ce que vous écrirez — le DataFrame gagne.
C’est la seule case « non » du DataFrame dans le tableau, et elle se paie en production :
df.select("age") // fonctionne
df.select("agge") // compile parfaitement… et échoue à l'exécutionLe compilateur Scala ne voit qu’un String. L’erreur remonte à l’analyse du plan par Spark — donc après le lancement du job, parfois après vingt minutes de calcul en amont, et sur un cluster que vous payez à la seconde.
Dataset[T] supprime cette classe d’erreurs en attachant un type réel aux lignes :
case class Etudiant(nom: String, age: Int)
val ds: Dataset[Etudiant] = df.as[Etudiant]
ds.map(_.age) // vérifié à la compilation
ds.map(_.agge) // ✗ ne compile pas — erreur détectée immédiatementDataFrame est en réalité Dataset[Row] : la même classe, avec un type de ligne générique. Passer de l’un à l’autre est donc gratuit, via .as[T].
Deux limites à connaître avant d’adopter Dataset partout :
ds.map(_.nom.toUpperCase), Catalyst repasse en boîte noire pour cette étape — les expressions SQL restent optimisables, les fonctions Scala non. Sur les étapes critiques, préférez org.apache.spark.sql.functions.* : c’est le mécanisme décrit dans pourquoi votre UDF est dix fois plus lente.Dataset[T] n’existe pas en Python. Le typage à la compilation suppose un compilateur ; PySpark n’en a pas. C’est l’un des rares avantages structurels de Scala évoqués dans Scala ou PySpark.En pratique, la règle qui tient : Dataset[T] sur les frontières — ce qui entre et sort de votre pipeline, là où une erreur de colonne coûte cher — et DataFrame au milieu, là où vous enchaînez des transformations et où vous voulez tout Catalyst.
Vous héritez d’un code RDD ? La conversion est bilatérale, et le coût est faible du côté RDD → DataFrame.
// RDD -> DataFrame (avec schéma)
import spark.implicits._
val rdd: RDD[(String, Int)] = /* … */
val df = rdd.toDF("prenom", "n")
// DataFrame -> RDD (rare, mais parfois nécessaire)
val backToRdd = df.rdd // RDD[Row]Faux ami : .rdd sur un DataFrame matérialise la table en Row sur la JVM. Vous perdez Tungsten, vous perdez Catalyst, et vous payez le coût de la conversion. À ne faire qu’en connaissance de cause — pas pour « revenir à quelque chose de plus familier ».
Écrivez en DataFrame par défaut. Descendez en RDD quand vous avez une raison précise que vous savez nommer.
Si votre pipeline commence par spark.read.parquet(...) et enchaîne des select, filter, join, groupBy, agg, window — vous êtes exactement au bon endroit. Catalyst est écrit pour ce vocabulaire ; il rend chaque étape trois fois plus rapide qu’un équivalent RDD, sans que vous ayez rien à optimiser vous-même. C’est probablement l’action à plus haut retour sur investissement que vous puissiez faire sur votre code Spark aujourd’hui.
Le RDD apprend Spark, le DataFrame le fait travailler. Deux APIs pour le même moteur, un seul optimiseur — et cet optimiseur ne comprend que le langage du DataFrame. Le seul cas où revenir au RDD est raisonnable est celui où vous pouvez expliquer, en une phrase, pourquoi Catalyst ne vous servira pas. Toute autre raison — « c’est ce qu’on faisait avant », « je préfère les tuples », « c’est plus court » — est une raison d’y regarder à deux fois.
Pour aller plus loin sur le contrat lazy/eager de Spark, la transformation vs action est le compagnon direct de cet article.
Développement et déploiement de solutions de données — les premiers modules sont en accès libre.