• 【SparkSQL】数据的加载和保存、项目实战


    一 数据的加载和保存

    1 通用的加载和保存方式

    SparkSQL提供了通用的保存数据和数据加载的方式。这里的通用指的是使用相同的API,根据不同的参数读取和保存不同格式的数据,SparkSQL默认读取和保存的文件格式为parquet

    (1)加载数据

    spark.read.load 是加载数据的通用方法

    scala> spark.read.tab键
    csv   format   jdbc   json   load   option   options   orc   parquet   schema   table   text   textFile
    
    • 1
    • 2

    如果读取不同格式的数据,可以对不同的数据格式进行设定

    scala> spark.read.format("…")[.option("…")].load("…")
    
    • 1
    • format(“…”):指定加载的数据类型,包括"csv"、“jdbc”、“json”、“orc”、“parquet"和"textFile”。
    • load(“…”):在"csv"、“jdbc”、“json”、“orc”、"parquet"和"textFile"格式下需要传入加载数据的路径。
    • option(“…”):在"jdbc"格式下需要传入JDBC相应参数,url、user、password和dbtable
    scala> spark.read.format("json").load("data/user.json")
    res4: org.apache.spark.sql.DataFrame = [age: bigint, username: string]
    
    scala> res4.show
    +---+--------+
    |age|username|
    +---+--------+
    | 20|zhangsan|
    | 30|    lisi|
    | 10|  wangwu|
    +---+--------+
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    前面都是使用read API 先把文件加载到 DataFrame然后再查询,其实,也可以直接在文件上进行查询: 文件格式.文件路径

    scala>spark.sql("select * from json.`data/user.json`").show
    
    • 1

    (2) 保存数据

    df.write.save 是保存数据的通用方法

    scala>df.write.
    csv  jdbc   json  orc   parquet textFile… …
    
    • 1
    • 2

    如果保存不同格式的数据,可以对不同的数据格式进行设定

    scala>df.write.format("…")[.option("…")].save("…")
    
    • 1
    • format(“…”):指定保存的数据类型,包括"csv"、“jdbc”、“json”、“orc”、“parquet"和"textFile”。
    • save (“…”):在"csv"、“orc”、"parquet"和"textFile"格式下需要传入保存数据的路径。
    • option(“…”):在"jdbc"格式下需要传入JDBC相应参数,url、user、password和dbtable
    # 创建df
    scala> var df = spark.read.json("data/user.json")
    df: org.apache.spark.sql.DataFrame = [age: bigint, username: string]
    # 保存,默认格式为parquet
    scala> df.write.save("output")
    # 保存为json格式,格式一
    scala> df.write.json("output1")
    # 保存为json格式,格式二
    scala> df.write.format("json").save("output2")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    保存操作可以使用 SaveMode, 用来指明如何处理数据,使用mode()方法来设置。

    有一点很重要: 这些 SaveMode 都是没有加锁的, 也不是原子操作。

    SaveMode是一个枚举类,其中的常量包括:

    Scala/JavaAny LanguageMeaning
    SaveMode.ErrorIfExists(default)“error”(default)如果文件已经存在则抛出异常
    SaveMode.Append“append”如果文件已经存在则追加
    SaveMode.Overwrite“overwrite”如果文件已经存在则覆盖
    SaveMode.Ignore“ignore”如果文件已经存在则忽略
    scala> df.write.mode("overwrite").format("json").save("output2")
    scala> df.write.mode("ignore").format("json").save("output2")
    scala> df.write.mode("append").format("json").save("output2")
    
    • 1
    • 2
    • 3

    2 Parquet

    Spark SQL的默认数据源为Parquet格式。Parquet是一种能够有效存储嵌套数据的列式存储格式。

    数据源为Parquet文件时,Spark SQL可以方便的执行所有的操作,不需要使用format。修改配置项spark.sql.sources.default,可修改默认数据源格式。

    # 加载数据
    val df = spark.read.load("output/part-00000-da461a9c-ea5b-489f-bccc-2b97d7bc2910-c000.snappy.parquet").show
    # 保存数据
    scala> val df = spark.read.json("data/user.json")
    df: org.apache.spark.sql.DataFrame = [age: bigint, username: string]
    # parquet格式保存
    scala> df.write.mode("append").save("output")
    # json格式保存
    scala> df.write.mode("append").format("json").save("output")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3 JSON

    Spark SQL 能够自动推测JSON数据集的结构,并将它加载为一个Dataset[Row]. 可以通过SparkSession.read.json()去加载JSON 文件。

    注意:Spark读取的JSON文件不是传统的JSON文件,每一行都应该是一个JSON串。格式如下:

    {"age":20,"username":"zhangsan"}
    {"age":30,"username":"lisi"}
    {"age":10,"username":"wangwu"}
    
    • 1
    • 2
    • 3
    # 导入隐式转换
    import spark.implicits._
    # 加载JSON文件
    val path = "data/user.json"
    val userDF = spark.read.json(path)
    # 创建临时表
    userDF.createOrReplaceTempView("user")
    # 数据查询
    val userNamesDF = spark.sql("SELECT username FROM user WHERE age BETWEEN 9 AND 19")
    userNamesDF.show()
    
    +--------+
    |username|
    +--------+
    |  wangwu|
    +--------+
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    4 CSV

    Spark SQL可以配置CSV文件的列表信息,读取CSV文件,CSV文件的第一行设置为数据列

    spark.read.format("csv").option("sep", ";").option("inferSchema", "true").option("header", "true").load("data/user.csv")
    
    scala> res27.show
    +------------+
    |username       age|
    +------------+
    | zhangsan      20|
    |     lisi      40|
    |   wangwu      50|
    +------------+
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    5 MySQL

    Spark SQL可以通过JDBC从关系型数据库中读取数据的方式创建DataFrame,通过对DataFrame一系列的计算后,还可以将数据再写回关系型数据库中。如果使用spark-shell操作,可在启动shell时指定相关的数据库驱动路径或者将相关的数据库驱动放到spark的类路径下。

    bin/spark-shell --jars mysql-connector-java-5.1.27-bin.jar
    
    • 1

    这里只演示在Idea中通过JDBC对Mysql进行操作

    (1)导入依赖

    
        mysql
        mysql-connector-java
        5.1.27
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (2)读取数据

    //创建配置文件对象
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
    
    //创建SparkSession对象
    val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
    
    import spark.implicits._
    
    //从mysql数据库中读取数据
    //方式1:通用的load方法读取
    spark.read.format("jdbc")
      .option("url", "jdbc:mysql://hadoop101:3306/test")
      .option("driver", "com.mysql.jdbc.Driver")
      .option("user", "root")
      .option("password", "123123")
      .option("dbtable", "user")
      .load().show
    //方式2:通用的load方法读取 参数另一种形式
    spark.read.format("jdbc")
      .options(Map("url"->"jdbc:mysql://hadoop101:3306/test?user=root&password=123123",
        "dbtable"->"user","driver"->"com.mysql.jdbc.Driver")).load().show
    
    //方式3:使用jdbc方法读取
    val props: Properties = new Properties()
    props.setProperty("user", "root")
    props.setProperty("password", "123123")
    val df: DataFrame = spark.read.jdbc("jdbc:mysql://hadoop101:3306/test", "user", props)
    df.show
    
    //释放资源
    spark.stop()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    (3)写入数据

    case class User2(name: String, age: Long)
    
    val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL")
    
    //创建SparkSession对象
    val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
    
    import spark.implicits._
    
    val rdd: RDD[User2] = spark.sparkContext.makeRDD(List(User2("lisi", 20), User2("zs", 30)))
    val ds: Dataset[User2] = rdd.toDS
    //方式1:通用的方式  format指定写出类型
    ds.write.format("jdbc").option[...].mode(SaveMode.append).save()
     
    //方式2:通过jdbc方法
    val props: Properties = new Properties()
    props.setProperty("user", "root")
    props.setProperty("password", "123123")
    ds.write.mode(SaveMode.Append).jdbc("jdbc:mysql://hadoop101:3306/test", "user", props)
    
    //释放资源
    spark.stop()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    6 Hive

    Apache Hive 是 Hadoop 上的 SQL 引擎,Spark SQL编译时可以包含 Hive 支持,也可以不包含。

    包含 Hive 支持的 Spark SQL 可以支持 Hive 表访问、UDF (用户自定义函数)以及 Hive 查询语言(HiveQL/HQL)等。需要强调的一点是,如果要在 Spark SQL 中包含Hive 的库,并不需要事先安装 Hive。一般来说,最好还是在编译Spark SQL时引入Hive支持,这样就可以使用这些特性了。如果下载的是二进制版本的 Spark,它应该已经在编译时添加了 Hive 支持。

    若要把 Spark SQL 连接到一个部署好的 Hive 上,必须把 hive-site.xml 复制到 Spark的配置文件目录中($SPARK_HOME/conf)。即使没有部署好 Hive,Spark SQL 也可以运行。 需要注意的是,如果没有部署好Hive,Spark SQL 会在当前的工作目录中创建出自己的 Hive 元数据仓库,叫作 metastore_db。此外,如果尝试使用 HiveQL 中的 CREATE TABLE (并非 CREATE EXTERNAL TABLE)语句来创建表,这些表会被放在默认的文件系统中的 /user/hive/warehouse 目录中(如果classpath 中有配好的 hdfs-site.xml,默认的文件系统就是 HDFS,否则就是本地文件系统)。

    spark-shell默认是Hive支持的;代码中是默认不支持的,需要手动指定(加一个参数即可)。

    (1)内嵌的HIVE

    如果使用 Spark 内嵌的 Hive, 则什么都不用做, 直接使用即可.

    Hive 的元数据存储在 derby 中, 默认仓库地址:$SPARK_HOME/spark-warehouse

    scala> spark.sql("show tables").show
    
    +--------+---------+-----------+
    |database|tableName|isTemporary|
    +--------+---------+-----------+
    +--------+---------+-----------+
    
    scala> spark.sql("create table aa(id int)")
    
    scala> spark.sql("show tables").show
    +--------+---------+-----------+
    |database|tableName|isTemporary|
    +--------+---------+-----------+
    | default|       aa|      false|
    +--------+---------+-----------+
    # 向表加载本地数据
    scala> spark.sql("load data local inpath 'input/ids.txt' into table aa")
    
    scala> spark.sql("select * from aa").show
    +---+
    | id|
    +---+
    |  1|
    |  2|
    |  3|
    |  4|
    +---+
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在实际使用中, 几乎没有任何人会使用内置的 Hive

    (2)外部的HIVE

    如果想连接外部已经部署好的Hive,需要通过以下几个步骤:

    • 确定原有Hive是正常工作的
    • Spark要接管Hive需要把hive-site.xml拷贝到spark的conf/目录下
    • 如果以前hive-site.xml文件中,配置过Tez相关信息,注释掉
    • 把Mysql的驱动copy到Spark的ars/目录下
    • 需要提前启动hive,dfs服务,hive/bin/hiveservices.sh start
    • 如果访问不到hdfs,则需要把core-site.xml和hdfs-site.xml拷贝到conf/目录下
    • 重启spark-shell
    scala> spark.sql("show tables").show
    20/04/25 22:05:14 WARN ObjectStore: Failed to get database global_temp, returning NoSuchObjectException
    +--------+--------------------+-----------+
    |database|           tableName|isTemporary|
    +--------+--------------------+-----------+
    | default|                 emp|      false|
    | default|hive_hbase_emp_table|      false|
    | default| relevance_hbase_emp|      false|
    | default|          staff_hive|      false|
    | default|                 ttt|      false|
    | default|   user_visit_action|      false|
    +--------+--------------------+-----------+
    
    scala> spark.sql("select * from emp").show
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    (3)运行Spark SQL CLI

    Spark SQLCLI可以很方便的在本地运行Hive元数据服务以及从命令行执行查询任务。在Spark目录下执行如下命令启动Spark SQ LCLI,直接执行SQL语句,类似Hive窗口。

    bin/spark-sql
    
    • 1

    (4)代码中操作Hive

    添加依赖
    
        org.apache.spark
        spark-hive_2.12
        3.0.0
    
    
    
        org.apache.hive
        hive-exec
        1.2.1
    
    
        mysql
        mysql-connector-java
        5.1.27
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    将hive-site.xml文件拷贝到项目的resources目录中
    代码实现
    def main(args: Array[String]): Unit = {
        //创建上下文环境配置对象
        val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkSQL01_Demo")
        val spark: SparkSession = SparkSession
          .builder()
          .enableHiveSupport()
          .master("local[*]")
          .appName("SQLTest")
          .getOrCreate()
        spark.sql("show tables").show()
        //释放资源
        spark.stop()
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    注意:在开发工具中创建数据库默认是在本地仓库,通过参数修改数据库仓库的地址:

    config("spark.sql.warehouse.dir", "hdfs://hadoop101:8020/user/hive/warehouse")
    
    • 1

    如果在执行操作时,出现如下错误:

    在这里插入图片描述

    可以代码最前面增加如下代码解决:

    System.setProperty("HADOOP_USER_NAME", "root")
    
    • 1

    此处的root改为自己的hadoop用户名称

    二 SparkSQL项目实战

    1 数据准备

    Spark-sql 操作中所有的数据均来自 Hive,首先在 Hive 中创建表,,并导入数据。

    city_info.txt、product_info.txt、user_visit_action.txt

    一共有3张表: 1张用户行为表,1张城市表,1 张产品表

    CREATE TABLE `user_visit_action`(
      `date` string,	
      `user_id` bigint,
      `session_id` string,
      `page_id` bigint,
      `action_time` string,
      `search_keyword` string,
      `click_category_id` bigint,
      `click_product_id` bigint,
      `order_category_ids` string,
      `order_product_ids` string,
      `pay_category_ids` string,
      `pay_product_ids` string,
      `city_id` bigint)
    row format delimited fields terminated by '\t';
    
    load data local inpath 'input/user_visit_action.txt' into table user_visit_action;
    
    
    CREATE TABLE `product_info`(
      `product_id` bigint,
      `product_name` string,
      `extend_info` string)
    row format delimited fields terminated by '\t';
    
    load data local inpath 'input/product_info.txt' into table product_info;
    
    
    CREATE TABLE `city_info`(
      `city_id` bigint,
      `city_name` string,
      `area` string)
    row format delimited fields terminated by '\t';
    
    load data local inpath 'input/city_info.txt' into table city_info;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    2 需求:各区域热门商品Top3

    (1)需求简介

    这里的热门商品是从点击量的维度来看的,计算各个区域前三大热门商品,并备注上每个商品在主要城市中的分布比例,超过两个城市用其他显示。

    例如:

    地区商品名称点击次数城市备注
    华北商品A100000北京21.2%,天津13.2%,其他65.6%
    华北商品P80200北京63.0%,太原10%,其他27.0%
    华北商品M40000北京63.0%,太原10%,其他27.0%
    东北商品J92000大连28%,辽宁17.0%,其他 55.0%

    (2)需求分析

    • 使用 sql 来完成,碰到复杂的需求,可以使用 udf 或 udaf

    • 查询出来所有的点击记录,并与 city_info 表连接,得到每个城市所在的地区,与 Product_info 表连接得到产品名称

    • 按照地区和商品 id 分组,统计出每个商品在每个地区的总点击次数

    • 每个地区内按照点击次数降序排列

    • 只取前三名

    • 城市备注需要自定义 UDAF 函数

    (3)功能实现

    • 连接三张表的数据,获取完整的数据(只有点击)

    • 将数据根据地区,商品名称分组

    • 统计商品点击次数总和,取Top3

    • 实现自定义聚合函数显示城市备注

    通过SQL实现求出各个地区热门排名在前3
    # 1.从用户行为表中,查询所有点击记录,并和city_info,product_info进行连接
    # 获取(1)表中的地区和商品名称
    select 
        c.area,
        p.product_name
    from 
        user_visit_action a
    join
        city_info c
    on
        a.city_id = c.city_id
    join
        product_info p
    on
        a.click_product_id = p.product_id
    where 
        a.click_product_id != -1
    limit 10
    
    
    # 2.按照地区和商品的名称进行分组,统计出每个地区每个商品的总点击数
    select
        t1.area,
        t1.product_name,
        count(*) as product_click_count
    from
        (
            select 
                c.area,
                p.product_name
            from 
                user_visit_action a
            join
                city_info c
            on
                a.city_id = c.city_id
            join
                product_info p
            on
                a.click_product_id = p.product_id
            where 
                a.click_product_id != -1
    
        )t1
    group by t1.area,t1.product_name
    limit 10
    
    # 3.针对每个地区,对商品点击数进行降序排序
    select
       t2.area,
       t2.product_name,
       t2.product_click_count,
       row_number() over(partition by t2.area order by t2.product_click_count desc) cn
    from 
        (
            select
                t1.area,
                t1.product_name,
                count(*) as product_click_count
            from
                (
                    select 
                        c.area,
                        p.product_name
                    from 
                        user_visit_action a
                    join
                        city_info c
                    on
                        a.city_id = c.city_id
                    join
                        product_info p
                    on
                        a.click_product_id = p.product_id
                    where 
                        a.click_product_id != -1
            
                )t1
            group by t1.area,t1.product_name
        )t2
    limit 30
    
    # 4.取当前地区的前3名
    select
        t3.area,
        t3.product_name,
        t3.product_click_count,
        t3.cn
    from
        (
            select
               t2.area,
               t2.product_name,
               t2.product_click_count,
               row_number() over(partition by t2.area order by t2.product_click_count desc) cn
            from 
                (
                    select
                        t1.area,
                        t1.product_name,
                        count(*) as product_click_count
                    from
                        (
                            select 
                                c.area,
                                p.product_name
                            from 
                                user_visit_action a
                            join
                                city_info c
                            on
                                a.city_id = c.city_id
                            join
                                product_info p
                            on
                                a.click_product_id = p.product_id
                            where 
                                a.click_product_id != -1
                    
                        )t1
                    group by t1.area,t1.product_name
                )t2
        )t3
    where t3.cn <= 3
    limit 12
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    代码实现
    def main(args: Array[String]): Unit = {
        val spark: SparkSession = SparkSession
          .builder()
          .master("local[2]")
          .appName("AreaClickApp")
          .enableHiveSupport()
          .getOrCreate()
        //选择hive库
        spark.sql("use sparkpractice")
        // 0 注册自定义聚合函数
        spark.udf.register("city_remark", new AreaClickUDAF)
        // 1. 查询出所有的点击记录,并和城市表产品表做内连接
        spark.sql(
          """
            |select
            |    c.*,
            |    v.click_product_id,
            |    p.product_name
            |from user_visit_action v join city_info c join product_info p on v.city_id=c.city_id and v.click_product_id=p.product_id
            |where click_product_id>-1
          """.stripMargin).createOrReplaceTempView("t1")
    
        // 2. 计算每个区域, 每个产品的点击量
        spark.sql(
          """
            |select
            |    t1.area,
            |    t1.product_name,
            |    count(*) click_count,
            |    city_remark(t1.city_name)
            |from t1
            |group by t1.area, t1.product_name
          """.stripMargin).createOrReplaceTempView("t2")
    
        // 3. 对每个区域内产品的点击量进行降序排列
        spark.sql(
          """
            |select
            |    *,
            |    row_number() over(partition by t2.area order by t2.product_click_count desc) cn
            |from t2
          """.stripMargin).createOrReplaceTempView("t3")
    
        // 4. 每个区域取top3
        spark.sql(
          """
            |select
            |    *
            |from t3
            |where t3.cn <= 3
          """.stripMargin).show
    
        //释放资源
        spark.stop()
    
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    UDAF函数定义
    class AreaClickUDAF extends UserDefinedAggregateFunction {
      // 输入数据的类型:  北京  String
      override def inputSchema: StructType = {
        StructType(StructField("city_name", StringType) :: Nil)
        //        StructType(Array(StructField("city_name", StringType)))
      }
    
      // 缓存的数据的类型: 北京->1000, 天津->5000  Map,  总的点击量  1000/?
      override def bufferSchema: StructType = {
        // MapType(StringType, LongType) 还需要标注 map的key的类型和value的类型
        StructType(StructField("city_count", MapType(StringType, LongType)) :: StructField("total_count", LongType) :: Nil)
      }
    
      // 输出的数据类型  "北京21.2%,天津13.2%,其他65.6%"  String
      override def dataType: DataType = StringType
    
      // 相同的输入是否应用有相同的输出.
      override def deterministic: Boolean = true
    
      // 给存储数据初始化
      override def initialize(buffer: MutableAggregationBuffer): Unit = {
        // 初始化map缓存
        buffer(0) = Map[String, Long]()
        // 初始化总的点击量
        buffer(1) = 0L
      }
    
      // 分区内合并 Map[城市名, 点击量]
      override def update(buffer: MutableAggregationBuffer, input: Row): Unit = {
        // 首先拿到城市名, 然后把城市名作为key去查看map中是否存在, 如果存在就把对应的值 +1, 如果不存在, 则直接0+1
        val cityName = input.getString(0)
          
        // 从缓存中获取存放城市点击数量的Map集合
        // val map: collection.Map[String, Long] = buffer.getMap[String, Long](0)
        val map: Map[String, Long] = buffer.getAs[Map[String, Long]](0)
          
        // 封装成KV结构,对缓存中的数据进行更新,城市点击量 + 1
        buffer(0) = map + (cityName -> (map.getOrElse(cityName, 0L) + 1L))
          
        // 碰到一个城市, 则总点击量要 + 1
        buffer(1) = buffer.getLong(1) + 1L
      }
    
      // 分区间的缓存合并
      override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = {
        // 获取没一个节点城市点击缓存Map
        val map1 = buffer1.getAs[Map[String, Long]](0)
        val map2 = buffer2.getAs[Map[String, Long]](0)
    
        // 合并两个节点上的城市点击,把map1的键值对与map2中的累加, 最后赋值给buffer1
        buffer1(0) = map1.foldLeft(map2) {
          case (map, (k, v)) =>
            map + (k -> (map.getOrElse(k, 0L) + v))
        }
    
        // 合并两个节点上的总点击数
        buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1)
      }
    
      // 最终的输出. "北京21.2%,天津13.2%,其他65.6%"
      override def evaluate(buffer: Row): Any = {
        // 从缓存中获取数据
        val cityCountMap = buffer.getAs[Map[String, Long]](0)
        val totalCount = buffer.getLong(1)
    
        // 对Map集合中城市点击记录进行降序排序,取前两个
        var citysRatio: List[CityRemark] = cityCountMap.toList.sortBy(-_._2).take(2).map {
          // 计算前2城市的点击率
          case (cityName, count) => {
            CityRemark(cityName, count.toDouble / totalCount)
          }
        }
        // 如果城市的个数超过2才显示其他,将其他添加到集合中
        if (cityCountMap.size > 2) {
          citysRatio = citysRatio :+ CityRemark("其他", citysRatio.foldLeft(1D)(_ - _.cityRatio))
        }
        citysRatio.mkString(", ")
      }
    }
    
    // 城市点击率格式化
    case class CityRemark(cityName: String, cityRatio: Double) {
      val formatter = new DecimalFormat("0.00%")
      override def toString: String = s"$cityName:${formatter.format(cityRatio)}"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
  • 相关阅读:
    net-java-php-python-班级信息管理系统计算机毕业设计程序
    C语言编程陷阱(一)
    基于中文金融知识的 LLaMA 系微调模型的智能问答系统:LLaMA大模型训练微调推理等详细教学
    leetcode:762. 二进制表示中质数个计算置位
    基于SSM的电子竞技管理平台
    .net core 和 WPF 开发升讯威在线客服系统:怎样实现拔网线也不丢消息的高可靠通信(附视频)
    Vue.js:渐进式JavaScript框架-前端开发
    【解决方法】树莓派4B安装wiringpi失败、gpio -v与gpio readall命令not found(arm64架构)
    深入理解Kafka核心设计及原理(二):生产者
    代码审计学习phpcms头像上传漏洞
  • 原文地址:https://blog.csdn.net/weixin_43923463/article/details/126678321