2020.11.23(spark-sql、复杂SQL、函数、自定义函数)

写SQL的时候都会遇到一个东西,函数。尤其写SQL的时候有两类场景,OLTP和OLAP,OLTP的时候也会写函数,但是多数情况属于时间函数、日期函数、金额函数,OLTP就是平常CRUD的时候,对着数据库发起的快速的,返回结果的。OLAP趋向于分析型的,时间粒度,OLAP会比OLTP复杂度高一点,分析的时候会卡在对数据,不是简单的一个单元格的值的修正,可能要做很多分组的值的转换,关联的操作,OLTP真正实操的时候也会将表里的东西转成视图,尽量做一些视图的物化等等。来缩减OLTP有可能做数据关联的时间复杂度。
OLAP在大数据里是经常会出现的。所以这里面函数是一个非常重要的知识点,每每说到函数,函数的分类有哪些?有3个维度。
偏向业务数据处理的:时间函数,字符串函数,日期函数,数学函数等等常规函数。
从另外一个维度来分,函数分为聚合函数,窗口函数(分成多少个分区然后做些事情),业务函数,还有一些关键的技能点,比如有一些转换的case when等等的。这是一个维度。
再从另外一个维度:系统提供的函数,用户自定义的函数。

基本数系统都会提供大量的函数,这些函数在哪?
hive也有一套函数,spark的函数基本上与它是兼容的。
org.apache.spark.sql.functions
未来工作的话应该知道spark的函数都有哪些,去哪里看。所有的函数都在这里出现了。除了它,看spark源码也可以。
下面通过实操来写找感觉:

import org.apache.spark.sql.SparkSession

object lesson06_sql_functions {
  def main(args: Array[String]): Unit = {
    val ss: SparkSession = SparkSession
      .builder()
      .appName("functions")
      .master("local")
      .getOrCreate()
      ss.sparkContext.setLogLevel("ERROR")
  }
}

一定要结合hive去练习,最终是以hive能做的范围之内去做这件事情,这节课只是尽量结合企业实战当中的比较难写的或者必须要知道的,因为在做OLAP分析的时候,偏分析的写的SQL的形式比较多一些,比如行转列,开窗,统计等等这样的写法,所以要把这节课里面的东西学会做到举一反三。
出去大数据面试上来笔试的时候,都会有类似的题出现。准备一批数据直接用最快的方式生成。

import ss.implicits._    //有了它之后,可以直接通过list来造数据
val dataDF: DataFrame = List(
  ("A", 1, 90),
  ("B", 1, 90),
  ("A", 2, 50),
  ("C", 1, 80),
  ("B", 2, 60)
).toDF("name", "class", "score")   //这笔数据的特征:谁,哪门学科,考了多少分

如果看A学生,考了第一门学科和第二门学科,分别考了90分和50分。如果看第一门学科,有三笔记录,分别来自于A B C三个人,第二个学科分别来自于A B
基于这个数据集,首先来一版最简单的事情,根据所有的原生的SQL里面,做一些分组,排序的统计。

dataDF.createTempView("users")
//写一个最简单的,在换行的时候千万别忘记空格,现在这条语句只是做了一个分组统计的事情
ss.sql("select name, " +    //要查什么东西
  "sum(score) " +   //中间要放什么逻辑
  "from users " +   //from哪张表,但现在是dataframe,差了一步将dataDF注册成一张表
  "group by name")  // //如果要做分组统计的话就group by 先按照名称 name 分组,最终要查出name,拼上一个统计内的数值
  .show()      

除了这种方式之外,如果加上排序的话,排序是必须写在所有的之后

order by name desc     //注意,order by排序后面能够接的列,是哪个维度的列?是原表的列还是结果表的列,作用在哪个作用域上?是结果!前面都执行完的结果再来次排序,所以order by只能在结果的列中挑一个,可以放name desc

这些都比较简单,但是在这里还有一个事情,第一个版本。

ss.sql("select * from users order by name,score").show()  //排序的时候可以基于一个列或多个列自由的正反序去排序,分组的时候也可以根据多个分组

它会根据name和score默认都是使用的从小到大排序的过程。这里面能不能做一件事情,期望的是名称或未来的应用场景,我可能一个是升序,一个是倒序,会有什么事情出现?在哪种场景下我们会这么去做?

select * from users order by name desc ,score
//如果没有给出分组的时候,等于做了全表的二次排序。先比较前面,再比较后面

