使用foldLeft和withColumn将SQL SQL替换为groupby / pivot / agg / collect_list,以提高性能

时间:2019-12-24 00:47:42

标签: apache-spark apache-spark-sql apache-spark-dataset

我有一个由三列组成的Spark DataFrame:

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

应用df.groupBy("id").pivot("col1").agg(collect_list("col2"))之后,我得到以下数据帧(aggDF):

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

然后我找到除id列以外的其他列的名称。

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

此后,我使用cols.foldLeft(aggDF)((df, x) => df.withColumn(x, when(size(col(x)) > 0, col(x)).otherwise(lit(null))))将空数组替换为null。当列数增加时,此代码的性能会变差。另外,我还有字符串列val stringColumns = Array("p1","p3")的名称。我想获得以下最终数据框:

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

是否有更好的解决方案来实现最终数据帧?

3 个答案:

答案 0 :(得分:1)

您当前的代码需要按结构支付2项性能费用:

  • 如Alexandros所述,您为每个DataFrame转换支付1个催化剂分析,因此如果循环其他几百或数千列,您会注意到在实际提交作业之前花了一些时间在驱动程序上。如果这对您来说是一个关键问题,则可以在withColumns上使用单个select语句而不是foldLeft,但这不会因为下一点而真正改变执行时间

  • 当在可以作为单个选择语句优化的列上使用诸如when()。otherwise()之类的表达式时,代码生成器将产生一个处理所有列的大型方法。如果您有两百多列,则默认情况下,JVM可能不会对所得方法进行JIT编译,从而导致执行性能非常慢(在Hotspot中,最大可支持JIT的方法为8k字节码)。

您可以通过查看执行程序日志来检查是否遇到了第二个问题,并检查是否看到了无法进行JIT的太大警告。

如何尝试解决此问题?

1-更改逻辑

您可以使用窗口变换来过滤枢轴之前的空白单元格

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"))

根据实际数据集,这可能会更快,也可能不会更快,因为数据透视表本身还会生成一个较大的select语句表达式,因此,如果遇到col1的值超过约500,它可能会达到较大的方法阈值。 您可能还希望将其与选项2结合使用。

2-尝试完善JVM

您可以在执行程序上添加一个extraJavaOption,以要求JVM尝试使用大于8k的JIT热方法。

例如,添加选项 --conf "spark.executor.extraJavaOptions=-XX:-DontCompileHugeMethods" 在您的Spark提交上,看看它如何影响数据透视执行时间。

在真实数据集上没有更多细节的情况下,很难保证大幅提高速度,但是绝对值得一试。

答案 1 :(得分:0)

问题的标题有点不正确,这无济于事。

如果您查看https://medium.com/@manuzhang/the-hidden-cost-of-spark-withcolumn-8ffea517c015,则会发现带有foldLeft的withColumn出现性能问题。选择是替代方法,如下所示使用varargs。

不确信collect_list是一个问题。我也保持了第一套逻辑。 Pivot启动Job,以获取不同的透视值。这是imo可接受的方法。

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)

返回:

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

也不错,顺便说一句:https://lansalo.com/2018/05/13/spark-how-to-add-multiple-columns-in-dataframes-and-how-not-to/。列数越多,效果越明显。最后,读者提出了一个相关的观点。

我认为,当列数较多时,使用select方法的性能会更好。

答案 2 :(得分:0)

这是另一种方法,将pivot替换为使用join的自定义实现,并将中间结果保存在内存和/或磁盘中:

import org.apache.spark.sql.functions.{collect_list, when, size}
import org.apache.spark.sql.DataFrame

val df = Seq(
  ("x", "p1", "a1"),
  ("x", "p2", "b1"),
  ("y", "p2", "b2"),
  ("y", "p2", "b3"),
  ("y", "p3", "c1"))
.toDF("id", "col1", "col2")

val col1Values = df.select("col1").distinct().collect().map{_.getString(0)}.sorted
var finalDf : DataFrame = null
val bufSize = 2 //for 100 columns set this ~6 or lower

for((c, idx) <- col1Values.zipWithIndex){
  val tmpDf = df.where(df("col1") === c).groupBy("id").agg(collect_list($"col2").alias(c))

  if (idx == 0)
    finalDf = df.select("id")

  finalDf = finalDf.join(tmpDf.alias("tmp_df"), Seq("id"), "left").drop($"tmp_df.id")

  // you can persist here every bufSize iterations
  if ((idx + 1) % bufSize == 0)
      finalDf = finalDf.persist()

}

finalDf.dropDuplicates("id").show

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

说明:

  1. 获取col1的唯一值
  2. 对于col1的每个值执行汇总 df.where(df("col1") === c).groupBy("id").agg(collect_list($"col2").alias(c))
  3. 在每次迭代中均加入先前的结果
  4. 如果我们达到了bufSize迭代,则用persist保存结果,这将修剪先前RDD的祖先的数据。

结合先前的实现,我将为您的集群建议下一个更改:

  • --driver-memory 8-12GB
  • --executor-cores 5:较高的值可能会导致I / O瓶颈
  • executor-memory 8-12G:内存太高会导致GC延迟