【问题标题】:Get distinct consecutive date ranges from overlapping date ranges从重叠的日期范围中获取不同的连续日期范围
【发布时间】:2018-10-16 07:33:43
【问题描述】:

我需要从重叠日期列表中获取彼此不重叠的日期范围列表,并获取重叠期间的硬币总和。我试过用谷歌搜索一个例子,但到目前为止还没有运气。我可能没有使用正确的关键词?

我有一个重叠日期列表

1.1.2018 - 31.1.2018 80
7.1.2018 - 10.1.2018 10
7.1.2018 - 31.1.2018 10
11.1.2018 - 31.1.2018 5
25.1.2018 - 27.1.2018 5
2.2.2018 - 23.2.2018 100

期望的结果是

1.1.2018 - 6.7.2018 80 coins
7.1.2018 - 10.1.2018 100 coins
11.1.2018 - 24.1.2018 95 coins
25.1.2018 - 27.1.2018 100 coins
28.1.2018 - 31.1.2018 95 coins
2.2.2018 - 23.2.2018 100 coins

这是一个应该如何工作的图

|------------------------------|
       |---|
       |-----------------------|
           |-------------------|
                      |---|
                                   |----------------------|
Outcome              
|------|---|----------|---|----|   |----------------------|
   80   100     95     100  95                100

这是我的测试数据

drop table coinsonperiod2;
create table coinsonperiod2(
  id serial,
  startdate date,
  enddate date,
  coins integer,
  userid integer
);
insert into coinsonperiod2 (startdate, enddate, coins,userid) values
  ('2018-01-01','2018-01-31', 80,1)
, ('2018-01-07','2018-01-10', 10,1)
, ('2018-01-07','2018-01-31', 10,1)
, ('2018-01-11','2018-01-31', 5,1)
, ('2018-01-25','2018-01-27', 5,1)
, ('2018-02-02','2018-02-23', 100,2)
, ('2018-01-01','2018-01-31', 80,2)
, ('2018-01-07','2018-01-10', 10,2)
, ('2018-01-07','2018-01-31', 10,2)
, ('2018-01-11','2018-01-31', 5,2)
, ('2018-01-25','2018-01-27', 5,2)
, ('2018-02-02','2018-02-23', 100,3)
; 

更新: 实际上 StephenM 和 joops 的回答不符合我想要的结果。两个答案都显示 enddate 错误。

当一个时期结束时,下一个时期应该在第二天开始(或者如果有间隔则更晚)。在我想要的结果中,1.1.2018-6.1.2018 包括第 6 天。 6th 和 7th 之间没有差距,因为 7th 包含在 7.1.2018-10.1.2018。

更新2: 现在我明白了开放、半开放和封闭间隔之间的区别。在 joops 解决方案中,必须针对半开区间进行计算,但我想要的结果是闭区间。这就是为什么必须减少 enddate 以使结果为闭区间。如果我错了,请纠正我。

我还在示例数据中添加了用户 ID,并进一步修改了 joops 解决方案。 这是给我想要的结果的查询。

with changes AS (
  SELECT
    userid,
    startdate AS tickdate,
    coins,
    1         AS cover
  FROM coinsonperiod2
  UNION ALL
  -- add 1 day to correct intervals into half open intervals, so the calculation is correct
  SELECT
    userid,
    1 + enddate AS tickdate,
    -1 * coins,
    -1          AS cover
  FROM coinsonperiod2
)
, sumchanges  AS (
    SELECT
      userid,
      tickdate,
      SUM(coins) AS change,
      SUM(cover) AS cover
    FROM changes
    GROUP BY tickdate, userid
)
, aggregated AS (
    SELECT
      userid   AS userid,
      tickdate AS startdate,
      lead(tickdate)
      over www AS enddate,
      sum(change)
      OVER www AS cash,
      sum(cover)
      OVER www AS cover
    FROM sumchanges
    WINDOW www AS (
      partition by userid
      ORDER BY tickdate )
)
-- reduce 1 day from the enddate to make closed interval
SELECT
userid
, startdate
, enddate-1 as enddate
, cash
, cover
FROM aggregated
WHERE cover > 0
ORDER BY userid, startdate
;