除了系统提供的可能需要人为去处理一些事情:
关于聚合函数这块,除了有系统提供的,可能还有需要我们自己去定义的。
自己去定义的话,有两种:
第一种:udf 普通函数作用在每条记录上
第二种:是聚合函数作用在整组数据上。

先看最简单的:
ss.udf.register("ooxx",(x:Int)=>{x*10})      
//第一个参数name:String:未来在SQL里写的函数的名称     
//第二个参数:可以写一个匿名函数(最多可以是22个参数)也可以是直接给出 UserDefinedFunction 
//如果自己定义类的话就可以不是这种样例类,它的参数个数可以很多,
//或者用户定义一个聚合的函数,先写一个最简单的,只是表示有这么一个功能,可以注册一个函数自己用
ss.sql("select *,ooxx(score) as ox from users").show()
//很简单的一个SQL语句,使用了自定义的函数,使用的情况不是特别多,
//因为给出的函数就足够你玩很久了。可以看到效果是分值放大了10倍,
//这种函数不是聚合函数,是作用在每条记录上的,而且这个语句里也不适合去写聚合的操作

这是种普通的UDF函数。除了这种普通的,像sum,count,avg这种聚合的函数怎么去写?需要自己去实现写出这个过程。
先看怎么去准备它:
定义一个类:

class MyAggFun extends UserDefinedAggregateFunction   //udf是普通函数,继承的UDAF是聚合函数,并实现里面的方法

如果要做聚合,首先要明白,聚合的前置/前提要有组的概念。但是,有组的概念是必须要写group by xxx么?聚合函数是必须要作用在group by上面么?
不是的。比如我就做了一个sum,把整张表的这一列全求出来了。但是不管写不写group by,不写就默认整张表拿聚合函数,作用在列的所有值上,如果写了group by,只不过将这张表,根据group by后面列的值分成若干组,每一组数据单拿出来作用在一个函数身上,其实就是不管怎么样,最终都是一组数据作用在一个函数身上。这个函数要对这一组数据进行处理。处理的过程,就分为这么几个阶段:
组里是由一条一条的数据组成。
这个数据是以条为单位,进入聚合的操作的环节。
如果一条一条进来的话,这里面是10条还是100条它不知道,所有在计算的过程当中,就应该会有一个buffer的概念,而且不光有buffer的概念,还有进入的概念,数据进来之后,聚合函数都是自定义的了,可以让一个列参与聚合,也可以让若干个列(参数列表,可以定义一参的,也可以定义多参的)参与聚合,一条记录进入到计算内部的时候,如何识别这条记录?记录应该有自己的schema,有多少个列,每个列什么类型,下标是什么,应该按什么类型取出来,以及会准备一个buffer,而且这个buffer如果前面combiner会觉得是前面第一条直接往里放,但是还有一种情况,可能会对buffer进行一个初始化。初始化的时候,比如未来我要做平均值,只需要把分值那个列进来, 但是进来的时候,buffer里面会有几个维度?总数/个数,所以buffer里面要维护两个值的变化,一个是进来一个值放进去,再进来一个值把它加进去,另外一个维度就是计数,进来多少条记录,未来无论把buffer设计成多少个格子,等于里面是一张虚表,临时表。无论多少个格子,至少要做一个初始化,比如把前面初始化成0,计数加1就把第一条记录的分值放进去,除了buffer的初始化,还有buffer往里放的逻辑规则,如果在分布式情况下都会讨论一个问题,如果溢写,最后还涉及到一个merge合并的过程。合并完的总结果,按照buffer设置的虚表,几张虚表给他union合并起来,合并完的结果,这个函数最终要做什么事情,可能是要给它原始输出,或者多次计算,所以最后还要给他准备一个环节,数据最后是以什么方式输出的。最终的计算,前面只是过渡来累计这件事情。输出的时候也有一个需要返回的类型,最终返回的记录是要拼回到物理表/结果表的某一列里,只要是表,schema里就要知道它的类型,最后一定要给出类型的概念。大体的作为聚合计算有这么一个流程。以上是逻辑描述。再来看为什么要实现这么多方法:

