Spark代码可读性与性能优化——示例十一(SQL与代码-蚂蚁森林示例)

Spark代码可读性与性能优化——示例十一(SQL与代码-蚂蚁森林示例)

前言

  • 编写SQL处理业务问题,通常有简单易用、便捷、适用人群广泛等优点,是数据分析师的不二之选。
  • 但是,SQL易用的同时也带来了性能的问题,当为了解决某些复杂的业务时,你不得不编写几十至几百行很复杂的SQL来处理。由于为了实现复杂的业务,SQL中会存在很多在逻辑层面来看的不必要的操作,导致性能浪费。
  • 这个时候代码的优势就体现出来了,虽然代码适用人群没有SQL广泛,但是在某些情况下处理同一个逻辑时相比SQL更为清晰、简单(SQL为了一个功能,可能会绕一大圈)。
  • 在这里,想讨论的就是此部分的业务,使用代码,抛弃无用的处理逻辑,以提升应用性能。

业务描述

  • 此处使用蚂蚁森林的一个业务问题,用作展示。为了易懂,会去除部分不必要的数据。

  • 数据表 user_id_carbon

    • 数据
    user_id data_dt low_carbon
    u_001 2018/2/3 100
    u_001 2018/2/3 20
    u_002 2018/2/3 50
    u_004 2018/2/4 70
    u_004 2018/2/5 90
    • 字段描述:
      • user_id 代表用户的唯一id
      • data_dt 代表日期
      • low_carbon 代表当日某次的减少的碳排放量(每日可能存在多次)
  • 业务需求

    • 获取在2018年中连续三天(及以上)每天一共减少的碳排放量大于100的用户
  • 业务条件分析

    • 时间范围需要在2018年
    • 每个用户每天可能会多次降低碳排放,因此需要统计每个用户每天降低的总的碳排放量
    • 每个用户每天降低的总的碳排放量需要大于100,并且要求连续三天(及以上)
  • 注意:后面的示例中,SQL已在HIVE中测试过,SparkSQL未知

使用SQL解决业务问题 - 方案1

  1. 得到2018年每个用户每日低碳量超过100的,生成表 t1
    SELECT 
        user_id,
        data_format(regexp_replace(data_dt, '/', '-'), 'yyyy-MM-dd') dt
    FROM user_low_carbon
    WHERE SUBSTRING(data_dt, 1, 4) = '2018'
    GROUP BY user_id, data_dt
    HAVING SUM(low_carbon) >= 100;
    
  2. 对于每个用户,获取到当前行前2行、后2行的日期,生成表 t2
    SELECT 
        user_id,
        dt,
        -- 使用开窗函数,LAG获取当前行前n行的数据
        LAG(dt, 2, '1970-01-01') OVER(PARTITION BY user_id ORDER BY dt) lag2,
        LAG(dt, 1, '1970-01-01') OVER(PARTITION BY user_id ORDER BY dt) lag1,
        -- 使用开窗函数,LEAD获取当前行后n行的数据
        LEAD(dt, 1, '9999-99-99') OVER(PARTITION BY user_id ORDER BY dt) lead1,
        LEAD(dt, 2, '9999-99-99') OVER(PARTITION BY user_id ORDER BY dt) lead2
    FROM t1;
    
  3. 用户连续3天大于100的,生成表 t3
    SELECT 
        user_id,
        dt,
        -- DATADIFF日期相减
        DATADIFF(dt, lag2) lag2_diff,
        DATADIFF(dt, lag1) lag1_diff,
        DATADIFF(dt, lead1) lead1_diff,
        DATADIFF(dt, lead2) lead2_diff
    FROM t2
    WHERE 
        (lag2_diff = '2' AND lag1_diff = '1') OR
        (lag1_diff = '1' AND lead1_diff = '-1') OR
        (lead1_diff = '-1' AND lead2_diff = '-2');
    
  • 对于当前条满足其一,即可满足连续3天降低的碳排放量大于100
    • 当天比前2行的日期小2当天比前1行的日期小1
    • 当天比前1行的日期小1当天比后1行的日期大1
    • 当天比后1行的日期大1当天比后2行的日期大2

