前言

先说个题外话,如何给hive表增加一个列,并且该把该列的所有字段设为’China’?
如果仅仅是增加一列倒是很简单:

alter table test add columns(flag string)

可要把这个flag字段全部设置为China,看起来的确是有点难度,因为往Hive表中装载数据的唯一途径就是使用一种“大量”的数据装载操作(如何往Hive表加载数据请参考),这个时候,如果数据集中本来就没有flag对应的数据,难道非要手动把China添加上去?这种情况,可以通过静态分区就能够解决:

load data local inpath '/data/test.txt' overwrite into table test partition (flag = 'China')

有人说,这不扯淡吗?如果这个China字段,并不是我们经常需要访问的字段,何须作为分区字段呢?的确是这样的,这个时候还可以通过下面的方式来解决这个问题:

insert into table test1  select id, name,'China' as flag from test;

好了步入正题:如何向Spark的DataFrame增加一列数据

案例详解

准备数据集:

张三,23
李四,24
王五,25
赵六,26

程序入口SparkSession和加载数据代码这里不再描述:

val spark = SparkSession
      .builder()
      .appName(this.getClass.getSimpleName)
      .master(master = "local[*]")
      .getOrCreate()

    import spark.implicits._
    val df = spark.read.textFile("./data/clm")
      .map(_.split(","))
      .map(x => (x(0), x(1)))
      .toDF("name", "age")
      .cache()
  • withColumn
    这个API是数据DataSet的,官网是这么定义的:

通过添加列或替换具有相同名称的现有列来返回新的数据集
column的表达式只能引用此数据集提供的属性。 添加引用其他数据集的列是错误的

新的列只能通过现有列转换得到,这个就有点局限,不过也能解决一部分问题:
比如,我想再增加一列为所有age增加1作为新的一列:

df.withColumn("new_age", col = df("age") + 1).show()

结果:

+----+---+-------+
|name|age|new_age|
+----+---+-------+
|张三| 23|   24.0|
|李四| 24|   25.0|
|王五| 25|   26.0|
|赵六| 26|   27.0|
+----+---+-------+

那么如果我想像前言中做那样的操作怎么办?

  • 借助functions中的内置函数lit

lit函数的作用:Creates a [[Column]] of literal value. 创建[[Column]]的字面量值

df.withColumn("class",lit("一班")).show()

结果:

+----+---+-----+
|name|age|class|
+----+---+-----+
|张三| 23| 一班|
|李四| 24| 一班|
|王五| 25| 一班|
|赵六| 26| 一班|
+----+---+-----+
  • 使用sql增加默认列
df.createTempView(viewName = "view1")
import spark.sql
sql(sqlText = "select name,age,'一班' as class from view1").show()

结果:

+----+---+-----+
|name|age|class|
+----+---+-----+
|张三| 23| 一班|
|李四| 24| 一班|
|王五| 25| 一班|
|赵六| 26| 一班|
+----+---+-----+
  • 利用concat函数
sql(sqlText = "select name,age,concat('','一班') as class from view1").show()

结果:

+----+---+-----+
|name|age|class|
+----+---+-----+
|张三| 23| 一班|
|李四| 24| 一班|
|王五| 25| 一班|
|赵六| 26| 一班|
+----+---+-----+
  • 增加自增长列(类似于sql中的自增长主键)
    这里用到了functions.scala文件中的内置函数monotonically_increasing_id()

该函数官网的描述是:一个列表达式,用于生成单调递增的64位整数。但是请注意:这个自增列在分区内是连续的,但是分区间并不连续

先来个简单的使用案例:

import org.apache.spark.sql.functions._
df.withColumn("id", monotonically_increasing_id()).show()

结果:

+----+---+---+
|name|age| id|
+----+---+---+
|张三| 23|  0|
|李四| 24|  1|
|王五| 25|  2|
|赵六| 26|  3|
+----+---+---+

但是,monotonically_increasing_id() 方法生成单调递增仅仅是针对同一个分区,尽管不同分区之间生成的id都是不同的,可不同分区间id不连续,也会造成使用上面的困难,下面进行详细讲解

  • 手动分为2个分区,看结果
df.repartition(2)
      .withColumn("id", monotonically_increasing_id())
      .show()

结果:

+----+---+----------+
|name|age|        id|
+----+---+----------+
|李四| 24|         0|
|赵六| 26|         1|
|张三| 23|8589934592|
|王五| 25|8589934593|
+----+---+----------+

显然,可以看出李四和赵六为同一分区,张三和王五为另一个分区,这两个分区间id虽然不同,但是并不连续

如何解决monotonically_increasing_id()分区不连续的问题

  • 使用rdd的zipWithIndex(),这里依然手动设置为两个分区
val tmpRdd: RDD[(Row, Long)] = df.rdd.repartition(2).zipWithIndex()
    val record: RDD[Row] = tmpRdd.map(x => {
      Row(x._1.get(0), x._1.get(1), x._2)
    })
    val schema = new StructType().add("name", "string")
      .add("age", "string")
      .add("id", "long")
    spark.createDataFrame(record, schema).show()

结果:

+----+---+---+
|name|age| id|
+----+---+---+
|张三| 23|  0|
|王五| 25|  1|
|李四| 24|  2|
|赵六| 26|  3|
+----+---+---+
  • 使用row_number().over(Windo.orderBy(ColName)),生成按某列排序后,新增单调递增,连续的一列。操作完后分区数变为1。id列从1开始
val w = Window.orderBy("age")
    df.repartition(2).withColumn("id", row_number().over(w)).show()

结果:

+----+---+---+
|name|age| id|
+----+---+---+
|张三| 23|  1|
|李四| 24|  2|
|王五| 25|  3|
|赵六| 26|  4|
+----+---+---+
  • 从上面大家也能看出,monotonically_increasing_id()分区不连续,那么如果我们在计算完后通过手动将分区设置为一个,那样也就解决了分区间不联系的问题,之后再通过repartition(n)进行重分区
df.repartition(1)
      .withColumn("id", monotonically_increasing_id())
      .repartition(2)
      .show()

结果:

+----+---+---+
|name|age| id|
+----+---+---+
|张三| 23|  0|
|李四| 24|  1|
|王五| 25|  2|
|赵六| 26|  3|
+----+---+---+