//未来如果调用这个函数,往里是传一个列还是十个列,每一个列的顺序和类型,是StructType要给出的,未来会用到它,再未来update对buffer进行更新的时候,会把传参传递进来的东西变成Row,能取出几列是由schema限制的,以及它的类型
override def inputSchema: StructType = ???
//buffer怎么去用,这边要给出一个Schema
override def bufferSchema: StructType = ???
//定义最终结果的类型
override def dataType: DataType = ???
//根据你的计算逻辑,它是不是属于幂等的函数(同一批数据无论调起多少次,在相同的数据的情况下,结果相同定义为true,如果是有随机参与的,为false)
override def deterministic: Boolean = ???
//buffer的初始化过程
override def initialize(buffer: MutableAggregationBuffer): Unit = ???
//每条往里放的时候,一条为单位调用一次方法
override def update(buffer: MutableAggregationBuffer, input: Row): Unit = ???
//最终做合并的时候
override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = ???
//执行计算的结果,最终结果必须有一个类型要和dataType定义的类型挂上钩
override def evaluate(buffer: Row): Any = ???

拿场景来模拟一下,先不实现它,怎么去用它,刚才注册了一个普通的udf的函数的时候是这样定义的:

ss.udf.register("ooxx",(x:Int)=>{x*10})     

如果想定义一个聚合函数,一样:

ss.udf.register("ooxx",new MyAggFun)  //new一下上面的对象,有了之后就可以ooxx代表了    

用的时候就和正常使用函数一样使用,把上面的函数就模拟成avg聚合函数:

ss.udf.register("ooxx",new MyAggFun)  // avg
ss.sql("select name,    " +  //如果按照名称分组,最终的名称是要打印出来的
  " ooxx(score)    " +   //以前是avg,现在换成我们定义的udf函数,按照谁去求平均值 score,先找到A为一组是group by的事情,面向A这一组把90和50传进去,这一组结束了,根据里面存的一个buffer,再算结果,返回,拼到结果表的列的位置。
  "from users  " +
  "group by name  ")  //聚合函数按组来的,按照name分组
  .show()

具体实现:

class MyAggFun extends UserDefinedAggregateFunction {  
//udf是普通函数,继承的UDAF是聚合函数,并实现里面的方法
//如果combineByKey会的话,这个聚合函数过程相对容易理解
  //描述的ooxx(score)这个环节,要传递进来的参数是什么类型的,可以传一个列也可以传多个列,如果传多个列的话,要准备多个列的定义。回忆一下StructType.apply()在定义的时候需要一个集合,有可能是一列也可能是多列。Array里是一个数组,可以是一个StructField也可以是多个StructField取决于你往里面传几个,目前是一个写一个就够了。字段名是score,类型是IntegerType,不能为空
  override def inputSchema: StructType = {
    // ooxx(score)
    StructType.apply(Array(
    	StructField.apply("score",IntegerType,false)   //顺序必须和传参的顺序一样,最终是一个Struct结构体
    ))
  }
  override def bufferSchema: StructType = {
    //avg  sum / count = avg  sum和count是两类值,所以一会儿buffer是由两类东西来准备的,它的StructType需要一个array,里面分为累加sum和累加计数的,等于一张虚表,有一个sum列,类型也是IntegerType,另外一个是count。这就定义了一个buffer是有两列的,进来的数据只有一列。
    StructType.apply(Array(
      StructField.apply("sum",IntegerType,false),
      StructField.apply("count",IntegerType,false)
    ))
  }
  //最后完成DataType定义的类型是DoubleType
  override def dataType: DataType = DoubleType
  override def deterministic: Boolean = true    //是幂等的
  //初始化需要对buffer定义的虚表的每一个单格先初始化一个值,初始值的时候因为已经把buffer按照schema做好了,要做的事情就是对buffer下标为0,就是第一个列初始成0.下标为1这一列初始成0。上面定义了几个列,下面就要给这几个列赋初始值。
  override def initialize(buffer: MutableAggregationBuffer): Unit = {
    buffer(0) = 0
    buffer(1) = 0
  }
  //初始值有了之后,组内一条记录调用一次方法,最终定义的StructType就是传递的Row的类型,要把分值拿出来向buffer把分值累加到sum里,并把count加一。在buffer下标为0是sum,因为一条一条记录要调起很多次,新调起的向前加,在buffer中取老值,可以直接getInt或者get给出泛型再给出Int,给出下标0把老值取出来,再加上input是个Row类型,因为只有一列并没有很多列,所以直接getInt(0),这里全都是用下标的取法很灵活,未来上面把输入schema定义成5个列的话,其实在这个位置就是get下标0、1、2比较灵活。这样可以把sum更新。
  //接下来是count要等于老的count值加上固定1就可以了。
  override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
    //组内,一条记录调用一次
    buffer(0) = buffer.getInt(0) +  input.getInt(0)  //sum
    buffer(1) = buffer.getInt(1) + 1  //count
  }
  //update结束之后是merge把他们累加起来,累加的时候可能多次溢写,会有多个buffer,这里是面向第一个buffer,就是未来会用的结束的buffer,一切都向buffer1里去汇总,把buffer2溢写的数据向里面去做。把buffer1里的下标0,sum更新成,buffer1.getInt(0)把老值取出来,再加上溢写的buffer2的getInt(0)
  //同理再把count列也给累计起来
  override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
    buffer1(0) = buffer1.getInt(0) + buffer2.getInt(0)
    buffer1(1) = buffer1.getInt(1) + buffer2.getInt(1)
  }
  //在最终做计算的时候才要做除法那个计算,这个buffer就是曾经的buffer1累加的结果,期望做啥事?buffer.getInt(0)这是sum除以buffer.getInt(1)这是count,注意类型一定要明确,是一个Double的类型,上面再完成最后DataType定义的类型。细写的话还要判断分母是否为0,这里就略过了。
  override def evaluate(buffer: Row): Double = {  buffer.getInt(0) / buffer.getInt(1)  }