使用SQL解决业务问题 - 方案2

  1. 得到2018年每个用户每日低碳量超过100的,生成表 t1
    SELECT 
        user_id,
        data_format(regexp_replace(data_dt, '/', '-'), 'yyyy-MM-dd') dt
    FROM user_low_carbon
    WHERE SUBSTRING(data_dt, 1, 4) = '2018'
    GROUP BY user_id, data_dt
    HAVING SUM(low_carbon) >= 100;
    
  2. 正对每个用户进行日期排序,并生成序号,生成表 t2
    SELECT 
        user_id,
        data_dt,
        -- 开窗函数,根据用户id分组,并根据日期排序,生成序号
        RANK() OVER(PARTITION BY user_id ORDER BY data_dt) rk
    FROM t1;
    
  3. 使用日期减去序号,生成表 t3
    SELECT 
        user_id,
        data_dt,
        DATE_SUB(data_dt, rk) date_sub_rk
    FROM t2;
    
  4. 根据用户+日期差值分组,并计数,如果大于等于3,那么说明连续3天降低的碳排放量大于100
    SELECT 
        user_id,
        data_dt
        -- 开窗函数,根据用户id、日期差值分组,并计数
        COUNT(1) OVER(PARTITION BY user_id, date_sub_rk) c
    FROM t3
    HAVING c >= 3;
    

使用代码带来更高的性能

  • 前面的两种SQL方案中使用了大量的Shuffle操作(分组聚合、开窗函数、排序),显然是会影响性能的。这点其实已经不言自明了!^_^
  • 从逻辑上来说,我首先只需要对用户id分组一次,然后在聚合时统计每天的总碳排放量,过滤掉小于100的,然后看下是否有连续3天(及以上)的即可,完全不需要这么多次Shuffle操作。
  • 写代码,总体来说有两种方案
    • 完全由代码实现处理逻辑(以此为示例)
    • SQL+代码实现处理逻辑(利用SQL进行分组,然后自定义聚合函数,在里面写代码)
  • 代码示例如下(未完)
    object AnalysisApp {
    
      def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
          .setAppName("AnalysisApp")
        val spark = SparkSession.builder()
          .config(conf)
          .enableHiveSupport()
          .getOrCreate()
    
        import spark.implicits._
    
        // 开始处理
        spark.read.table("user_low_carbon")
          .as[UserCarbon]
          .filter(_.data_dt.startsWith("2018")) // 只获取2018年的数据
          .groupByKey(_.user_id) // 按用户id分组
          .flatMapGroups(continue3Func) // 计算出碳排放量连续3天大于100的用户
          .show()
    
        spark.stop()
      }
    
      // 原始数据示例:u_001, 2018/2/3, 100
      private case class UserCarbon(user_id: Long, data_dt: String, low_carbon: Long)
    
      // 计算碳排放量连续3天大于100的函数
      private val continue3Func = (id: Long, iter: Iterator[UserCarbon]) => {
        // 统计每天总的碳排放量
        // TreeMap默认根据key排序(日期排序),自然顺序、升序
        val treeMap = new java.util.TreeMap[String, Long]()
        for (userCarbon <- iter) {
          val value = treeMap.getOrDefault(userCarbon.data_dt, 0L)
          treeMap.put(userCarbon.data_dt, value + userCarbon.low_carbon)
        }
    
        // 计算连续3天及以上碳排放量大于100的
        val resultList = ListBuffer[UserCarbon]() // 符合条件的UserCarbon
        val tempList = ListBuffer[UserCarbon]() // 临时的UserCarbon
        val format = new SimpleDateFormat("yyyy/MM/dd")
        // 初始值
        var dayMills = 0L // 起始时间
        var count = 0L // 连续大于100的个数
        // 开始遍历,treeMap已按时间升序
        val entryIter = treeMap.entrySet().iterator()
        while (entryIter.hasNext) {
          val entry = entryIter.next()
          // 碳排放量大于100的
          if (entry.getValue >= 100) {
            val time = format.parse(entry.getKey).getTime
            // 比前面的日期大1天
            if (time == dayMills + 86400L * 1000) {
              // 计数,并存入临时区
              count += 1
              tempList.append(UserCarbon(id, entry.getKey, entry.getValue))
            } else {
              // 如果日期相差不是1天,那么处理前面临时的数据
              if (count < 3) {
                // 连续时间小3的,直接清空
                tempList.clear()
              } else {
                // 连续时间大于等于3的,先保留,再清空
                resultList.appendAll(tempList)
                tempList.clear()
              }
              // 重置时间起点
              dayMills = time
            }
          }
        }
    
        resultList
      }
    
    }
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章