比较每月数据,同时保持每日粒度

Posted

技术标签:

【中文标题】比较每月数据,同时保持每日粒度【英文标题】:Compare monthly data while retaining daily granularity 【发布时间】:2020-02-03 22:01:14 【问题描述】:

我有以下数据,其中包含一组 ID 的每月目标。目标是针对每个 id,针对 2020 年的每个月。名为 targets 的表。 month 列表示一年中的月份。

+-------+-------+----+--------+
| month | name  | id | target |
+-------+-------+----+--------+
| 1     | Comp1 | 1  | 6000   |
+-------+-------+----+--------+
| 2     | Comp1 | 1  | 6000   |
+-------+-------+----+--------+
| 3     | Comp1 | 1  | 6000   |
+-------+-------+----+--------+
| 1     | Comp2 | 2  | 6000   |
+-------+-------+----+--------+
| 2     | Comp2 | 2  | 6000   |
+-------+-------+----+--------+
| 3     | Comp2 | 2  | 6000   |
+-------+-------+----+--------+
| 1     | Comp3 | 3  | 6000   |
+-------+-------+----+--------+
| 2     | Comp3 | 3  | 6000   |
+-------+-------+----+--------+
| 3     | Comp3 | 3  | 6000   |
+-------+-------+----+--------+
| 1     | Comp4 | 4  | 6000   |
+-------+-------+----+--------+
| 2     | Comp4 | 4  | 6000   |
+-------+-------+----+--------+
| 3     | Comp4 | 4  | 6000   |
+-------+-------+----+--------+

然后我有第二个表,其中包含一组 id 的每日数据,并且每天更新。在我的实际数据集中,我有从 2019-01-01 到今天的数据。

+------------+-------+----+--------+--------+
| yyyy_mm_dd | name  | id | actual | region |
+------------+-------+----+--------+--------+
| 2019-01-01 | Comp1 | 1  | 1000   | LATAM  |
+------------+-------+----+--------+--------+
| 2019-01-01 | Comp1 | 1  |   0    |  EU    |
+-------------------------------------------+
| 2019-01-02 | Comp1 | 1  | 2000   |  EU    |
+------------+-------+----+--------+--------+
| 2019-01-03 | Comp1 | 1  | 4000   |  EU    |
+------------+-------+----+--------+--------+
| 2019-01-01 | Comp2 | 2  | 1000   |  EU    |
+------------+-------+----+--------+--------+
| 2019-01-02 | Comp2 | 2  | 2000   |  EU    |
+------------+-------+----+--------+--------+
| 2019-01-03 | Comp2 | 2  | 3000   |  EU    |
+------------+-------+----+--------+--------+
| 2019-01-01 | Comp3 | 3  | 1000   |  EU    |
+------------+-------+----+--------+--------+
| 2019-01-02 | Comp3 | 3  | 2000   |  EU    |
+------------+-------+----+--------+--------+
| 2019-01-03 | Comp3 | 3  | 8000   |  EU    |
+------------+-------+----+--------+--------+
| 2019-01-01 | Comp4 | 4  | 1000   |  EU    |
+------------+-------+----+--------+--------+
| 2019-01-02 | Comp4 | 4  | 2000   |  EU    |
+------------+-------+----+--------+--------+
| 2019-02-03 | Comp4 | 4  | 3000   |  EU    |
+------------+-------+----+--------+--------+

基于以上两个表,我想创建第三个表,加上一些额外的逻辑。最后,我想引入一个名为payment 的新专栏。此列应始终为 0,除非公司已通过其每月目标。如果达到/超过每月目标,则应支付sum actual for that month - monthly target for that month * 1%

以下是输出数据的外观:

+------------+-------+----+--------+--------+
| yyyy_mm_dd | name  | id | actual | payout |
+------------+-------+----+--------+--------+
| 2020-01-01 | Comp1 | 1  | 1000   | 0      |
+------------+-------+----+--------+--------+
| 2020-01-02 | Comp1 | 1  | 2000   | 0      |
+------------+-------+----+--------+--------+
| 2020-01-03 | Comp1 | 1  | 4000   | 10     |
+------------+-------+----+--------+--------+
| 2020-01-01 | Comp2 | 2  | 1000   | 0      |
+------------+-------+----+--------+--------+
| 2020-01-02 | Comp2 | 2  | 2000   | 0      |
+------------+-------+----+--------+--------+
| 2020-01-03 | Comp2 | 2  | 3000   | 0      |
+------------+-------+----+--------+--------+
| 2020-01-01 | Comp3 | 3  | 1000   | 0      |
+------------+-------+----+--------+--------+
| 2020-01-02 | Comp3 | 3  | 2000   | 0      |
+------------+-------+----+--------+--------+
| 2020-01-03 | Comp3 | 3  | 8000   | 50     |
+------------+-------+----+--------+--------+
| 2020-01-01 | Comp4 | 4  | 1000   | 0      |
+------------+-------+----+--------+--------+
| 2020-01-02 | Comp4 | 4  | 2000   | 0      |
+------------+-------+----+--------+--------+
| 2020-02-03 | Comp4 | 4  | 3000   | 0      |
+------------+-------+----+--------+--------+