结果:

【问题讨论】:

  • 非常好的问题,带有示例数据和可视化。我希望所有第一次发帖的人都能在他们的问题上投入那么多精力。
  • 我假设您想要的结果中第一个时期的结束日期有错字(如果我正确解释了您的日期格式,应该是 6.1.2018)
  • 诀窍是将水平触发表示转换为边缘触发,将它们累加并转换回来。
  • 不重叠的部分其实很简单:rextester.com/JMZC97931
  • 添加键域(user_id) 并没有真的改变问题。并且不应该更改问题并在问题主体中添加答案。相反,问一个新问题,(也许回答你自己的问题)

标签: sql postgresql overlap


【解决方案1】:

逻辑是:

  • 在间隔的开始将其值添加到累积总和
  • 在区间的结尾从这个总和中减去它的值
  • 但为了扫描日期变更线,我们必须收集所有(唯一的)日期/时间戳,无论是开始还是停止。

所以重点是:将数据从一系列间隔转换为一系列(开始/停止)事件,并在这些之上聚合。


-- \i tmp.sql

create table coinsonperiod(
  id serial,
  startdate date,
  enddate date,
  coins integer
);
insert into coinsonperiod (startdate, enddate, coins) values
  ('2018-01-01','2018-01-31', 80)
, ('2018-01-07','2018-01-10', 10)
, ('2018-01-07','2018-01-31', 10)
, ('2018-01-11','2018-01-31', 5)
, ('2018-01-25','2018-01-27', 5)
, ('2018-02-02','2018-02-23', 100)
        ;

WITH changes AS (
    SELECT startdate AS tickdate , coins
            , 1 AS cover
    FROM coinsonperiod
    UNION ALL
    -- add 1 day to convert to half-open intervals
    SELECT 1+enddate AS tickdate, -1* coins
            , -1 AS cover
    FROM coinsonperiod
    )
, sumchanges  AS (
        SELECT tickdate, SUM(coins) AS change, SUM(cover) AS cover
        FROM changes
        GROUP BY tickdate
        )
, aggregated AS (
        SELECT
        tickdate AS startdate
        , lead(tickdate) over www AS enddate
        , sum(change) OVER www AS cash
          -- number of covered intervals
        , sum(cover) OVER www AS cover
        FROM sumchanges
        WINDOW www AS (ORDER BY tickdate)
        )
             -- substract one day from enddate to correct back to closed intervals
SELECT startdate, enddate-1 AS enddate, cash, cover
FROM aggregated
WHERE cover > 0
ORDER BY startdate
        ;

【讨论】:

  • -- 将最终结果缩短 1 天以更正结束日期 SELECT startdate, enddate-1, cash, cover FROM aggregated WHERE cover > 0 ORDER BY startdate
  • 正确,但我还是更喜欢半开间隔 ;-)
【解决方案2】:

看起来我发现了一个丑陋的作品

select t1.dt, t1.enddt, sum(coins)
from (
    select distinct cp1.dt, min(cp2.dt) enddt
    from ( select startdate as dt from coinsonperiod union all select enddate as dt from coinsonperiod ) cp1, 
         ( select startdate as dt from coinsonperiod union all select enddate as dt from coinsonperiod ) cp2
    where cp2.dt > cp1.dt
    group by cp1.dt
    order by cp1.dt ) t1, coinsonperiod t2
where t1.dt between t2.startdate and t2.enddate
and t1.enddt between t2.startdate and t2.enddate
group by t1.dt, t1.enddt

输出:

dt         |enddt      |sum |
-----------|-----------|----|
2018-01-01 |2018-01-07 |80  |
2018-01-07 |2018-01-10 |100 |
2018-01-10 |2018-01-11 |90  |
2018-01-11 |2018-01-25 |95  |
2018-01-25 |2018-01-27 |100 |
2018-01-27 |2018-01-31 |95  |
2018-02-02 |2018-02-23 |100 |

与您的输出的唯一区别是我想您忘记了 01/10 和 01/11 之间的时间间隔

