我试图确定某个人持有特定身份的日期范围内的日期数。我有三个表,具有以下(简化)结构:
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 |
+--------+-------+-------+---------+
答案 0 :(得分:2)
以下是两个解决方案:
获得正确状态的想法是在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
应该给你你想要的东西..