SQL - 计算状态在日期范围内保持的日期
SQL - Count dates that a status was held in a date range
我正在尝试确定一个人在某个日期范围内拥有特定身份的日期数。我有三个具有以下(简化)结构的 table:
Table Fields
Calendar Date
DateRange RangeID, StartDate, EndDate
StatusHistory PersonID, Status, Date
日历 table 包含我要考虑计数的日期列表。一个人的状态变化可能在该范围之前、之后或中间被记录下来,或者可能在该范围内多次状态切换。
我愿意:
select PersonID, RangeID, Status, count(*) as DateCount
或者至少有一个具有该结构的结果集。
我在 DB2 for IBM i 上使用 SQL。
使用示例数据编辑:
DateRange table(包含我要考虑的范围)
RangeID StartDate EndDate
+--------+------------+------------+
| A | 2015-01-01 | 2015-01-31 |
| B | 2015-02-06 | 2015-03-05 |
| C | 2015-03-07 | 2015-04-30 |
+--------+------------+------------+
日历table(包含我要计算的日期)
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 table(包含输入或更改人员状态的日期)
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
- 按分组只显示计数 > 0 的组合
获得正确状态的想法是在 <= 日历日期的日期加入 StatusHistory,但不存在比具有相同 PersonID 且 <= 日历的状态的日期大的日期日期。所以基本上这个技巧 select 是一个人(如果有的话)在给定日历日的最后一个现有状态。
版本 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
第二个查询中进行查询,您将看到实际上只有计数 = 0 的行被 return 编辑,因为除了 0 之外查询应该 return 相同的行。
select 已经正确,所需要的只是分组并正确加入/过滤表格。需要分组,因为计数是一个聚合函数(如总和、最小值、最大值等)并且它们在组上工作。您可以想象您只查看 group by 中指定的列,并且它们相同的列被放在一组中,对于其他列,您必须使用聚合函数(您不能在一个单元格中存储多行,除非您使用group_concat (mysql) 或 listagg (oracle) 也是聚合函数。
虽然您通常以平等的方式加入,但这不是必需的。
在你的情况下,你需要使用 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
应该给你想要的..
如果您在 LUW 上,并且可以访问 LEAD
(window 函数很好),我们会更轻松一些,但我们只需要模拟它。
您需要问的第一件事是一个概念性问题:您要计算什么?答案是“天”——是的,你有条件,但这就是你要计算的。所以你的初始 table(FROM
中的那个)实际上是你的日历 table.
接下来我们需要做的是获取 StatusHistory
的下一个范围的开始(请注意,这将是一个独占上限。始终使用 dates/times/timestamps 查询一个独占的上限...事实上,如果你假装 BETWEEN
does not exist) 会更好。 i 上没有 LEAD
,我们将不得不模仿它。首先,我们需要对条目进行索引,为每个人重新开始,并按他们的条目排序:
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)
... 因为我们有一个开放的上限 - 我们 运行 直到“最后一个可能的条目”,并且我们 没有 “最后”条目 - 我们需要 LEFT JOIN
表示 Nxt
(下一个),并且结束日期(重要 - 下一个状态的开始!)对于最后一个条目将为空。这种逻辑是包装在视图中的主要候选者(以提供完整范围的外观 table),并且如果性能是一个问题,则可能构建 MQT。
从这里开始,就很简单了。我们不必担心重复 - 我们加入的方式会解决这个问题 - 范围也会自动重叠。
快速演示:
给定一个看起来像这样的日历 table -
2015-01-01
2015-01-02
2015-01-03
2015-01-04
2015-01-05
... 和这样的范围 table -
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
被忽略,因为它比范围的结尾 greater-than/equal。将更多次与其他相似范围连接起来只会进一步限制所选数据。我们拥有所需的所有零件。
完整的语句最终看起来像这样:
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
,尽管您的测试数据的 none 达到了那个程度。可以将 COALESCE(Nxt.startDate, CURRENT_DATE)
放在 CTE 范围内,但这留作 reader.
的练习
我正在尝试确定一个人在某个日期范围内拥有特定身份的日期数。我有三个具有以下(简化)结构的 table:
Table Fields
Calendar Date
DateRange RangeID, StartDate, EndDate
StatusHistory PersonID, Status, Date
日历 table 包含我要考虑计数的日期列表。一个人的状态变化可能在该范围之前、之后或中间被记录下来,或者可能在该范围内多次状态切换。
我愿意:
select PersonID, RangeID, Status, count(*) as DateCount
或者至少有一个具有该结构的结果集。
我在 DB2 for IBM i 上使用 SQL。
使用示例数据编辑:
DateRange table(包含我要考虑的范围)
RangeID StartDate EndDate
+--------+------------+------------+
| A | 2015-01-01 | 2015-01-31 |
| B | 2015-02-06 | 2015-03-05 |
| C | 2015-03-07 | 2015-04-30 |
+--------+------------+------------+
日历table(包含我要计算的日期)
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 table(包含输入或更改人员状态的日期)
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
- 按分组只显示计数 > 0 的组合
获得正确状态的想法是在 <= 日历日期的日期加入 StatusHistory,但不存在比具有相同 PersonID 且 <= 日历的状态的日期大的日期日期。所以基本上这个技巧 select 是一个人(如果有的话)在给定日历日的最后一个现有状态。
版本 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
第二个查询中进行查询,您将看到实际上只有计数 = 0 的行被 return 编辑,因为除了 0 之外查询应该 return 相同的行。
select 已经正确,所需要的只是分组并正确加入/过滤表格。需要分组,因为计数是一个聚合函数(如总和、最小值、最大值等)并且它们在组上工作。您可以想象您只查看 group by 中指定的列,并且它们相同的列被放在一组中,对于其他列,您必须使用聚合函数(您不能在一个单元格中存储多行,除非您使用group_concat (mysql) 或 listagg (oracle) 也是聚合函数。
虽然您通常以平等的方式加入,但这不是必需的。
在你的情况下,你需要使用 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
应该给你想要的..
如果您在 LUW 上,并且可以访问 LEAD
(window 函数很好),我们会更轻松一些,但我们只需要模拟它。
您需要问的第一件事是一个概念性问题:您要计算什么?答案是“天”——是的,你有条件,但这就是你要计算的。所以你的初始 table(FROM
中的那个)实际上是你的日历 table.
接下来我们需要做的是获取 StatusHistory
的下一个范围的开始(请注意,这将是一个独占上限。始终使用 dates/times/timestamps 查询一个独占的上限...事实上,如果你假装 BETWEEN
does not exist) 会更好。 i 上没有 LEAD
,我们将不得不模仿它。首先,我们需要对条目进行索引,为每个人重新开始,并按他们的条目排序:
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)
... 因为我们有一个开放的上限 - 我们 运行 直到“最后一个可能的条目”,并且我们 没有 “最后”条目 - 我们需要 LEFT JOIN
表示 Nxt
(下一个),并且结束日期(重要 - 下一个状态的开始!)对于最后一个条目将为空。这种逻辑是包装在视图中的主要候选者(以提供完整范围的外观 table),并且如果性能是一个问题,则可能构建 MQT。
从这里开始,就很简单了。我们不必担心重复 - 我们加入的方式会解决这个问题 - 范围也会自动重叠。
快速演示:
给定一个看起来像这样的日历 table -
2015-01-01
2015-01-02
2015-01-03
2015-01-04
2015-01-05
... 和这样的范围 table -
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
被忽略,因为它比范围的结尾 greater-than/equal。将更多次与其他相似范围连接起来只会进一步限制所选数据。我们拥有所需的所有零件。
完整的语句最终看起来像这样:
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
,尽管您的测试数据的 none 达到了那个程度。可以将 COALESCE(Nxt.startDate, CURRENT_DATE)
放在 CTE 范围内,但这留作 reader.