【讨论】:

  • 是的。我忘记了 01/10 和 01/11 之间的时间间隔。您的解决方案几乎就是我正在寻找的。如果 startdt 与 enddt 相等,则 enddt 应为“-1 天”。例如:2018-01-01 |2018-01-07 |80 | 2018-01-07 |2018-01-10 |100 |应该变成 2018-01-01 |2018-01-06 |80 | 2018-01-07 |2018-01-10 |100 |
  • @MattiIlvonen - 我特别推荐使用datetime,但也经常使用date,最好将此类区间建模为半开区间,包含 开始日期和专有结束日期。对它们进行推理通常更容易(例如,原始数据中出现的所有日期也出现在您的去重叠间隔中,并且对于每个去重叠间隔,每个结束日期对应于另一个间隔的开始日期(除了开始/结束)
  • @Damien_The_Unbeliever: Postgres 的range 非常适合这种情况,如果您将示例中的开始日期/结束日期值转换为daterange,Postgres 实际上会使其成为半开区间。
【解决方案3】:

好的,所以我将帮助您解决逻辑问题,您可以在网上找到语法。

您可以做的是创建一个临时表并将数据移动到那里,然后选择每一行数据并将每列数据的值存储在声明的变量中。

然后简单地使用游标,并从源表中选择所有数据,然后再次使用普通的大于或小于运算符并按自己的方式获取计数。

简单获取第 1 行第 1 列,与所有其他第 1 列和第 2 列数据进行比较。

【讨论】:

  • RDBMS 的优势在于处理数据集以及能够重新组织/重新排序操作以获得最佳结果。使用游标执行过程逻辑削弱了两个概念。在某些情况下需要临时表和/或游标,但您通常希望先用尽基于集合的方法。
【解决方案4】:

正确答案:

这是我的测试数据

drop table coinsonperiod2;
create table coinsonperiod2(
  id serial,
  startdate date,
  enddate date,
  coins integer,
  userid integer
);
insert into coinsonperiod2 (startdate, enddate, coins,userid) values
  ('2018-01-01','2018-01-31', 80,1)
, ('2018-01-07','2018-01-10', 10,1)
, ('2018-01-07','2018-01-31', 10,1)
, ('2018-01-11','2018-01-31', 5,1)
, ('2018-01-25','2018-01-27', 5,1)
, ('2018-02-02','2018-02-23', 100,2)
, ('2018-01-01','2018-01-31', 80,2)
, ('2018-01-07','2018-01-10', 10,2)
, ('2018-01-07','2018-01-31', 10,2)
, ('2018-01-11','2018-01-31', 5,2)
, ('2018-01-25','2018-01-27', 5,2)
, ('2018-02-02','2018-02-23', 100,3)
; 

更新2: 现在我明白了开放、半开放和封闭间隔之间的区别。在 joops 解决方案中,必须针对半开区间进行计算,但我想要的结果是闭区间。这就是为什么必须减少 enddate 以使结果为闭区间。如果我错了,请纠正我。

我还在示例数据中添加了用户 ID,并进一步修改了 joops 解决方案。 这是给我想要的结果的查询。

with changes AS (
  SELECT
    userid,
    startdate AS tickdate,
    coins,
    1         AS cover
  FROM coinsonperiod2
  UNION ALL
  -- add 1 day to correct intervals into half open intervals, so the calculation is correct
  SELECT
    userid,
    1 + enddate AS tickdate,
    -1 * coins,
    -1          AS cover
  FROM coinsonperiod2
)
, sumchanges  AS (
    SELECT
      userid,
      tickdate,
      SUM(coins) AS change,
      SUM(cover) AS cover
    FROM changes
    GROUP BY tickdate, userid
)
, aggregated AS (
    SELECT
      userid   AS userid,
      tickdate AS startdate,
      lead(tickdate)
      over www AS enddate,
      sum(change)
      OVER www AS cash,
      sum(cover)
      OVER www AS cover
    FROM sumchanges
    WINDOW www AS (
      partition by userid
      ORDER BY tickdate )
)
-- reduce 1 day from the enddate to make closed interval
SELECT
userid
, startdate
, enddate-1 as enddate
, cash
, cover
FROM aggregated
WHERE cover > 0
ORDER BY userid, startdate
;

结果:

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多