web-dev-qa-db-fra.com

Spark Alternatives SQL à groupby / pivot / agg / collect_list en utilisant foldLeft & withColumn afin d'améliorer les performances

J'ai un Spark DataFrame composé de trois colonnes:

 id | col1 | col2 
-----------------
 x  |  p1  |  a1  
-----------------
 x  |  p2  |  b1
-----------------
 y  |  p2  |  b2
-----------------
 y  |  p2  |  b3
-----------------
 y  |  p3  |  c1

Après avoir appliqué df.groupBy("id").pivot("col1").agg(collect_list("col2")) j'obtiens le dataframe suivant (aggDF):

+---+----+--------+----+
| id|  p1|      p2|  p3|
+---+----+--------+----+
|  x|[a1]|    [b1]|  []|
|  y|  []|[b2, b3]|[c1]|
+---+----+--------+----+

Ensuite, je trouve le nom des colonnes sauf la colonne id.

val cols = aggDF.columns.filter(x => x != "id")

Après cela, j'utilise cols.foldLeft(aggDF)((df, x) => df.withColumn(x, when(size(col(x)) > 0, col(x)).otherwise(lit(null)))) pour remplacer le tableau vide par null. Les performances de ce code deviennent médiocres lorsque le nombre de colonnes augmente. De plus, j'ai le nom des colonnes de chaîne val stringColumns = Array("p1","p3"). Je souhaite obtenir la trame de données finale suivante:

+---+----+--------+----+
| id|  p1|      p2|  p3|
+---+----+--------+----+
|  x| a1 |    [b1]|null|
|  y|null|[b2, b3]| c1 |
+---+----+--------+----+

Existe-t-il une meilleure solution à ce problème afin d'obtenir la trame de données finale?

5
Abir Chokraborty

Votre code actuel paie 2 coûts de performance structurés:

  • Comme mentionné par Alexandros, vous payez 1 analyse de catalyseur par transformation DataFrame, donc si vous bouclez quelques autres centaines ou milliers de colonnes, vous remarquerez du temps passé sur le pilote avant que le travail ne soit réellement soumis. Si c'est un problème critique pour vous, vous pouvez utiliser une seule instruction select au lieu de votre foldLeft avec withColumns, mais cela ne changera pas vraiment le temps d'exécution en raison du point suivant

  • Lorsque vous utilisez une expression telle que when (). Sinon () sur des colonnes dans ce qui peut être optimisé comme une seule instruction select, le générateur de code produira une seule grande méthode traitant toutes les colonnes. Si vous avez plus de quelques centaines de colonnes, il est probable que la méthode résultante ne soit pas compilée JIT par défaut par la JVM, ce qui entraîne des performances d'exécution très lentes (la méthode max JIT est de 8k bytecode dans Hotspot).

Vous pouvez détecter si vous rencontrez le deuxième problème en inspectant les journaux de l'exécuteur et vérifier si vous voyez un AVERTISSEMENT sur une méthode trop volumineuse qui ne peut pas être JITed.

Comment essayer de résoudre cela?

1 - Changer la logique

Vous pouvez filtrer les cellules vides avant le pivot en utilisant une transformation de fenêtre

import org.Apache.spark.sql.expressions.Window

val finalDf = df
  .withColumn("count", count('col2) over Window.partitionBy('id,'col1)) 
  .filter('count > 0)
  .groupBy("id").pivot("col1").agg(collect_list("col2"))

Cela peut ou non être plus rapide en fonction de l'ensemble de données réel, car le pivot génère également une grande expression d'instruction select par lui-même, de sorte qu'il peut atteindre le grand seuil de méthode si vous rencontrez plus d'environ 500 valeurs pour col1. Vous pouvez également combiner cela avec l'option 2.

2 - Essayez de finaliser la JVM

Vous pouvez ajouter une extraJavaOption sur vos exécuteurs pour demander à la JVM d'essayer des méthodes chaudes JIT supérieures à 8 Ko.

Par exemple, ajoutez l'option --conf "spark.executor.extraJavaOptions=-XX:-DontCompileHugeMethods" sur votre spark-submit et voyez comment cela affecte le temps d'exécution du pivot.

Il est difficile de garantir une augmentation de vitesse substantielle sans plus de détails sur votre véritable ensemble de données, mais cela vaut vraiment le coup.

1
rluta

Si vous regardez https://medium.com/@manuzhang/the-hidden-cost-of-spark-withcolumn-8ffea517c015 alors vous voyez que withColumn avec un foldLeft a connu des problèmes de performances. Select est une alternative, comme indiqué ci-dessous - en utilisant des varargs.

Pas convaincu que collect_list est un problème. 1er jeu de logique que j'ai gardé aussi. pivot lance un Job pour obtenir des valeurs distinctes pour le pivotement. C'est une approche acceptée imo. Essayer de lancer la vôtre me semble inutile, mais les autres réponses peuvent me prouver le contraire ou Spark 2.4 a été amélioré.

import spark.implicits._ 
import org.Apache.spark.sql.functions._

// Your code & assumig id is only col of interest as in THIS question. More elegant than 1st posting.
val df = Seq( ("x","p1","a1"), ("x","p2","b1"), ("y","p2","b2"), ("y","p2","b3"), ("y","p3","c1")).toDF("id", "col1", "col2")
val aggDF = df.groupBy("id").pivot("col1").agg(collect_list("col2")) 
//aggDF.show(false)

val colsToSelect = aggDF.columns  // All in this case, 1st col id handled by head & tail

val aggDF2 = aggDF.select((col(colsToSelect.head) +: colsToSelect.tail.map(col => when(size(aggDF(col)) === 0,lit(null)).otherwise(aggDF(col)).as(s"$col"))):_*)
aggDF2.show(false)

retour:

+---+----+--------+----+
|id |p1  |p2      |p3  |
+---+----+--------+----+
|x  |[a1]|[b1]    |null|
|y  |null|[b2, b3]|[c1]|
+---+----+--------+----+

Aussi une belle lecture BTW: https://lansalo.com/2018/05/13/spark-how-to-add-multiple-columns-in-dataframes-and-how-not-to/ =. Les effets deviennent plus visibles avec un plus grand nombre de colonnes. À la fin, un lecteur fait un point pertinent.

Je pense que les performances sont meilleures avec l'approche choisie lorsqu'un nombre plus élevé de colonnes prévaut.

UPD: Pendant les vacances, j'ai testé les deux approches avec Spark 2.4.x avec une petite différence observable jusqu'à 1000 colonnes. Cela m'a intrigué.

0
thebluephantom