上述数据集中的所有名称/ID 的月度 target 为 6000。因此,当名称/ID 在当月通过该目标时,应该只有 payout。 Comp1 和 Comp3 都在 1 月的第三天超过了每月目标,因此他们从那天起直到月底都获得了付款。然后在 2 月重置,因为这是一个新的月份,有一个新的目标,随着月份的进行,我们将获得新的每日数据。


我尝试过的:

SELECT
    agg.yyyy_mm_dd,
    agg.name,
    agg.id,
    CASE WHEN agg.actual >= targets.target THEN ((agg.actual-targets.target)/100) * 1 ELSE 0 END AS payout
FROM(
    SELECT
        sum(x.actual) AS actual,
        x.yyyy_mm_dd,
        x.name,
        x.id
    FROM(
        SELECT
            yyyy_mm_dd,
            name,
            id,
            cast(actual as int) as actual
        FROM
            schema.daily_data
        WHERE
            yyyy_mm_dd >= '2020-01-01' AND (name = 'Comp1' OR name = 'Comp2')
    ) x
    GROUP BY
        2,3,4
) agg
INNER JOIN(
    SELECT
      id,
      month,
      target
    FROM
        schema.targets
) targets ON targets.id = agg.id
GROUP BY
    1,2,3,4

但是,上述每个name 输出多行。这是每日表每天多次使用同一家公司的结果(预期)。我以为我的分组会处理这个问题。另外,我认为这不是最简单的解决方案,我可能想多了/可以更有效地完成。

【问题讨论】:

【参考方案1】:

您似乎想将每个公司和每个月的 actua 的累积总和与 target 进行比较。您可以使用连接和窗口函数来做到这一点:

select 
    d.yyyy_mm_dd, 
    case when sum(d.actual) over(partition by d.name, t.month order by d.yyyy_mm_dd) > t.target
        then (sum(d.actual) over(partition by d.name, t.month order by d.yyyy_mm_dd) - t.target) / 100.0
        else 0
    end payout
from schema.targets t
inner join schema.daily_data d
    on  month(d.yyyy_mm_dd) = t.month
    and d.name = t.name
where
    d.yyyy_mm_dd >= '2020-01-01' 
    and d.name in ('Comp1', 'Comp2')

【讨论】:

在每日数据中,yyyy_mm_dd 存储为日期。月表没有日期,只有 month,它存储为 int。 @stackq:我明白了。 Hive 的做法与其他数据库不同。我相应地修正了我的答案。 存在一些语法错误,我已修复,但我不确定:Both left and right aliases encountered in JOIN 'yyyy_mm_dd' @stackq:我也修正了一些错别字。你还在报错吗? 您好,您更改的代码仍然存在同样的问题。 Both left and right aliases encountered in JOIN '1'【参考方案2】:

另一种选择是使用窗口 SUM 函数来创建运行总计,然后在 CASE 语句中使用它来获取列值。

SELECT d.yyyy_mm_dd
    ,d.name
    ,d.id
    ,d.actual
    ,CASE 
        WHEN 
      SUM(d.actual) 
        OVER (PARTITION BY d.id ORDER BY d.yyyy_mm_dd ROWS UNBOUNDED PRECEDING) <= t.target
            THEN 0
        ELSE 
      (
        SUM(d.actual) 
          OVER (PARTITION BY d.id ORDER BY d.yyyy_mm_dd ROWS UNBOUNDED PRECEDING) - t.target
            ) * 0.01
        END AS payout
FROM dailies AS d
JOIN targets AS t 
    ON d.month = MONTH(d.yyyy_mm_dd)
    AND d.id = d.id;

我不能 100% 确定 Hive 语法,但这非常接近。具体来说,ROWS UNBOUNDED PRECEDING 可能还不够。您可能需要在其中添加一个FOLLOWING 子句才能正确计算总数。

【讨论】:

嗨,这个运行但与这​​个问题的其他答案有类似的问题。输出在同一天提供多个ids。这是因为每日数据在同一天可以有几个相同的ids。我已经更新了示例数据以反映这一点。【参考方案3】:

您对运行(部分)实际值总和的请求很容易通过窗口函数解决。不幸的是我不使用 Hive,所以这是我的 Postgres 工作解决方案

