SQL - 计算状态在日期范围内保存的日期

时间:2015-08-04 22:59:46

标签: sql db2

我试图确定某个人持有特定身份的日期范围内的日期数。我有三个表,具有以下(简化)结构:

Table             Fields
 Calendar          Date
 DateRange         RangeID, StartDate, EndDate
 StatusHistory     PersonID, Status, Date

“日历”表包含我要为计数考虑的日期列表。可能在范围之前,之后或中间记录了人的状态变化,或者可能在该范围内多次在状态之间切换。

我想:

select PersonID, RangeID, Status, count(*) as DateCount

或者至少有一个具有该结构的结果集。

我在DB2 for IBM i上使用SQL。

使用示例数据

修改

DateRange表(包含我想要考虑的范围)

 RangeID  StartDate    EndDate
+--------+------------+------------+
| A      | 2015-01-01 | 2015-01-31 |
| B      | 2015-02-06 | 2015-03-05 |
| C      | 2015-03-07 | 2015-04-30 |
+--------+------------+------------+

日历表(包含我想要计算的日期)

 Date            RangeID (not in Calendar table, but shown here for clarity)
+------------+   ----
| 2015-01-05 |
| 2015-01-06 |    A
| 2015-01-07 |
| 2015-01-08 |
                 ----
| 2015-02-05 |
                 ----
| 2015-02-06 |
| 2015-02-07 |    B
| 2015-02-08 |
| 2015-03-05 |
                 ----
| 2015-03-06 |
                 ---- 
| 2015-03-07 |
| 2015-03-08 |
| 2015-04-05 |    C
| 2015-04-06 |
| 2015-04-07 |
| 2015-04-08 |
+------------+   ----

StatusHistory表(包含输入或更改某人的状态的日期)

 PersonID Status  Date
+--------+-------+------------+      Edit for clarification:
| 1      | HAPPY | 2015-01-05 |      While there's only one date  
| 1      | SAD   | 2015-02-07 |      in each of these records, 
| 1      | HAPPY | 2015-04-06 |      a date range is implied. That is,
| 2      | HAPPY | 2015-01-07 |      Person 1 is HAPPY from 2015-01-05
| 3      | SAD   | 2014-10-31 |      to 2015-02-07, then SAD 'til
| 3      | SAD   | 2015-01-07 |      2015-04-06 and HAPPY from then on.
| 3      | HAPPY | 2015-04-05 |
| 3      | SAD   | 2015-04-06 |
| 3      | SAD   | 2015-04-07 |
+--------+-------+------------+

结果集

 PersonID RangeID Status  DateCount
+--------+-------+-------+---------+
| 1      | A     | HAPPY | 4       |
| 1      | B     | HAPPY | 1       |
| 1      | B     | SAD   | 3       |
| 1      | C     | HAPPY | 3       |
| 1      | C     | SAD   | 3       |
| 2      | A     | HAPPY | 2       |
| 2      | B     | HAPPY | 4       |
| 2      | C     | HAPPY | 6       |
| 3      | A     | SAD   | 4       |
| 3      | B     | SAD   | 4       |
| 3      | C     | HAPPY | 1       |
| 3      | C     | SAD   | 5       |
+--------+-------+-------+---------+

3 个答案:

答案 0 :(得分:2)

以下是两个解决方案:

  1. 计算所有组合并对其进行计数,以便显示0
  2. 仅显示包含>的组合0分组
  3. 获得正确状态的想法是在StatusHistory< =日历日期的日期加入,但是没有比具有相同PersonID和< =日历的状态更大的日期日期。所以基本上这个技巧选择给定日历日的人(如果有的话)的最后一个现有状态。

    版本1 :在PostgreSQL和Oracle(SQL Fiddle)上测试。

    SELECT
       p.PersonID,
       r.RangeID,
       s.Status,
       (SELECT COUNT(*) FROM Calendar c WHERE c.Date_ BETWEEN r.StartDate AND r.EndDate AND
          EXISTS(SELECT * FROM StatusHistory h WHERE
             h.PersonID = p.PersonID AND h.Status = s.Status AND h.Date_ <= c.Date_ AND
             NOT EXISTS(SELECT * FROM StatusHistory z WHERE
                z.PersonID = p.PersonID AND z.Date_ <= c.Date_ AND z.Date_ > h.Date_))
       ) AS Amount
    FROM
       (SELECT DISTINCT PersonID FROM StatusHistory) p,
       (SELECT RangeID, StartDate, EndDate FROM DateRange) r,
       (SELECT DISTINCT Status FROM StatusHistory) s
    ;
    

    版本2 :或者,如果您不想要0(SQL Fiddle),则可以修改旧解决方案:

    SELECT
       h.PersonID,
       r.RangeID,
       h.Status,
       COUNT(*)
    FROM
       Calendar c,
       DateRange r,
       StatusHistory h
    WHERE
       c.Date_ BETWEEN r.StartDate AND r.EndDate AND
       h.Date_ <= c.Date_ AND
       NOT EXISTS (SELECT s.Date_ FROM StatusHistory s WHERE
          s.Date_ <= c.Date_ AND s.Date_ > h.Date_ AND s.PersonID = h.PersonID)
    GROUP BY
       h.PersonID,
       r.RangeID,
       h.Status
    ;
    

    如果您对第二个查询进行第一次查询MINUS,您将看到确实只返回count = 0的行,因为除了0计数之外,查询应该返回相同的行。

    选择已经正确,所需的只是分组并正确加入/过滤表格。需要分组是因为count是一个聚合函数(如sum,min,max等),它们适用于组。您可以想象您只查看group by中指定的列以及它们放在一个组中的相同位置,对于其他列,您必须使用聚合函数(除非您使用,否则不能在一个单元格中存储多行group_concat(mysql)或listagg(oracle),它们也是聚合函数)。

