默认情况下,Spark sql模式中的可空性是建议性的。严格执行此规则的最佳方法是什么?

时间:2019-05-14 06:32:54

标签: apache-spark null schema

我正在研究一个简单的ETL项目,该项目读取CSV文件,执行 对每列进行一些修改,然后将结果写为JSON。 我想要读取我的结果的下游流程 确信我的输出符合 商定的模式,但是我的问题是,即使我定义了 我所有字段的输入模式都为nullable = false,null可以潜行 进入并损坏我的输出文件,似乎没有(高效)的方法 让Spark对我的输入字段强制执行'not null'。

这似乎是一种功能,如以下《权威指南》 Spark中所述:

  

当您定义一个架构,其中所有列均声明为不具有   null值,Spark不会强制执行此操作,并且会很乐意让null   值进入该列。可为空的信号只是为了帮助Spark   SQL优化处理该列。如果您有空值   不应该为空值的列,您可能会得到错误的结果   结果或看到难以调试的奇怪异常。

我编写了一个小检查工具来遍历数据帧的每一行,然后 如果在任何列中(在任何级别的 如果是字段或子字段(例如map,struct或array),则嵌套。)

我特别想知道:我是否已使用此检查实用程序重新插入车轮?是否有任何现有的库,或者 可以为我做到这一点的Spark技术(理想情况下比我实现的方法更好)?

下面将显示检查实用程序和管道的简化版本。如图所示, 检查实用程序已被注释掉。如果在未启用检查实用程序的情况下运行,则将在 /tmp/output.csv。

cat /tmp/output.json/*
(one + 1),(two + 1)
3,4
"",5

标题后的第二行应为数字,但为空字符串 (我猜这是spark写出null的方式。)此输出对于 读取我的ETL作业输出的下游组件:这些组件只需要整数。

现在,我可以通过取消注释行来启用检查

   //checkNulls(inDf)

执行此操作时,出现异常,通知我无效的null值并打印 删除整个违规行,如下所示:

        java.lang.RuntimeException: found null column value in row: [null,4]

Spark /权威指南中给出的一种可能的替代方法

Spark,权威指南提到了这样做的可能性:

<dataframe>.na.drop() 

但这会(AFAIK)静默删除坏记录,而不是标记坏记录。 然后,我可以在删除前后对输入进行“设置减”,但这看起来像 严重影响性能以找出什么是null和什么不是。乍一看,我会 更喜欢我的方法。。。。但是我仍然想知道是否有更好的方法。 完整的代码如下。谢谢!

package org

import java.io.PrintWriter
import org.apache.spark.SparkConf
import org.apache.spark.sql._
import org.apache.spark.sql.types._

// before running, do; rm -rf /tmp/out* /tmp/foo*
object SchemaCheckFailsToExcludeInvalidNullValue extends App {

  import NullCheckMethods._

  //val input = "2,3\n\"xxx\",4"          // this will be dropped as malformed
  val input = "2,3\n,4"                   // BUT.. this will be let through

  new PrintWriter("/tmp/foo.csv") { write(input); close }

  lazy val sparkConf = new SparkConf()
    .setAppName("Learn Spark")
    .setMaster("local[*]")
  lazy val sparkSession = SparkSession
    .builder()
    .config(sparkConf)
    .getOrCreate()
  val spark = sparkSession

  val schema = new StructType(
    Array(
      StructField("one", IntegerType, nullable = false),
      StructField("two", IntegerType, nullable = false)
    )
  )

  val inDf: DataFrame =
    spark.
      read.
      option("header", "false").
      option("mode", "dropMalformed").
      schema(schema).
      csv("/tmp/foo.csv")

  //checkNulls(inDf)

  val plusOneDf = inDf.selectExpr("one+1", "two+1")
  plusOneDf.show()

  plusOneDf.
    write.
    option("header", "true").
    csv("/tmp/output.csv")

}

object NullCheckMethods extends Serializable {

  def checkNull(columnValue: Any): Unit = {
    if (columnValue == null)
      throw new RuntimeException("got null")
    columnValue match {
      case item: Seq[_] =>
        item.foreach(checkNull)
      case item: Map[_, _] =>
        item.values.foreach(checkNull)
      case item: Row =>
        item.toSeq.foreach {
          checkNull
        }
      case default =>
        println(
          s"bad object [ $default ] of type: ${default.getClass.getName}")
    }
  }

  def checkNulls(row: Row): Unit = {
    try {
      row.toSeq.foreach {
        checkNull
      }
    } catch {
      case err: Throwable =>
        throw new RuntimeException(
          s"found null column value in row: ${row}")
    }
  }


  def checkNulls(df: DataFrame): Unit = {
    df.foreach { row => checkNulls(row) }
  }
}

1 个答案:

答案 0 :(得分:1)

您可以使用内置的Row方法anyNull拆分数据帧并以不同的方式处理这两种拆分:

val plusOneNoNulls = plusOneDf.filter(!_.anyNull)
val plusOneWithNulls = plusOneDf.filter(_.anyNull)

如果您不打算进行手动的空值处理过程,则使用内置的DataFrame.na方法会更简单,因为它已经实现了所有自动处理空值的常用方法(即使用默认值删除或填写空值)值)。