with t (month, name, id, target) as (values
  (1 , 'Comp1', 1 , 6000 ),
  (2 , 'Comp1', 1 , 6000 ),
  (3 , 'Comp1', 1 , 6000 ),
  (1 , 'Comp2', 2 , 6000 ),
  (2 , 'Comp2', 2 , 6000 ),
  (3 , 'Comp2', 2 , 6000 ),
  (1 , 'Comp3', 3 , 6000 ),
  (2 , 'Comp3', 3 , 6000 ),
  (3 , 'Comp3', 3 , 6000 ),
  (1 , 'Comp4', 4 , 6000 ),
  (2 , 'Comp4', 4 , 6000 ),
  (3 , 'Comp4', 4 , 6000 )
), d (yyyy_mm_dd, name, id, actual, region) as (values
 ( date '2019-01-01' , 'Comp1' , 1  , 1000 , 'LATAM' ),
 ( date '2019-01-01' , 'Comp1' , 1  ,    0 , 'EU' ),
 ( date '2019-01-02' , 'Comp1' , 1  , 2000 , 'EU' ),
 ( date '2019-01-03' , 'Comp1' , 1  , 4000 , 'EU' ),
 ( date '2019-01-01' , 'Comp2' , 2  , 1000 , 'EU' ),
 ( date '2019-01-02' , 'Comp2' , 2  , 2000 , 'EU' ),
 ( date '2019-01-03' , 'Comp2' , 2  , 3000 , 'EU' ),
 ( date '2019-01-01' , 'Comp3' , 3  , 1000 , 'EU' ),
 ( date '2019-01-02' , 'Comp3' , 3  , 2000 , 'EU' ),
 ( date '2019-01-03' , 'Comp3' , 3  , 8000 , 'EU' ),
 ( date '2019-01-01' , 'Comp4' , 4  , 1000 , 'EU' ),
 ( date '2019-01-02' , 'Comp4' , 4  , 2000 , 'EU' ),
 ( date '2019-02-03' , 'Comp4' , 4  , 3000 , 'EU' )
)
select dr.yyyy_mm_dd, dr.name, dr.id, dr.actual,
       case when dr.running_sum < t.target then 0 else (dr.running_sum - t.target) / 100 end as payment
from t
join (
  select dg.*, sum(actual) over (partition by name order by yyyy_mm_dd) as running_sum
  from (
     select yyyy_mm_dd, name, id, sum(actual) as actual
     from d
     group by yyyy_mm_dd, name, id
  ) dg
) dr on dr.name = t.name
     and month(dr.yyyy_mm_dd) = t.month -- edited to hive equivalent of postgres' extract(month from dr.yyyy_mm_dd) = t.month

从日期中提取月份的方法可能有所不同,但我希望你能明白。

【讨论】:

嗨。在 hive 中,月份提取是这样的:and month(dr.yyyy_mm_dd) = t.month。但是,我每天在输出中获得多家公司。我相信您应该在 dr 子查询中按公司分组。你能编辑一下解决方案吗? 月比较已编辑。根据您的示例输出,我认为您想要“每天有多家公司”。 dr 中没有 group by 是有意的,因为要计算运行总和,您需要保留每家公司的每一行。 partition by 子句将窗口拆分为组,但不聚合它们。 我确实希望同一天有多家公司,但不是同一家id。例如,2020-01-01 将有 3 家公司,但同一家公司不会出现超过一次。在您的解决方案中,输出在一天内多次提供同一家公司。在我的真实数据中,d 确实每天有多个 ids 用于某些公司,但不是全部。我想汇总它并根据示例输出每天保留它。希望我澄清了这一点 我已经编辑了原始问题并更新了示例每日数据以反映每天多家公司。希望这个问题现在有意义。 我更接近于理解您的意思,但是,您更改的示例并未显示两行在日期和名称上相等且在 ID 上不同的任何情况。相反,有一个新的列区域,其值排序不明确。正确的值排序对于计算运行总和很重要(如果 2019-01-01 | Comp1 的目标是 1000 并且有两个具有不同区域的 1000 值怎么办?)。此外,对于任何愿意帮助将样本数据作为 CTE 而不是 ascii-art 表获取的人来说,这将非常方便。【参考方案4】:

我想我现在有了一个可行的解决方案。下面给出了预期的输出。它可能会被优化一点,因为它不是最快的。

SELECT
    x.yyyy_mm_dd,
    x.id,
    x.name,
    x.actual,
    x.target,
    x.actual_to_date,
    CASE WHEN x.actual_to_date > x.target THEN ((x.actual_to_date - x.target) /100) * 1 ELSE 0 END AS payout
FROM(
    SELECT
        daily.yyyy_mm_dd,
        daily.id,
        daily.name,
        daily.actual,
        t.target,
        SUM(daily.actual) OVER (PARTITION BY MONTH(daily.yyyy_mm_dd), daily.id ORDER BY daily.yyyy_mm_dd RANGE UNBOUNDED PRECEDING) AS actual_to_date
    FROM(
        SELECT
            yyyy_mm_dd,
            id,
            name,
            sum(cast(actual as int)) as actual
        FROM
            daily_data_table
        WHERE
            yyyy_mm_dd >= '2020-01-01'
        GROUP BY
            1,2,3
    ) daily
    INNER JOIN
        monthly_target_table t
        ON t.id = daily.id AND t.month = month(daily.yyyy_mm_dd)
    WHERE
        daily.name = 'Comp1'
) x

【讨论】:

以上是关于比较每月数据,同时保持每日粒度的主要内容,如果未能解决你的问题,请参考以下文章

识别数据模型粒度

数据仓库之粒度

在多维数据集设计中接近操作期间的混合粒度日期维度

SQL查询以选择具有改变粒度的记录

“如果可能发生数据丢失,阻止增量部署”的粒度

用替换数据填充缺失数据