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
      }
    
    }
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章