答案 1 :(得分:2)

如果您使用LUW,并且可以访问LEAD(窗口函数很好),我们可以更轻松地完成此操作,但我们只需要模拟它。

您首先要问的是一个概念性问题:您想要算什么?答案是&#34;天&#34; - 是的,你有条件,但这就是你想要计算的东西。所以你的初始表(FROM中的那个)实际上就是你的日历表。

接下来我们需要做的是获取StatusHistory的下一个开始范围(请注意,这将是一个独占上限。始终查询日期/时间/时间戳与一个专属的上限...事实上,如果你假装BETWEEN does not exist)它会更好。我没有LEAD,我们必须模仿它。首先,我们需要为条目编制索引,从每个人开始,并按条目排序:

StatusHistoryIndex (personId, status, startDate, index) 
                AS (SELECT personId, status, startDate,
                           ROW_NUMBER() OVER (PARTITION BY personId ORDER BY startDate) 
                    FROM StatusHistory)

...接下来,我们需要使用它来连接&#34;当前&#34;排在&#34; next&#34;一,由生成的索引:

StatusHistoryRange (personId, status, startDate, endDate)
                AS (SELECT Curr.personId, Curr.status, Curr.startDate,
                           Nxt.startDate
                    FROM StatusHistoryIndex Curr
                    LEFT JOIN StatusHistoryIndex Nxt
                           ON Nxt.personId = Curr.personId
                              AND Nxt.index = Curr.index + 1)   

....因为我们有一个开放的上限 - 我们一直跑到最后可能的进入&#34;,我们不 a&# 34;最后&#34;输入 - 我们需要LEFT JOIN Nxt(下一个),结束日期(重要 - 下一个状态的开始!)对于最后一个条目将为null。这种逻辑是包装在视图中的主要候选者(为了给出完整范围表的外观),并且如果性能是个问题,可能会构建MQT。

从这里开始,它很简单。我们不必担心重复 - 我们将加入的方式负责这一点 - 并且范围也将自动重叠。

快速演示:
给定一个看起来像这样的日历表 -

2015-01-01
2015-01-02
2015-01-03
2015-01-04
2015-01-05

...和这样的范围表 -

2015-01-02   2015-01-05

...然后加入只能限制所选的行,就像它是WHERE子句一样:

SELECT date
FROM Calendar
JOIN Range
  ON Calendar.date >= Range.start
     AND Calendar.date < Range.end

会产生:

2015-01-02
2015-01-03
2015-01-04

在排除的行中,2015-01-01被忽略,因为它小于范围的开头,2015-01-05被忽略,因为它大于/等于范围结束。使用其他类似范围加入更多次数只会进一步限制所选数据。我们拥有所需的所有部分。


完整的陈述最终看起来像这样:

WITH StatusHistoryIndex (personId, status, startDate, index) 
                     AS (SELECT personId, status, startDate,
                                ROW_NUMBER() OVER (PARTITION BY personId ORDER BY startDate) 
                     FROM StatusHistory),
     StatusHistoryRange (personId, status, startDate, endDate)
                     AS (SELECT Curr.personId, Curr.status, Curr.startDate,
                                Nxt.startDate
                         FROM StatusHistoryIndex Curr
                         LEFT JOIN StatusHistoryIndex Nxt
                                ON Nxt.personId = Curr.personId
                                   AND Nxt.index = Curr.index + 1)

SELECT SHR.personId, DateRange.id, SHR.status, COUNT(*)
FROM Calendar
JOIN DateRange
  ON Calendar.calendarDate >= DateRange.startRange
     AND Calendar.calendarDate < DateRange.endRange
JOIN StatusHistoryRange SHR
  ON Calendar.calendarDate >= SHR.startDate
     AND (Calendar.calendarDate < SHR.endDate OR SHR.endDate IS NULL)
GROUP BY SHR.personId, DateRange.id, SHR.status
ORDER BY SHR.personId, DateRange.id, SHR.status

SQL Fiddle Example
(请注意,我的数字与您的示例结果有很大不同。鉴于起始数据,我确信我得到的数字是正确的结果,但如果我错过了某些内容,请告诉我)

您没有指定,但我将DateRange中的结束日期视为独占上限,您可能需要调整(应该存储独占在这里上限。) 我也没有限制状态的结束日期。据推测,这可能是CURRENT_DATE,尽管你的测试数据都没有那么远。可以将COALESCE(Nxt.startDate, CURRENT_DATE)放在CTE范围内,但这是留给读者的练习。

答案 2 :(得分:0)

虽然你平时加入平等,但并不是必需的。

在您的情况下,您将要使用BETWEEN

select PersonID, RangeID, Status, count(*) as DateCount
from Calendar c
     join DateRange d on c.date between d.StartDate and d.EndDate
     join StatusHistory s on s.date between d.StartDate and d.EndDate
group by s.PersonID, d.RangeID, s.Status  

应该给你你想要的东西..