真正写自定义函数的场景不是特别多,往下看会发现写SQL挺有意思的,不比做开发简单。往下的思路走,做一件事情。一般做分析的时候会有一些奇奇怪怪的需求,先说case when有多少人用过?前面讲hive时候说过,应该大部分人都会。
比如评定一下90以上的是good,80~90的是优秀,80以下的是良好,这么一件事情应该怎么去写SQL?要写出一个SQL逻辑,在遇到每一行的时候,那个变换的列,值的变化会造成结果表会有一个随着它变化的另外一个状态。
这种场景用case when是相对能表示的:
比如:

ss.sql("select * from users")  //但是除了要去打印结果表以外还想多一个列来标识它是优秀,良好还是差,这么三个结果,应该怎么去写?
//用到了case ... when ... 
//when我们的score它小于等于100并且score大于90分的时候,then 输出一个字符串到结果的位置,比如'good'
//when score <=90 分的时候,and score>= 80 分,then 'you'
//else 'cha'   end as 列名  ox  写完了,这是case when 基本的写法
ss.sql("select * ," +
  "case " +
  "  when score <= 100 and score >=90  then 'good'  " +
  "  when score < 90  and score >= 80 then 'you' " +
  "  else 'cha' " +
  "end as ox " +
  "from users").show()

case when还可以用在哪里?如果想变换成在where条件里面,可以先达到个列,然后再group by ox ,拿到这个子查询,再group by分组,再统计他们的个数,但其实可以完全由一步SQL来写,不把它作为子查询再加一个分组的事情了。还有另外一种写法:

//在group by条件里也可以使用 case when,因为最终分组的时候不是拿的列的具体东西了,是拿上面的分值,如果分值是在某一个区间之内的给它划成一个组,就按这个组来做了。case when就把前面的照抄就可以了。这时候是按照 you good cha做的分组做的一个count。
ss.sql("select  " +
  "  case  "  +
  "  when score <= 100 and score >=90  then 'good'  " +
  "  when score < 90  and score >= 80 then 'you' " +
  "  else 'cha' " +
  "end   as ox ,"  +
  "   count(*)  " +
  "from users   " +
  "group by " +
  "  case  "  +
"  when score <= 100 and score >=90  then 'good'  " +
  "  when score < 90  and score >= 80 then 'you' " +
  "  else 'cha' " +
  "end  "
).show

除了case when这种写法之外,在做分析的时候还有没有其他分析的需求,比如说,行列转换。根据上面这张表,一个人有两列,如果想将这个人各生成独立的行,应该怎么去做?
把一行多列转成多行:
最笨的方法,字符串拼接,再split切割,再炸开的过程:

//先把两个列拼成一个列,需要加一个空格符,方便切割,再用split切割了,再用explode炸开,分成若干行了,再用name拼他们
select name,explode(split(concat(class,' ',score),' ')) as ox from users

这个时候往往还需要做一件什么事情:
这个1要变成一些字符,要做变换的话,是不是可以再加case when

select name,explode(split(concat(case when class=1 then 'AA' else 'BB' end,' ',score),' ')) as ox from users

