# Group By
一个带Group By的伪代码示例如下。
```
SELECT  shop_id
       ,sum(is_open) AS 营业天数
FROM    table_xxx_di
WHERE   dt BETWEEN '${bizdate_365}' AND '${bizdate}'
GROUP BY shop_id;
```
当发生数据倾斜时，可以通过如下两种方案解决：
| **序号** | **方案** | **说明**          |
|:---|:---|:---|
| 方案一    | 添加随机数 | 把引起长尾的key进行拆分。 |
| 方案二   | 创建滚存表   | 降本提效。           |
   
**方案一：添加随机数。**
对SQL进行改写，添加随机数，把引起长尾的key进行拆分是解决Group By长尾的一种比较好的方法。
```
SELECT Key, Count(*) AS Cnt FROM TableName GROUP BY Key;
```
不考虑Combiner，M节点会Shuffle到R节点上，然后R节点再做Count操作，对应的执行计划是M-\>R。
假定已经找到了引起长尾的key，对长尾的key再做一次工作再分配，就变成：
```
-- 假设长尾的Key已经找到是KEY001
SELECT  a.Key
       ,SUM(a.Cnt) AS Cnt
FROM (
    SELECT  Key
           ,COUNT(*) AS Cnt
    FROM    TableName
    GROUP BY Key
           ,CASE WHEN Key = 'KEY001' THEN pmod(hash(rand()), 50)
                 ELSE 0
            END
) a
GROUP BY a.Key;
```
改完之后的执行计划变成了 M-\>R-\>R，虽然执行步骤变长了，但是长尾的key经过了 2 个步骤的处理，整体的时间消耗可能反而有所减少。与不加随机数的直接聚合相比，资源消耗与耗时效果基本持平；但实际场景中引发长尾的key不止一个，再考虑寻找长尾key和SQL改写的投入成本，直接对长尾key添加随机数（不做两阶段改写）的投入成本会更低一些。
**方案二：创建滚存表。**
降本提效，核心诉求是取过去一年的商户数据，对于线上任务而言，每次都要读取T-1至T-365的所有分区其实是对资源的很大浪费，创建滚存表可以减少分区的读取但是又不影响过去一年的取数，示例如下。
首次初始化365天的商户营业数据（Group By汇总），标记数据更新日期，记为表a；后续线上任务切换为T-2日表a关联table_xxx_di表再Group By，这样每天读取的数据从365减少到了2个，主键shopid的重复性极大降低，对资源的消耗也会减少。
```
-- 创建滚存表
CREATE TABLE IF NOT EXISTS m_xxx_365_df (
   shop_id STRING COMMENT '商户ID',
   last_update_ds STRING COMMENT '最近更新日期',
   `365d_open_days` BIGINT COMMENT '365天营业天数'
)
PARTITIONED BY (ds STRING COMMENT '日期分区');
-- 假定 365d 是 2021.5.1-2022.5.1，先完成一次初始化
INSERT OVERWRITE TABLE m_xxx_365_df PARTITION(ds = '20220501')
SELECT shop_id
      ,max(ds) AS last_update_ds
      ,sum(is_open) AS `365d_open_days`
FROM table_xxx_di
WHERE ds BETWEEN '20210501' AND '20220501'
GROUP BY shop_id;
-- 那么之后线上任务要执行的是
INSERT OVERWRITE TABLE m_xxx_365_df PARTITION(ds = '${bizdate}')
SELECT aa.shop_id
      ,aa.last_update_ds
      ,`365d_open_days` - COALESCE(is_open, 0) AS `365d_open_days`  -- 消除营业天数的无限滚存
FROM (
    SELECT shop_id
          ,max(last_update_ds) AS last_update_ds
          ,sum(`365d_open_days`) AS `365d_open_days`
    FROM (
        SELECT shop_id
              ,ds AS last_update_ds
              ,sum(is_open) AS `365d_open_days`
        FROM table_xxx_di
        WHERE ds = '${bizdate}'
        GROUP BY shop_id
        UNION ALL
        SELECT shop_id
              ,last_update_ds
              ,`365d_open_days`
        FROM m_xxx_365_df
        WHERE ds = '${bizdate_2}' AND last_update_ds >= '${bizdate_365}'
        GROUP BY shop_id
    )
    GROUP BY shop_id
) AS aa
LEFT JOIN (
    SELECT shop_id, is_open
    FROM table_xxx_di
    WHERE ds = '${bizdate_366}'
) AS bb
ON aa.shop_id = bb.shop_id;
```
