当DF列太多

时间:2016-10-29 15:17:38

标签: scala apache-spark apache-spark-sql

我正在使用Spark 1.6.1并遇到一个奇怪的行为:我在包含一些输入数据的数据帧上运行一个带有一些繁重计算(物理模拟)的UDF,并构建一个包含许多列的结果-Dataframe (〜40)。

奇怪的是,在这种情况下,我的输入数据帧的每个记录多次调用我的UDF(经常增加1.6倍),我发现这是不可接受的,因为它非常昂贵。如果我减少列数(例如减少到20),则此行为将消失。

我设法写下一个小脚本来证明这一点:

import org.apache.spark.sql.SQLContext
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.sql.functions.udf


object Demo {

  case class Result(a: Double)

  def main(args: Array[String]): Unit = {

    val sc = new SparkContext(new SparkConf().setAppName("Demo").setMaster("local[*]"))
    val sqlContext = new SQLContext(sc)
    import sqlContext.implicits._

    val numRuns = sc.accumulator(0) // to count the number of udf calls

    val myUdf = udf((i:Int) => {numRuns.add(1);Result(i.toDouble)})

    val data = sc.parallelize((1 to 100), numSlices = 5).toDF("id")

    // get results of UDF
    var results = data
      .withColumn("tmp", myUdf($"id"))
      .withColumn("result", $"tmp.a")


    // add many columns to dataframe (must depend on the UDF's result)
    for (i <- 1 to 42) {
      results=results.withColumn(s"col_$i",$"result")
    }

    // trigger action
    val res = results.collect()
    println(res.size) // prints 100

    println(numRuns.value) // prints 160

  }
}

现在,有没有办法在不减少列数的情况下解决这个问题?

3 个答案:

答案 0 :(得分:9)

我无法真正解释这种行为 - 但显然查询计划会以某种方式选择一些路径,其中一些记录会被计算两次。这意味着如果我们缓存中间结果(在应用UDF之后),我们可能会“强制”Spark不重新计算UDF。实际上,一旦添加了缓存,它就会按预期运行 - UDF被称为100次:

// get results of UDF
var results = data
  .withColumn("tmp", myUdf($"id"))
  .withColumn("result", $"tmp.a").cache()

当然,缓存有其自身的成本(内存......),但如果它保存了许多UDF调用,它可能最终会对您有所帮助。

答案 1 :(得分:7)

大约一年前我们遇到了同样的问题,花了很多时间才终于找出问题所在。

我们还有一个非常昂贵的UDF来计算,我们发现每次引用它的列时都会反复计算。它刚刚发生在我们几天前,所以我决定在这个问题上打开一个错误: SPARK-18748

我们当时也提出了一个问题,但现在我看到标题不是很好: Trying to turn a blob into multiple columns in Spark

我同意Tzach关于某种方式&#34;强迫&#34;计算UDF的计划。我们做得更好,但我们必须这样做,因为我们无法缓存()数据 - 它太大了:

val df = data.withColumn("tmp", myUdf($"id"))
val results = sqlContext.createDataFrame(df.rdd, df.schema)
             .withColumn("result", $"tmp.a")

更新

现在我看到我的jira票证已链接到另一个:SPARK-17728,它仍然没有以正确的方式处理这个问题,但它提供了一个可选的工作:

val results = data.withColumn("tmp", explode(array(myUdf($"id"))))
                  .withColumn("result", $"tmp.a")

答案 2 :(得分:1)

在较新的spark版本(2.3+)中,我们可以将UDF标记为不确定的:https://spark.apache.org/docs/latest/api/scala/org/apache/spark/sql/expressions/UserDefinedFunction.html#asNondeterministic():org.apache.spark.sql.expressions.UserDefinedFunction

即使用

val myUdf = udf(...).asNondeterministic()

这确保UDF仅被调用一次