1. RDD
RDD,全称为 Resilient Distributed Datasets,即分布式数据集,是 Spark 中最基 本的数据抽象,它代表一个不可变、 可分区、里面的元素可以并行计算的集合。在 Spark 中,对数据的所有操作不外乎创建 RDD、转化已有 RDD 以及调用 RDD 操作
进行求值。每个 RDD 都被分为多个分区,这些分区运行在集群中的不同的节点上。 RDD 可以包含 Python、Java、Scala 中任意类型的对象,甚至可以包含用户自定义 的对象。RDD 具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。RDD 允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工 作集,这极大地提升查询速度。
RDD 支持两种操作:transformation 操作和 action 操作。RDD 的 transformation 操作是返回一个新的 RDD 的操作,比如 map 和 filter(),而 action 操作则是向驱动 器程序返回结果或者把结果写入外部系统的操作,比如 count()和 first()。
2. DataFrame
DataFrame 是一个分布式数据容器。相比于 RDD,DataFrame 更像传统数据库 中的二维表格,除了数据之外,还记录数据的结构信息,即 schema。同时,与 Hive 类似,DataFrame 也支持嵌套数据类型(struct,array 和 map)。从 API 易用性的角 度上看,DataFrame API 提供的是一套高层的关系操作,比函数式的 RDD API 要更 加友好,门槛更低。由于与 R 和 Pandas 中的 DataFrame 类似,Spark DataFrame 很 好地继承了传统单机数据分析的开和体验。
以Person实体为例探究
当我们以两种不同的方式读取到Person数据,Person会作为RDD的类型参数,但是Spark无法知晓内部结构,而dataFrame可以清楚加载到Person实体的内部结果和具体的数据:
RDD虽然以Person为类型参数,但是Spark框架并不了解Person类的内部结构;右侧的DataFrame却提供了详细的结构信息,使得Spark Sql可以清楚地知道数据集中有哪些列,每列的名称和类型分别是什么。
DataFrame中多了数据集的结构信息即Schema,所以我们在获取到DataFrame结果集之后可以通过printSchema方法来查看当前数据文件的结构信息。所以我们可以这样理解,RDD是分布式的Java对象集合(这也就是为什么RDD会有Broadcast广播变量的方法),DataFrame是分布式的Row对象的集合。
DataFrame 处理提供了比 RDD 更为丰富的 算子以外,更重要的是提升了执行效率、减少数据读取以及执行计划的优化,比如 filter 下推、裁剪等。
DataSet可以理解成DataFrame的一种特例,DataFrame = DataSet[Row],主要区别是DataSet每一个record存储的是一个强类型值而不是一个Row。
所以可以通过 as 方 法将 DataFrame 转换为 DataSet。Row 是一个类型,跟 Car、Person 这些类型一样, 所有的表结构信息都用 Row 来表示。
DataFrame和DataSet可以相互转化,
df.as[ElementType]这样可以把DataFrame转化为DataSet,
ds.toDF()这样可以把DataSet转化为DataFrame。
DataSet 是 DataFrame API 的一个拓展,是 Spark 最新的数据抽象。DataSet 具有 用户友好的 API 风格,既具有类型安全检查也具有 DataFrame 的查询优化特性。 DataSet 支持编解码器,当需要访问非堆上的数据时可以避免反序列化整个对象,提 高了效率。
样例类被用来在 DataSet 中定义数据的结构信息,样例类中每个属性的名称直 接映射到 DataSet 中的字段名称。
DataSet 是强类型的。比如可以有 DataSet[Car],DataSet[Person]
DataFrame 只知道字段,但是不知道字段的类型,所以在执行这些操作的时候 是没有办法在编译的时候检查是否类型失败的,比如你可以对一个 String 类型进行 加减法操作,在执行的时候才会报错,而 DataSet 不仅仅知道字段,而且知道字段 类型,所以有更为严格的错误检查。就更 JSON 对象和类对象之间的类比。
Dataset是从Spark 1.6开始引入的一个新的抽象,当时还是处于alpha版本;然而在Spark 2.0,它已经变成了稳定版了。下面是DataSet的官方定义:
A Dataset is a strongly typed collection of domain-specific objects that can be transformed
in parallel using functional or relational operations. Each Dataset also has an untyped view
called a DataFrame, which is a Dataset of Row.
Dataset是特定域对象中的强类型集合,它可以使用函数或者相关操作并行地进行转换等操作。每个Dataset都有一个称为DataFrame的非类型化的视图,这个视图是行的数据集。上面的定义看起来和RDD的定义类似,RDD的定义如下:
RDD represents an immutable,partitioned collection of elements that can be operated on in parallel
RDD也是可以并行化的操作,DataSet和RDD主要的区别是:DataSet是特定域的对象集合;然而RDD是任何对象的集合。DataSet的API总是强类型的;而且可以利用这些模式进行优化,然而RDD却不行。
Dataset的定义中还提到了DataFrame,DataFrame是特殊的Dataset,它在编译时不会对模式进行检测。在未来版本的Spark,Dataset将会替代RDD成为我们开发编程使用的API(注意,RDD并不是会被取消,而是会作为底层的API提供给用户使用)。
✎.RDD:
不支持SparkSql操作。RDD一般和spark mlib同时使用
✎.DataFrame:
1. 与RDD和Dataset不同,DataFrame每一行的类型固定为Row,只有通过解析才能获取各个字段的值,无法直接获取每一列值
testDF.foreach{
line =>
val col1=line.getAs[String]("col1")
val col2=line.getAs[String]("col2")
}
2. DataFrame和DataSet均支持SparkSql的操作,比如select,groupby之类的,还能注册临时表、进行sql语句操作
dataDF.createOrReplaceTempView("tmp")
spark.sql("select ROW,DATE from tmp where DATE is not null order by DATE").show(100,false)
3. DataFrame和DataSet支持一些方便的保存方式,比如保存成csv,可以带上表头,这样每一列的字段名一目了然:
//保存
val saveoptions = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://172.xx.xx.xx:9000/test")
datawDF.write.format("com.databricks.spark.csv").mode(SaveMode.Overwrite).options(saveoptions).save()
//读取
val options = Map("header" -> "true", "delimiter" -> "\t", "path" -> "hdfs://172.xx.xx.xx:9000/test")
val datarDF= spark.read.options(options).format("com.databricks.spark.csv").load()
✎.DataSet与DataFrame
1. DataFrame也可以叫做DataSet[Row],每一行的类型都是Row,不解析我们就无法知晓其中有哪些字段,每个字段又是什么类型。我们只能通过getAs[类型]或者row(i)的方式来获取特定的字段内容
2. 而在Dataset中,每一行的类型是不一定的,在自定义了case class之后就可以很自由的获取每一行的信息
case class Coltest(col1:String,col2:Int) extends Serializable //定义字段名和类型
/**
rdd
("a", 1)
("b", 1)
("a", 1)
* */
val test: Dataset[Coltest]=rdd.map{line=>
Coltest(line._1,line._2)
}.toDS
test.map{
line=>
println(line.col1)
println(line.col2)
}
到此为止,三者的区别就讲完了,在数据处理过程中,小编很少用到DataSet,处理文本文件使用第一种读取方式比较多,第二种读取方式一般用来读取parquet。
RDD、DataFrame、Dataset三者有许多共性,有各自适用的场景常常需要在三者之间转换
DataFrame/Dataset转RDD:
val rdd1=testDF.rdd
val rdd2=testDS.rdd
RDD转DataFrame:
import spark.implicits._
val testDF: DataFrame = rdd.map {line=>
(line._1,line._2)
}.toDF("col1","col2")
一般用元组把一行的数据写在一起,然后在toDF中指定字段名
RDD转Dataset:
import spark.implicits._
case class Person(col1:String,col2:Int)extends Serializable //定义字段名和类型
val testDS: Dataset[Person] = rdd.map {line=>
Person(line._1,line._2)
}.toDS
可以注意到,定义每一行的类型(case class)时,已经给出了字段名和类型,后面只要往case class里面添加值即可
Dataset转DataFrame:
这个也很简单,因为只是把case class封装成Row
import spark.implicits._
val testDF = testDS.toDF
DataFrame转Dataset:
import spark.implicits._
case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型
val testDS = testDF.as[Coltest]
这种方法就是在给出每一列的类型后,使用as方法,转成Dataset,这在数据类型是DataFrame又需要针对各个字段处理时极为方便
这个方式简单,但是不建议使用,因为在工作当中,使用这种方式是有限制的。
对于以前的版本来说,case class最多支持22个字段如果超过了22个字段,我们就必须要自己开发一个类,实现product接口才行。因此这种方式虽然简单,但是不通用;因为生产中的字段是非常非常多的,是不可能只有20来个字段的。
object createDF {
// 方法1 根据包括case class数据的RDD转换成DataFrame
// case class定义表的schema,case class的属性会被读取并且成为列的名字
case class Person(name: String, age: Int)
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("sparkSQLTest").setMaster("local")
val sc = new SparkContext(conf)
val sqlContext = new SQLContext(sc)
//先将RDD转化成case class 数据类型,然后再通过toDF()方法隐式转换成DataFrame
import sqlContext.implicits._
val people = sc.textFile("E:/path/data/people.txt")
.map(_.split(","))
.map(p =>Person(p(0), p(1).trim.toInt)) //将rdd的每一行都转换成了一个people
.toDF() //必须先导入import spark.implicits._ 不然这个方法会报错
people.show()
val bdf: DataFrame = boyRDD.toDF
val bdf2: DataFrame = sqlContext.createDataFrame(boyRDD)
//变成DF后就可以使用两种API进行编程了
///////////////////////////////////////////////
val people: DataFrame = sc.textFile("E:/path/data/people.txt")
.map(_.split(",")).map { line =>
(line(0).toLong, line(1))
}.toDF("id", "name")
///////////////////////////////////////////////
val rdd: RDD[(Long, String, Int, Double)] = lines.map(line => {
val fields = line.split(",")
(line(0).toLong, line(1).toString(), line(2).toInt, line(3).toDouble)
})
val testDF: DataFrame = rdd.map { line =>
(line._1, line._2)
}.toDF("id", "name")
/////////////////////////////////////////////////////
testDF.select("id").show()
/////////////////////////////////////////////
val testDS: Dataset[Boy] = rdd.map { line =>
Boy(line._1, line._2, line._3, line._4)
}.toDS
val toDS: Dataset[Boy] = bDF.as[Boy]
//DSL风格
//SparkSQL提供了一个领域特定语言(DSL)以方便操作结构化数据
personDF.select(col("name")).show
personDF.select("name").show
personDF.select("name", "age").show
//4.过滤age大于等于25的,使用filter方法过滤
ersonDF.filter(col("age") >= 25).show
personDF.filter($"age" >25).show
//5.统计年龄大于30的人数
personDF.filter(col("age")>30).count()
personDF.filter($"age" >30).count()
//6.按年龄进行分组并统计相同年龄的人数
personDF.groupBy("age").count().show
//SQL风格
//DataFrame的一个强大之处就是我们可以将它看作是一个关系型数据表,然后可以通过在程序中使用spark.sql() 来执行SQL查询,结果将作为一个DataFrame返回
//如果想使用SQL风格的语法,需要将DataFrame注册成表:
personDF.createOrReplaceTempView("t_person")
//注册成一个表
people.registerTempTable("peopleTable")
//然后可以对表进行各种操作,比如打印出13到19岁青少年的姓名
val teenagers = sqlContext.sql("SELECT name, age FROM people WHERE age >= 13 AND age <= 19") //WHERE age BETWEEN 13 AND 19
teenagers.map(t => "Name: " + t(0)).collect().foreach(println)
// teenager(0)代表第一个字段
// 取值的第一种方式:index from zero
teenagers.map(teenager => "Name: " + teenager(0)).show()
// 取值的第二种方式: byName
teenagers.map(teenager => "Name: " + teenager.getAs[String]("name") + "," + teenager.getAs[Int]("age")).show()
}
}
创建一个DataFrame,使用编程的方式 这个方式用的非常多。通过编程方式指定schema ,对于第一种方式的schema其实定义在了case class里面了。
object createDF {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("sparkSQLTest").setMaster("local")
val sc = new SparkContext(conf)
val sqlContext = new SQLContext(sc)
//step1: 从原来的 RDD 创建一个行的 RDD
val peopleRow = sc.textFile("E:/path/data/people.txt ")
.map(_.split(","))
.map(p => Row(p(0), p(1).trim))
//将数据进行整理
val rowRDD: RDD[Row] = sc.textFile("E:/path/data/people.txt ")
.map(line => {
val fields = line.split(",")
val id = fields(0).toLong
val name = fields(1)
val age = fields(2).toInt
val fv = fields(3).toDouble
Row(id, name, age, fv)
})
//step2: 创建由一个 StructType 表示的模式, 并且与第一步创建的 RDD 的行结构相匹配
//构造schema用到了两个类StructType和StructFile,其中StructFile类的三个参数分别是(字段名称,类型,数据是否可以用null填充)
val schema = StructType(Array(
StructField("name", StringType, true),
StructField("age", IntegerType, true)
))
//step3.在行 RDD 上通过 createDataFrame 方法应用模式
val people = sqlContext.createDataFrame(peopleRow, schema)
peopleDF.show()
people.registerTempTable("peopleTable")
//将临时表的数据插入hive表user
sqlContext.sql("insert into table user SELECT name, age FROM people WHERE age >= 13 AND age <= 19")
//然后可以对表进行各种操作,比如打印出13到19岁青少年的姓名
val teenagers = sqlContext.sql("SELECT name, age FROM people WHERE age >= 13 AND age <= 19")
teenagers.map(t => "Name: " + t(0)).collect().foreach(println)
}
}
1、UDF:User-defined Function,用户自定义函数。一般为单输出类型,这里以scala代码为例:
/**
* @function 自定义UDF————依照姓名字符长短倒排学生姓名,并统计姓名字符长度
* 郑重声明,scala中自定义函数需继承UDF类
*/
object UDF {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local").appName("UDF").getOrCreate()
val nameList: List[String] = List[String]("zhangsan", "lisi", "wangwu", "zhaoliu", "tianqi")
import spark.implicits._
val nameDF: DataFrame = nameList.toDF("name")
nameDF.createOrReplaceTempView("students")
nameDF.show()
spark.udf.register("STRLEN",(name:String)=>{
name.length
})
spark.sql("select name ,STRLEN(name) as length from students order by length desc").show(100)
}
}
object UDFTest {
case class Person(name: String, age: Int)
def main(args: Array[String]): Unit = {
//常见SparkSession
val sparkSession: SparkSession = SparkSession.builder().appName("DataFrameTest").master("local[2]").getOrCreate()
//根据文件获取RDD
val personRDD: RDD[String] = sparkSession.sparkContext.textFile("C:\\Users\\39402\\Desktop\\person.txt")
/**
* 注册一个udf函数,
* toString:为自定义函数的引用名,
* (str: String) => str + "我是UDF自定义函数":这个是自定义的函数体,它是一个匿名函数
*/
sparkSession.udf.register("toString", (str: String) => str + "我是UDF自定义函数")
import sparkSession.implicits._
//引入隐式转换
//利用反射将RDD转换成DataFrame
val personDF: DataFrame = personRDD.map(_.split(",")).map(line => Person(line(0), line(1).toInt)).toDF()
//将DataFrame注册成一张表
personDF.createOrReplaceTempView("person")
//利用Spark的SQL来查询数据,其中toString就是我们自定义的UDF函数
sparkSession.sql("select toString(name),age from person").show()
}
}
2、UDAF:User- Defined Aggregation Funcation,用户定义聚合函数。注意关键词“聚合”,一般为多进单出类型;有使用数据库经验的小伙伴可以思考一下avg、sum、count、max、min这五个颇具代表性的聚合函数。
这里我们依然用scala代码来完成任务。
依然是统计学生(但例子与上面UDF并不相同),UDAF与UDF不同,它必须继承UserDefinedAggregateFunction,并重写以下8个方法:
object MyCustomUDAF extends UserDefinedAggregateFunction {
//:: Nil 作用就是为StructField常见Array集合,并放入进去
def inputSchema: StructType = StructType(StructField("age", IntegerType) :: Nil)
//输入数据类型
override def inputSchema: StructType = StructType(List(
//给输入变量定义变量名 和 数据类型
StructField("value", DoubleType)
))
//缓存字段类型,也就是每个分区的共享变量
def bufferSchema: StructType = StructType(
StructField("sum", IntegerType) ::StructField("count", IntegerType) :: Nil)
//中间结果的数据类型
override def bufferSchema: StructType = StructType(
List(
StructField("product", DoubleType), //相乘之后的积
StructField("counts", LongType) //参与运算的数字总数
))
//UDF输出数据类型
def dataType: DataType = IntegerType
//输入类型和输出类型是否一致
def deterministic: Boolean = true
//初始化分区中的共享变量
def initialize(buffer: MutableAggregationBuffer): Unit = {
//为每个分组的数据执行初始化值
buffer(0) = 0 //初始化每个分区上的年龄总和为0
buffer(1) = 0 //初始化每个分区上的人数为0
}
// 每个组,有新的值进来的时候,进行分组对应的聚合值的计算
//局部聚合 就是每个分区内的运算,每个分区中每一条记录,聚合的时候需要调用该方法
def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
//将新输入进来的数据一之前合并的结果做聚合操作,
//buffer(0)就是上边定义的年龄总和sum,也就是每个分区上的年龄总和
buffer(0) = buffer.getInt(0) + input.getInt(0)
//buffer(1)就是上边定义的人的个数count,也就是每个分区上的人个数
buffer(1) = buffer.getInt(1) + 1
}
// 全局聚合,对分区结果进行合并
def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
// buffer1(0)就是所有分区的年龄总和
//下标为0的就是年龄总和
buffer1(0) = buffer1.getInt(0) + buffer2.getInt(0) //就是将没分区上的年龄相加
//buffer(1)就是所有分区的人个数
//下标为1就是人的个数
buffer1(1) = buffer1.getInt(1) + buffer2.getInt(1) // 就是将每个分区人个数聚合在一起,
}
//最终结算结果
def evaluate(buffer: Row): Any = {
buffer.getInt(0) / buffer.getInt(1)
}
}
object MyCustomUDAFMain {
def main(args: Array[String]): Unit = {
val sparkSession: SparkSession = SparkSession.builder().appName("DataFrameTest").master("local[2]").getOrCreate()
//根据文件获取RDD
val personRDD: RDD[String] = sparkSession.sparkContext.textFile("C:\\Users\\39402\\Desktop\\person.txt")
import sparkSession.implicits._
//引入隐式转换
//利用反射将RDD转换成DataFrame
val personDF: DataFrame = personRDD.map(_.split(",")).map(line => Person(line(0), line(1).toInt)).toDF()
sparkSession.udf.register("myCustomUDAF", MyCustomUDAF)
personDF.createOrReplaceTempView("person")
/**
* 输出结果为:15
*/
sparkSession.sql("select myCustomUDAF(age) from person").show()
}
}