摘要:Spark SQL
,Scala
由于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"))
這樣直接避免了數據類型不一致的問題,并且在最后也不需要在轉化類型了