背景
我们在ETL数据处理过程中,有时会遇到以下业务场景,需要将一列拆分成多列,比如将一个类型为Array的字段按元素拆分成多列,或者将类型为Map的字段按value值拆分成多列,key值为每列的列名。假设某个大学教务系统,在学期末需要将学生的多科考试成绩录入系统,起初成绩是以Array类型存储[见下图],为了便于查询统计各科成绩的各项指标,需要将Array形式的成绩列拆分成多列。
方案一
适用于Array列是静态数据,大小固定有限
spark.createDataset(Seq(("201314001", Seq(90,91,95)),("201314002", Seq(89,96,92)))).toDF("id", "scores").selectExpr("id", "scores[0] as `高数`", "scores[1] as `线代`", "scores[2] as `Java编程`").show
方案二
适用于Array列是动态数据,且数组大小较大,调用Row.fromSeq方法将数组Array类型转换成Row类型,实现多列拆分。下面针对不同类型的原始数据使用不同的拆分办法。
- 原始数据是RDD类型
import org.apache.spark.sql.types._
import org.apache.spark.sql.Row
val newRDD = spark.createDataset(Seq(("201314001", Seq(90,91,95)),("201314002", Seq(89,96,92)))).rdd.map(ele => Row.fromSeq(ele._1+:ele._2))
val newSchema = StructType(StructField("id", StringType)+:Seq("高数","线代","Java编程").map(name => StructField(s"$name", IntegerType)))
spark.createDataFrame(newRDD, newSchema).show
- 原始数据是Dataset类型(含DataFrame,参见DataFrame转Dataset方式)
import org.apache.spark.sql.types._
import org.apache.spark.sql.Row
import org.apache.spark.sql.catalyst.encoders.RowEncoder
val newSchema = StructType(StructField("id", StringType)+:Seq("高数","线代","Java编程").map(name => StructField(s"$name", DoubleType)))
spark.createDataset(Seq(("201314001", Seq(90,91,95)),("201314002", Seq(89,96,92)))).as[(String, Seq[Double])].map(ele => Row.fromSeq(ele._1+:ele._2))(RowEncoder(newSchema)).show
方案三
上述两方案是针对Array类型字段拆分成多列的有效方式,该方案针对Map类型字段做拆分。假设学生的成绩起初存储方式为Map类型,key值是各科名称,value值是各科成绩,见下图。
val curriculum = Seq("高数", "线代", "Java编程", "其它")
spark.createDataset(Seq(("201314001", Map("高数"->90, "线代"->91, "Java编程"->95)),("201314002", Map("高数"->89, "线代"->96, "Java编程"->92)))).toDF("id", "scores").selectExpr("id"+:curriculum.map(course=>s"scores['$course'] as `$course`"):_*).show