再也不是之前写CRUD那种SQL,现在是做的OLAP分析,分析的时候这些函数用的是比较多的,如果做的是web server写这些东西,而且还要写开窗函数的话,项目会卡死,特别特别慢。所以分为OLAP和OLTP(select简单查询,疯狂在where那里变条件,避免做分组join的事情,真的要发生分组join都要做一些预计算,比如视图物化的方式来加速,尽量的少在SQL里有复杂的操作聚合查询,不要和很多的数据库建立连接然后分片的去查,再做数据的汇集。比如把一个用户一个订单的所有的维度的数据在不同库中查出来,放到一个redis里,未来做的就是取值的过程,这样才能达到速度特别特别的快,才能应付高并发,某台机子如果卡住了一个查询,后面机子就被阻塞住了),做的事情真的是有些差异的。
前面这些比较基础的先讲这么多,以上都是最最基础默认都应该会,不应该再讲的东西了。会了这些东西之后还要进一步深化的学习。把前面hive没看的看,看过的忘了再看,看过一遍的再看一遍,这是不可能跳过的环节,spark重新讲一遍么有意思了。
数据仓库ods(open data system)很多的项目组的数据可以第一步先到ods层里,它们互相去取走各自的近三天/一个月的数据都可以在里面穿插,而且可以做一下各种维度/粒度的加工。ods是贴源层的,因为俩项目组里面如果没有中台的概念,原先平台的概念都很低的时候,ods就是整个数据的贴源层。所有项目组的数据以天为单位,先去加载到ods里,或者半实时的加载到ods里面,由ods转换加工之后,再给别的项目组拿去用。这里面可以控制时间粒度是天,小时,或者原始数据,时间粒度在这里不是特别清晰。没有死板的定义,而且数据可以update,可以被变化,整合计算,这是ods贴源层,有的公司有有的公司就没有。取决于公司项目多不多。
dw是数据仓库,data warehouse,最重要的是warehouse仓库的概念,仓库就想怎么把你家里乱七八糟长得不同样的东西,但它们之间又有丝丝的关联关系,要在仓库里放好,而且你一找就能找到它,还不能破坏这些东西,所有就是数仓建模,主题等等概念,最主要的概念是粒度,因为数据每个人每天都会产生,而且数据是不能被覆盖和破坏,这是仓库的概念。比如我今天拿了一瓶红酒,明天拿了一瓶红酒,我把两瓶红酒倒在一起,未来取出来的时候是两瓶红酒了,这是不可以的。两瓶红酒是独立的放在某一个位置,而且放在一起,酒就放在那个位置我过去取就可以了。这是仓库。重点是仓库的概念,而不是数据的概念。只不过是仓库里放的数据了。所有叫数仓,这里面最关注的是颗粒度,是每天还是每月,每周,会有这样的报表/设计,做一个拉链,我取的时候,只能取出这个人,在第一周的存款余额、第二周的存款余额、还是可以取出这个月每条的,还是这十二个月每个月的,这个粒度要去设计。根据未来数仓里的数据要做什么样的分析来决定的。
dm集市。因为数仓太庞大了,整家公司的什么数据都在里面,在上层开发应用做分析的时候,基于它如果谁都作用在它身上的话速度特别慢,全连在数仓上,关联的表特别多,很麻烦。因为这个数仓要放很多东西,所有速度不会非常快。所有要出一个集市层,集市层有垂直的概念,比如主题的集市,这个集市就卖日常用品,那个集市就卖生鲜。集市可以以业务先划分出来,关联的数据从数仓先加载过来,把它们底层的硬件和环境设置的查询速度快一些,最终可以为bi和后续分析提供一个近似实时的快速的过程。这一系列的后面再讲,先把事干了,最后再想偷懒的事情。这些东西可以删掉没有,单独弄一个数据库,但做着做着就烦了,这时候软件工程学的概念,拆解一下分一下层,这些就有了。
什么叫拉链表?
比如说流水交易,每天每个人都会有流水交易,放到仓库里的时候,这个人每天的流水交易应该怎么去放?要拉链表。这个仓库会有一个时间列,这张表曾经在业务端的时候,整个这一套的外界是webserver,面向data要做update,事务这种操作。一天下来最终会有一个结果,要把数据拿到ods放到数据仓库存放的时候,最终加上一个日期列,因为有数据的时候,企业要基于信息系统做决策。怎么做的决策?如果能够看到历史当中的每一天数据的变化,可以为A用户产生一张图表,记录他每天存款的变化,根据往期同期的对比,就知道他最近要发生大的消费了,是不是可以营销贷款的事情,决策就出来了,如果再加上年份,去年1月1号到4号疯狂消费了,今年发现这个事了,是不是可以开始营销了,这是最笨的一种决策方案。数仓、仓库就是真实的历史的对数据的反应,就只是仓库,你家仓库不叫车间,车间在bi商业智能这个层次,这些东西能不能做决策,能不能干出点事情来,能不能让企业挣钱,还需要一个团队做商业智能。它们会有大批的行业经验,商业经验,根据仓库里已有的数据产生报表,来决策企业的运营方向,这整个是一个链条。集市只不过是为了让商业智能使用数仓里的数据的时候,速度能变得更快一些,没必要的数据就不放在我这个集市里。我bi分析到销售这个环节的时候有一个专门以销售这个业务为核心的所有相关的表,没关系的就不往这个集市里去放,这时候后续对接的查询的速度很快,所有bi是作用在集市上而不是数仓上,数仓就只是把数据放在里面。不做加工。不做破坏。

