Spark 2.0如何处理列的可空性?

时间:2017-11-24 21:01:59

标签: apache-spark pyspark apache-spark-sql apache-spark-2.0

在最近发布的The Data Engineer's Guide to Apache Spark中,作者表示(第74页):

  

“...当您定义一个模式,其中所有列都被声明为不   具有空值--Spark不会强制执行该操作,并且乐意让它   该值为空值。可以为空的信号只是为了帮助   Spark SQL优化用于处理该列。如果您有空值   在不应具有空值的列中,您可能会出错   结果或看到很难调试的奇怪异常。“

在翻阅笔记和之前的JIRA时,上述陈述似乎真的不再适用。

根据SPARK-13740SPARK-15192,在DataFrame创建中定义架构时,可以强制执行可为空性。

我可以澄清一下吗?我不再确定这是什么行为。

2 个答案:

答案 0 :(得分:6)

对于null类型,不同的DataFrame创建过程的处理方式不同。它并不是那么简单,因为至少有三个不同的区域,空值的处理方式完全不同。

  1. 首先,SPARK-15192与RowEncoders有关。在RowEncoders的情况下,不允许空值,并且错误消息已得到改进。例如,在SparkSession.createDataFrame()的二十几个重载中,有很多createDataFrame()的实现基本上将RDD转换为DataFrame。 在下面的示例中,没有接受空值。所以尝试类似于使用createDateFrame()方法将RDD转换为DataFrame,如下所示,您将得到相同的结果......

    val nschema = StructType(Seq(StructField("colA", IntegerType, nullable = false), StructField("colB", IntegerType, nullable = true), StructField("colC", IntegerType, nullable = false), StructField("colD", IntegerType, nullable = true)))
    val intNullsRDD = sc.parallelize(List(org.apache.spark.sql.Row(null,null,null,null),org.apache.spark.sql.Row(2,null,null,null),org.apache.spark.sql.Row(null,3,null,null),org.apache.spark.sql.Row(null,null,null,4)))
    spark.createDataFrame(intNullsRDD, schema).show()
    
  2. 在Spark 2.1.1中,错误消息非常好。

    17/11/23 21:30:37 ERROR Executor: Exception in task 0.0 in stage 4.0 (TID 6)
    java.lang.RuntimeException: Error while encoding: java.lang.RuntimeException: The 0th field 'colA' of input row cannot be null.
    validateexternaltype(getexternalrowfield(assertnotnull(input[0, org.apache.spark.sql.Row, true], top level row object), 0, colA), IntegerType) AS colA#73
    +- validateexternaltype(getexternalrowfield(assertnotnull(input[0, org.apache.spark.sql.Row, true], top level row object), 0, colA), IntegerType)
       +- getexternalrowfield(assertnotnull(input[0, org.apache.spark.sql.Row, true], top level row object), 0, colA)
          +- assertnotnull(input[0, org.apache.spark.sql.Row, true], top level row object)
             +- input[0, org.apache.spark.sql.Row, true]
    

    单步执行代码,您可以看到发生这种情况的位置。在doGenCode()方法的下面有验证。紧接下方,当使用RowEncoder创建val encoder = RowEncoder(schema)对象时,该逻辑开始。

         @DeveloperApi
         @InterfaceStability.Evolving
         def createDataFrame(rowRDD: RDD[Row], schema: StructType): DataFrame = {
         createDataFrame(rowRDD, schema, needsConversion = true)
        }
    
        private[sql] def createDataFrame(
          rowRDD: RDD[Row],
          schema: StructType,
          needsConversion: Boolean) = {
        // TODO: use MutableProjection when rowRDD is another DataFrame and the applied
        // schema differs from the existing schema on any field data type.
        val catalystRows = if (needsConversion) {
          val encoder = RowEncoder(schema)
          rowRDD.map(encoder.toRow)
        } else {
          rowRDD.map{r: Row => InternalRow.fromSeq(r.toSeq)}
          }
          val logicalPlan = LogicalRDD(schema.toAttributes, catalystRows)(self)
          Dataset.ofRows(self, logicalPlan)
        }
    

    在逐步完成此逻辑之后,这是在objects.scala中改进的消息,这是代码处理空值的地方。实际上错误消息被传递到ctx.addReferenceObj(errMsg),但你明白了。

     case class GetExternalRowField(
        child: Expression,
        index: Int,
        fieldName: String) extends UnaryExpression with NonSQLExpression {
    
      override def nullable: Boolean = false
      override def dataType: DataType = ObjectType(classOf[Object])
      override def eval(input: InternalRow): Any =
        throw new UnsupportedOperationException("Only code-generated evaluation is supported")
    
      private val errMsg = s"The ${index}th field '$fieldName' of input row cannot be null."
    
      override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
        // Use unnamed reference that doesn't create a local field here to reduce the number of fields
        // because errMsgField is used only when the field is null.
        val errMsgField = ctx.addReferenceObj(errMsg)
        val row = child.genCode(ctx)
        val code = s"""
          ${row.code}
    
          if (${row.isNull}) {
            throw new RuntimeException("The input external row cannot be null.");
          }
    
          if (${row.value}.isNullAt($index)) {
            throw new RuntimeException($errMsgField);
          }
    
          final Object ${ev.value} = ${row.value}.get($index);
         """
        ev.copy(code = code, isNull = "false")
      }
    } 
    
    1. 从HDFS数据源拉出时会发生完全不同的事情。在这种情况下,当存在非可空列并且出现null时,将不会显示错误消息。该列仍接受空值。查看我创建的快速testFile“testFile.csv”,然后将其放入hdfs hdfs dfs -put testFile.csv /data/nullTest

         |colA|colB|colC|colD| 
         |    |    |    |    |
         |    |   2|   2|   2|
         |    |   3|    |    |
         |   4|    |    |    |
      
    2. 当我使用相同的nschema架构从下面的文件中读取时,即使该字段不可为空,所有空值也都为空。有多种方法可以用不同的方式处理空白,但这是默认设置。 csv和镶木地板都有相同的结果。

      val nschema = StructType(Seq(StructField("colA", IntegerType, nullable = true), StructField("colB", IntegerType, nullable = true), StructField("colC", IntegerType, nullable = true), StructField("colD", IntegerType, nullable = true)))
      val jListNullsADF = spark.createDataFrame(List(org.apache.spark.sql.Row(null,null,null,null),org.apache.spark.sql.Row(2,null,null,null),org.apache.spark.sql.Row(null,3,null,null),org.apache.spark.sql.Row(null,null,null,4)).asJava,nschema)
      jListNullsADF.write.format("parquet").save("/data/parquetnulltest")
      spark.read.format("parquet").schema(schema).load("/data/parquetnulltest").show()
      
      +----+----+----+----+
      |colA|colB|colC|colD|
      +----+----+----+----+
      |null|null|null|null|
      |null|   2|   2|   2|
      |null|null|   3|null|
      |null|   4|null|   4|
      +----+----+----+----+
      

      允许空值的原因始于DataFrameReader创建,其中在DataFramerReader.scala中调用baseRelationToDataFrame()。 SparkSession.scala中的baseRelationToDataFrame()在方法中使用QueryPlan类,QueryPlan正在重新创建StructType始终具有可空字段的方法fromAttributes()与原始字段基本相同,但强制为空。因此,当它返回RowEncoder()时,它现在是原始模式的可空版本。

      在DataFrameReader.scala的下方,您可以看到baseRelationToDataFrame()来电...

        @scala.annotation.varargs
        def load(paths: String*): DataFrame = {
          sparkSession.baseRelationToDataFrame(
            DataSource.apply(
              sparkSession,
              paths = paths,
              userSpecifiedSchema = userSpecifiedSchema,
              className = source,
              options = extraOptions.toMap).resolveRelation())
        }
      

      在SparkSession.scala文件的下方,您可以看到正在调用Dataset.ofRows(self: SparkSession, lr: LogicalRelation)方法,请密切关注LogicalRelation计划构造函数。

        def baseRelationToDataFrame(baseRelation: BaseRelation): DataFrame = {
          Dataset.ofRows(self, LogicalRelation(baseRelation))
        }
      

      在Dataset.scala中,分析的QueryPlan对象的schema属性作为第三个参数传递,以在new Dataset[Row](sparkSession, qe, RowEncoder(qe.analyzed.schema))中创建数据集。

        def ofRows(sparkSession: SparkSession, logicalPlan: LogicalPlan): DataFrame = {
          val qe = sparkSession.sessionState.executePlan(logicalPlan)
          qe.assertAnalyzed()
          new Dataset[Row](sparkSession, qe, RowEncoder(qe.analyzed.schema))
        }
      }
      

      在QueryPlan.scala中正在使用StructType.fromAttributes()方法

       lazy val schema: StructType = StructType.fromAttributes(output)
      

      最后在StructType.scala中,可以为null的属性始终为空。

        private[sql] def fromAttributes(attributes: Seq[Attribute]): StructType =
          StructType(attributes.map(a => StructField(a.name, a.dataType, a.nullable, a.metadata)))
      

      关于基于可空性的查询计划不同,我认为LogicalPlan完全有可能根据列是否可为空而不同。很多信息都会传递到该对象中,并且有很多后续逻辑可以实现该计划。但是,正如我们在一秒钟之前看到的那样,在实际编写数据帧时不能保持可空

      1. 第三种情况依赖于DataType。当您使用方法createDataFrame(rows: java.util.List[Row], schema: StructType)创建DataFrame时,它实际上将创建零,其中将null传递到不可为空的IntegerType字段。你可以看到下面的例子......

          val schema = StructType(Seq(StructField("colA", IntegerType, nullable = false), StructField("colB", IntegerType, nullable = true), StructField("colC", IntegerType, nullable = false), StructField("colD", IntegerType, nullable = true))) 
          val jListNullsDF = spark.createDataFrame(List(org.apache.spark.sql.Row(null,null,null,null),org.apache.spark.sql.Row(2,null,null,null),org.apache.spark.sql.Row(null,3,null,null),org.apache.spark.sql.Row(null,null,null,4)).asJava,schema)
          jListNullsDF.show() 
        
          +----+----+----+----+
          |colA|colB|colC|colD|
          +----+----+----+----+
          |   0|null|   0|null|
          |   2|null|   0|null|
          |   0|   3|   0|null|
          |   0|null|   0|   4|
          +----+----+----+----+
        
      2. 看起来org.apache.spark.sql.catalyst.expressions.BaseGenericInternalRow$class.getInt()中有逻辑用零替换零。但是,对于不可为空的StringType字段,空值不会正常处理。

           val strschema = StructType(Seq(StructField("colA", StringType, nullable = false), StructField("colB", StringType, nullable = true), StructField("colC", StringType, nullable = false), StructField("colD", StringType, nullable = true)))
           val strNullsRDD = sc.parallelize(List(org.apache.spark.sql.Row(null,null,null,null),org.apache.spark.sql.Row("r2colA",null,null,null),org.apache.spark.sql.Row(null,"r3colC",null,null),org.apache.spark.sql.Row(null,null,null,"r4colD")))
        spark.createDataFrame(List(org.apache.spark.sql.Row(null,null,null,null),org.apache.spark.sql.Row("r2cA",null,null,null),org.apache.spark.sql.Row(null,"row3cB",null,null),org.apache.spark.sql.Row(null,null,null,"row4ColD")).asJava,strschema).show()
        

        但下面是不是非常有用的错误消息,它没有指定字段的序号位置......

        java.lang.NullPointerException
          at org.apache.spark.sql.catalyst.expressions.codegen.UnsafeRowWriter.write(UnsafeRowWriter.java:210)
        

答案 1 :(得分:1)

长话短说我们不知道。确实,Spark在执行nullable属性

时变得更加严格

然而,考虑到Spark的复杂性(客户语言的数量,库的大小,用于优化的低级机制的数量,可插入的数据源以及相对较大的遗留代码池),实际上无法保证相当有限的安全检查最新版本中包含的内容涵盖了所有可能的场景。