Spark SQL:map操作連環報錯記錄(too many elements for tuple,ClassNotFoundException: scala.Any,<refinement>

摘要:Spark SQLScala

由于Scala限制Tuple最大元素為22個導致的血案

問題復現

實際的業務場景是使用Spark SQL加工數倉的表,由于邏輯比較復雜如果直接Spark SQL自帶的算子實現需要多好幾次groupBy和join,因此直接使用DataFrame.map算子,轉為RDD操作再轉為Dataframe一次搞定。固定格式如下

    DataFrame
      .map {
        row => {
          // TODO業務邏輯
          // 列字段輸出
          (element1, element2, element3...)
        }
      }.toDF(columnName1, columnName2, columnName3...)

最后輸出在一個Tuple中使用toDF即可完成要求,但是Scala的Tuple要求元素最大22個

scala> (1, 2, 3, 4, 5, 6, 7, 8, 9, 10,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1)
<console>:1: error: too many elements for tuple: 25, allowed: 22

Spark SQL要加工的業務字段為25個,顯然得想其他辦法去實現


初步解決方案

百度使用Array的方式解決,將所有字段包裝在一個Array中,在獲得Dataframe之后使用select($array字段名(索引))來完成,這個方法是正確,直接繞過了Tuple,以一個例子代碼試一下

val a = Seq(("a", 1), ("b", 2)).toDF("a", "b")
    .map {
      row => {
        val one: String = row.getAs[String]("a")
        val two: Int = 1
        (one, Array(one, two))
      }
    }.toDF("a", "b").select($"a", $"b"(0).as("two"), $"b"(1).as("three"))

以上例子假設one和tow分別是業務要求加工的兩個字段,執行報錯

java.lang.ClassNotFoundException: scala.Any

直接報錯在map那一行,第二個bug出現了,這個問題的原因是Array中元素類型不一致,Scala直接推斷為Any,而Array[Any]不能toDF

scala> Seq(("a", Array(1, "a")), ("b", Array(2, "b"))).toDF("a", "b")
java.lang.ClassNotFoundException: scala.Any

繼續修改將所有Array中元素改為String類型,在最后select的時候再將各別字段使用cast轉回來,新代碼如下

val a = Seq(("a", 1), ("b", 2)).toDF("a", "b")
    .map {
      row => {
        val one: String = row.getAs[String]("a")
        val two: Int = 1
        (one, Array(one, two.toString))
      }
    }.toDF("a", "b").select($"a", $"b"(0).as("two"), $"b"(1).as("three").cast("int"))

問題似乎解決了,但是實際業務代碼繼續報錯,第三個bug

java.lang.ClassNotFoundException: <refinement>

深入檢查時候發現Array中有Option[String]對象,Array中存在String和Option[String]兩種類型。因為某些業務指標有空值,所有使用了Some和None類型,因此改成了最終方案如下


最終解決方案

解決方案是引入多個Array,將同一數據類型的字段放在一個Array中,代碼如下

val a = Seq(("a", 1), ("b", 2)).toDF("a", "b")
      .map {
        row => {
          val one = row.getAs[String]("a")
          (one, Array(1, 2), Array(Some(one), None))
        }
      }.toDF("a", "b", "c").select($"a", $"b"(0).as("two"), $"c"(0).as("three"))

這樣直接避免了數據類型不一致的問題,并且在最后也不需要在轉化類型了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容