接着讲:
在数据分析OLAP的时候会有一些比较复杂的方式。
如果我想看的是学科里面,做一个排序排名,然后取出前三名。未来我可能OLAP的结果,跑这个SQL跑了很久,但是结果加上一个排名列之后,晚上把这个数据加工完,推送到webServer那个数据库里,第二天架构师就能基于我这个结果做CRUD快速查询了。它就不需要做分析的过程,OLAP由我来做,他那边只要做OLTP就可以了,如何让这边再多出一个列来?
比如1类里面,90,90,80这三个就要排除123来,然后2类里面50,60要排出一个12来,能把排名做出来, 有了排名之后用户想去第几名只要给出一个区间就可以了。这时候就用到了开窗函数。
在分析的时候有一个很重要的东西叫开窗,开窗函数是谁?最主要的开窗函数它依靠over,over后面有括号,前面是想做的函数是什么,最核心的是over,没有over前面的东西实现不了,这个function可以数聚合函数,也可以是条目函数,它作用在开窗,什么叫开窗?其实就是做分组,但这个分组和group by的分组不一样,group by是作用在查询的末端,过程当中,而开窗函数是趋向于查询完的结果集要再做一次分区的事情,一般里面会有partition和order两个维度,function里面可以有的row_number(),rank(),sum(),count()都可以有,这个函数可以是聚合的也可以是非聚合,举个栗子:

先说第一个版本,如果我想给按学科排出名次,只做这么一件事情,加一个名次列怎么去做
首先 select * (中间做一些处理) from users 如果group by的话,一定是纯聚合函数
什么意思,一组最后就一条,这一条要么是sum(),要么是count(),要么是avg(),要么是min(),要么是max()
只能变成一条了,但像刚才描述,这三条还在,还得划分成一组,而且还得根据分值来个排序排出名次来
还不能影响表的记录的数量,这时候group by是不可能实现的。应该怎么去做?
除了显示原有的列的名称之外,最终要在原有基础上多一个第四个列,第四个列怎么来的?
先去解决第四个列怎么划分,over()里面是partition by,根据刚刚描述,是按照学科里的去排名,所以是按学科去分区分组,partition by class,是在select的这个作用域里面,按照出来的结果里面的,按照class学科先去做分区分组,且分区分组里面的东西order by 按score来排序,且倒序。这个over是解决数据的码放和整理的过程,这个数据摆好之后,要做一件事情,就是面向摆好的数据,它需要一个函数来处理over()处理完的数据结构,这个结果集做一个什么处理?比如rank()做排名,是作用在分区里每条记录身上,而且绝对是按partition分组去做的,给它按顺序打排名,这个位置可以是rank(),也可以是sum(),count()其他的,给它来个别名,as rank 先给它跑一遍看结果找感觉。

ss.sql("select *, " +
  "rank() over (partition by class order by score desc) as rank " +
  "from users").show()

打印结果:

+----+-----+-----+----+
|name|class|score|rank|
+----+-----+-----+----+
|   A|    1|   90|   1|
|   B|    1|   90|   1|
|   C|    1|   80|   3|
|   B|    2|   60|   1|
|   A|    2|   50|   2|
+----+-----+-----+----+

