我有一个由三列组成的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 |
+---+----+--------+----+
是否有更好的解决方案来实现最终数据帧?
答案 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]|
// +---+----+--------+----+
说明:
col1
的唯一值col1
的每个值执行汇总
df.where(df("col1") === c).groupBy("id").agg(collect_list($"col2").alias(c))
bufSize
迭代,则用persist
保存结果,这将修剪先前RDD的祖先的数据。结合先前的实现,我将为您的集群建议下一个更改:
--driver-memory 8-12GB
--executor-cores 5
:较高的值可能会导致I / O瓶颈executor-memory 8-12G
:内存太高会导致GC延迟