不是只有Oracle,高版本之后Oracle其他东西都有,来看结果集,结果集里面看partition by class,所以class1都分在一起了,class2都分在一起了,order by score 且倒序,是不是就倒序了,首先over能完成的功能能看懂了吧,over它在做一件什么事情?partition by class分好区了,order by score 且是倒序也倒序了,over就做的这件事情,剩下前面这个函数做什么事情,换函数会有不同的效果,现在写的rank()就是打排名,看他怎么去打的,相同分数最高的打成第一名了,它是一个打排名而且分值相同的时候会并列排名排号。
除了rank还有一个东西是row_number(),row_number()只是按着顺序组里打123就可以了,但是rank里面会有相同的分数打成了相同的排名。和使用场景不一样来最终选用一个就可以了,没有谁好谁坏,喜欢谁不喜欢谁。有时候可能就需要这种方式就必须使用rank(),现在理解什么是开窗函数了。

ss.sql("select * ," +
  " rank()  over(partition by class order by score desc  )  as rank ,   " +
  " row_number()  over(partition by class order by score desc  )  as number    " +
  "from users ").show()

输出结果:

+----+-----+-----+----+------+
|name|class|score|rank|number|
+----+-----+-----+----+------+
|   A|    1|   90|   1|     1|
|   B|    1|   90|   1|     2|
|   C|    1|   80|   3|     3|
|   B|    2|   60|   1|     1|
|   A|    2|   50|   2|     2|
+----+-----+-----+----+------+

开窗函数在hive当中也是可以用支持的,那么,除了可以做这种rank(),row_number(),好多人一说开窗函数就说这两个,其实最主要的是over(),为啥?再来写一个

ss.sql("select *, " +
  "count(score) over (partition by class) as num " +
  "from users").show()

输出结果:

+----+-----+-----+---+
|name|class|score|num|
+----+-----+-----+---+
|   A|    1|   90|  3|
|   B|    1|   90|  3|
|   C|    1|   80|  3|
|   A|    2|   50|  2|
|   B|    2|   60|  2|
+----+-----+-----+---+

首先来思考这条SQL语句,有意义么?会是一个什么样的结果,select * 表当中原有的字段都会出现,又拼了一个新的字段num列,是什么东西,做的一个count(score),如果有同学说不要这么写,在后面按group by分组是不是也能统计?先写另一个版本,写完之后就知道怎么回事了。

ss.sql("select  class , count(score) as num   from users   group by class ").show()

但是接了group by的话,前面能接的列就是你group by的那个列,只有class,后面能够拼的顶多就是一个count(score),count()就是一个做计数的没有啥意义了。
一直在强调group by有一个约束:
结果表group by那组就只剩下一条记录了。
输出结果:

+-----+---+
|class|num|
+-----+---+
|    1|  3|
|    2|  2|
+-----+---+

1类学科里有3条记录,2类学科里有2条记录。前面所有信息都丢了,但是有的时候需要的是为每一个人补充,他如果报了1类科目了,那么他所报的1类科目里一共有多少条记录。每一个人都要出现他所报的科目,以及科目里有多少条记录,为每个人补充这个数值。要把它炸开,贴上去,而不是作为一个聚合的结果,最后聚合的结果可能没有意义。是要为每个人补充汇总信息。
这时候就看出来group by是做最终最终聚合汇总的,那么开窗函数其实虽然也会有count,count()如果作用在over身上,它其实是在为每一条记录补充汇总信息。这时候,A哥们报了1类科目了,它自己考了90分,1类科目里一共有3条记录,这个时候既能知道是谁报了哪个科目,考了多少分,又能知道这个科目里一共有多少条,这样的信息是最终一个带汇总的明细表。有时候希望的是这样的表,所有SQL语句没有对错,只不过把它用到哪里去了,要灵活一点,这才叫做OLAP,这和之前没做过大数据分析时候,很难去考虑这么复杂的事情。这些SQL不适用于OLTP,速度太慢了,一牵扯到partition,group,join等等的东西就会有所谓的shuffle、聚合的动作,比较消耗性能。
sql一定要练熟了,因为需求只要随随便便变一个词,你左右的写法可能就会换一下,所以SQL其实和编程一样,需要丰富的思维逻辑的,但是这个思维逻辑向编程一样不是与生俱来的,是多敲才能感悟的。

知识点到这里就结束了:
带点理论为下节课讲源码铺路。

你可能感兴趣的:(大数据)