SQL 服务器 - 重叠数据的累计总和 - 获取总和达到给定值的日期
SQL Server - cumulative sum on overlapping data - getting date that sum reaches a given value
在我们公司,我们的客户执行我们在不同 table 登录的各种活动 - 面试出席、课程出席和其他一般活动。
我有一个数据库视图,它结合了所有这些 table 的数据,为我们提供了如下所示的 ActivityView。
正如您所看到的一些活动重叠 - 例如,在参加面试时,客户可能一直在执行简历更新 activity。
+----------------------+---------------+---------------------+-------------------+
| activity_client_id | activity_type | activity_start_date | activity_end_date |
+----------------------+---------------+---------------------+-------------------+
| 112 | Interview | 2015-06-01 09:00 | 2015-06-01 11:00 |
| 112 | CV updating | 2015-06-01 09:30 | 2015-06-01 11:30 |
| 112 | Course | 2015-06-02 09:00 | 2015-06-02 16:00 |
| 112 | Interview | 2015-06-03 09:00 | 2015-06-03 10:00 |
+----------------------+---------------+---------------------+-------------------+
每个客户都有一个"Sign Up Date",记录在客户table上,这是他们加入我们计划的时间。这是我们的示例客户:
+-----------+---------------------+
| client_id | client_sign_up_date |
+-----------+---------------------+
| 112 | 2015-05-20 |
+-----------+---------------------+
我需要创建一个将显示以下列的报告:
+-----------+---------------------+--------------------------------------------+
| client_id | client_sign_up_date | date_client_completed_5_hours_of_activity |
+-----------+---------------------+--------------------------------------------+
我们需要这份报告来了解我们的计划的有效性。该计划的一个重要目标是让每个客户尽快完成至少 5 小时的 activity。
所以这份报告会告诉我们每个客户从注册到达到这个数字需要多长时间。
更棘手的是,当我们计算 5 小时的总时长 activity 时,我们必须扣除重叠活动:
在上面的示例数据中,客户在 09:00 和 11:00 之间参加了面试。
同一天,他们还进行了 CV 更新 activity,从 09:30 到 11:30。
对于我们的计算,这将使他们在 2.5 小时(150 分钟)的一天中总共 activity - 我们只计算 30 分钟的简历更新,因为面试重叠到 11:00。
因此我们示例客户的报告将给出以下结果:
+-----------+---------------------+--------------------------------------------+
| client_id | client_sign_up_date | date_client_completed_5_hours_of_activity |
+-----------+---------------------+--------------------------------------------+
| 112 | 2015-05-20 | 2015-06-02 |
+-----------+---------------------+--------------------------------------------+
所以我的问题是如何使用 select 语句创建报告?
我可以通过编写一个循环遍历视图并将结果写入报告 table 的存储过程来弄清楚如何做到这一点。
但我更愿意避免使用存储过程,并有一个 select 语句可以即时给我报告。
我正在使用 SQL Server 2005。
这是一种方法:
;WITH CTErn AS (
SELECT activity_client_id, activity_type,
activity_start_date, activity_end_date,
ROW_NUMBER() OVER (PARTITION BY activity_client_id
ORDER BY activity_start_date) AS rn
FROM activities
),
CTEdiff AS (
SELECT c1.activity_client_id, c1.activity_type,
x.activity_start_date, c1.activity_end_date,
DATEDIFF(mi, x.activity_start_date, c1.activity_end_date) AS diff,
ROW_NUMBER() OVER (PARTITION BY c1.activity_client_id
ORDER BY x.activity_start_date) AS seq
FROM CTErn AS c1
LEFT JOIN CTErn AS c2 ON c1.rn = c2.rn + 1
CROSS APPLY (SELECT CASE
WHEN c1.activity_start_date < c2.activity_end_date
THEN c2.activity_end_date
ELSE c1.activity_start_date
END) x(activity_start_date)
)
SELECT TOP 1 client_id, client_sign_up_date, activity_start_date,
hoursOfActivicty
FROM CTEdiff AS c1
INNER JOIN clients AS c2 ON c1.activity_client_id = c2.client_id
CROSS APPLY (SELECT SUM(diff) / 60.0
FROM CTEdiff AS c3
WHERE c3.seq <= c1.seq) x(hoursOfActivicty)
WHERE hoursOfActivicty >= 5
ORDER BY seq
常用 Table 表达式 和 ROW_NUMBER()
是在 SQL Server 2005 中引入的,因此上述查询应该适用于该版本。
第一个 CTE
,即 CTErn
,产生以下输出:
client_id activity_type start_date end_date rn
112 Interview 2015-06-01 09:00 2015-06-01 11:00 1
112 CV updating 2015-06-01 09:30 2015-06-01 11:30 2
112 Course 2015-06-02 09:00 2015-06-02 16:00 3
112 Interview 2015-06-03 09:00 2015-06-03 10:00 4
第二个CTE
,即CTEdiff
,使用上面的table表达式来计算每条记录的时间差,同时考虑到与前一条记录的任何重叠:
client_id activity_type start_date end_date diff seq
112 Interview 2015-06-01 09:00 2015-06-01 11:00 120 1
112 CV updating 2015-06-01 11:00 2015-06-01 11:30 30 2
112 Course 2015-06-02 09:00 2015-06-02 16:00 420 3
112 Interview 2015-06-03 09:00 2015-06-03 10:00 60 4
最终查询计算时差累计和,选择第一个超过activity5小时的记录。
以上查询适用于 简单 间隔重叠,即当 activity 的结束日期与下一个 activity 的开始日期重叠时.
参见SQLFiddlehere。
with tbl as (
-- this will generate daily merged ovelaping time
select distinct
a.id
,(
select min(x.starttime)
from act x
where x.id=a.id and ( x.starttime between a.starttime and a.endtime
or a.starttime between x.starttime and x.endtime )
) start1
,(
select max(x.endtime)
from act x
where x.id=a.id and ( x.endtime between a.starttime and a.endtime
or a.endtime between x.starttime and x.endtime )
) end1
from act a
), tbl2 as
(
-- this will add minute and total minute column
select
*
,datediff(mi,t.start1,t.end1) mi
,(select sum(datediff(mi,x.start1,x.end1)) from tbl x where x.id=t.id and x.end1<=t.end1) totalmi
from tbl t
), tbl3 as
(
-- now final query showing starttime and endtime for 5 hours other wise null in case not completed 5(300 minutes) hours
select
t.id
,min(t.start1) starttime
,min(case when t.totalmi>300 then t.end1 else null end) endtime
from tbl2 t
group by t.id
)
-- final result
select *
from tbl3
where endtime is not null
几何方法
对于 another issue,迄今为止我采用了几何方法
包装。也就是说,我将日期和时间转换为 sql 几何
键入并使用 geometry::UnionAggregate
合并范围。
我认为这在 sql-server 2005 中行不通。但是你的
问题是如此有趣的谜题,我想看看
几何方法是否有效。所以任何未来
用户 运行 进入此问题后可以访问
版本可以考虑。
代码说明
在'numbers'中:
- 我构建了一个 table 表示一个序列
- 用你最喜欢的方式交换它来制作数字table。
- 对于联合操作,您永远不需要比
你原来的table,所以我只是用它作为基础来构建它。
在'mergeLines'中:
- 我将日期转换为浮点数并使用这些浮点数
创建几何点。
- 然后我通过 STUnion 和 STEnvelope 连接这些点。
- 最后,我通过 UnionAggregate 合并了所有这些行。所结果的
'lines' 几何对象可能包含多条线,但如果它们
重叠,他们变成一条线。
在'redate'中:
- 我使用数字 CTE 来提取 'lines' 中的各个行。
- 我将这些行包起来,这确保了这些行被存储
仅作为它的两个端点。
- 我读取端点 x 值并将它们转换回它们的时间
表示(这通常是最终目标,但您需要更多)。
- 我计算 activity 开始和之间的分钟差
结束日期(我先在几秒钟内完成,然后除以 60
为了精度问题)。
- 我计算每行这些分钟的累计总和。
在外部查询中:
- 我将之前的累计分钟数总和与每个当前行对齐
- 我过滤了达到 5 小时目标但
前几分钟显示前一行的 5 小时目标
没有遇到。
- 然后我计算用户在当前行范围内的位置
遇见了5个小时,不仅到了5个小时的约会
目标已达到,但时间准确。
代码
with
numbers as (
select row_number() over (order by (select null)) i
from @activities -- where I put your data
),
mergeLines as (
select activity_client_id,
lines = geometry::UnionAggregate(line)
from @activities
cross apply (select
startP = geometry::Point(convert(float,activity_start_date), 0, 0),
stopP = geometry::Point(convert(float,activity_end_date), 0, 0)
) pointify
cross apply (select line = startP.STUnion(stopP).STEnvelope()) lineify
group by activity_client_id
),
redate as (
select client_id = activity_client_id,
activities_start_date,
activities_end_date,
minutes,
rollingMinutes = sum(minutes) over(
partition by activity_client_id
order by activities_start_date
rows between unbounded preceding and current row
)
from mergeLines ml
join numbers n on n.i between 1 and ml.lines.STNumGeometries()
cross apply (select line = ml.lines.STGeometryN(i).STEnvelope()) l
cross apply (select
activities_start_date = convert(datetime, l.line.STPointN(1).STX),
activities_end_date = convert(datetime, l.line.STPointN(3).STX)
) unprepare
cross apply (select minutes =
round(datediff(s, activities_start_date, activities_end_date) / 60.0,0)
) duration
)
select client_id,
activities_start_date,
activities_end_date,
met_5hr_goal = dateadd(minute, (60 * 5) - prevRoll, activities_start_date)
from (
select *,
prevRoll = lag(rollingMinutes) over (
partition by client_id
order by rollingMinutes
)
from redate
) ranker
where rollingMinutes >= 60 * 5
and prevRoll < 60 * 5;
在我们公司,我们的客户执行我们在不同 table 登录的各种活动 - 面试出席、课程出席和其他一般活动。 我有一个数据库视图,它结合了所有这些 table 的数据,为我们提供了如下所示的 ActivityView。 正如您所看到的一些活动重叠 - 例如,在参加面试时,客户可能一直在执行简历更新 activity。
+----------------------+---------------+---------------------+-------------------+
| activity_client_id | activity_type | activity_start_date | activity_end_date |
+----------------------+---------------+---------------------+-------------------+
| 112 | Interview | 2015-06-01 09:00 | 2015-06-01 11:00 |
| 112 | CV updating | 2015-06-01 09:30 | 2015-06-01 11:30 |
| 112 | Course | 2015-06-02 09:00 | 2015-06-02 16:00 |
| 112 | Interview | 2015-06-03 09:00 | 2015-06-03 10:00 |
+----------------------+---------------+---------------------+-------------------+
每个客户都有一个"Sign Up Date",记录在客户table上,这是他们加入我们计划的时间。这是我们的示例客户:
+-----------+---------------------+
| client_id | client_sign_up_date |
+-----------+---------------------+
| 112 | 2015-05-20 |
+-----------+---------------------+
我需要创建一个将显示以下列的报告:
+-----------+---------------------+--------------------------------------------+
| client_id | client_sign_up_date | date_client_completed_5_hours_of_activity |
+-----------+---------------------+--------------------------------------------+
我们需要这份报告来了解我们的计划的有效性。该计划的一个重要目标是让每个客户尽快完成至少 5 小时的 activity。 所以这份报告会告诉我们每个客户从注册到达到这个数字需要多长时间。
更棘手的是,当我们计算 5 小时的总时长 activity 时,我们必须扣除重叠活动:
在上面的示例数据中,客户在 09:00 和 11:00 之间参加了面试。
同一天,他们还进行了 CV 更新 activity,从 09:30 到 11:30。
对于我们的计算,这将使他们在 2.5 小时(150 分钟)的一天中总共 activity - 我们只计算 30 分钟的简历更新,因为面试重叠到 11:00。
因此我们示例客户的报告将给出以下结果:
+-----------+---------------------+--------------------------------------------+
| client_id | client_sign_up_date | date_client_completed_5_hours_of_activity |
+-----------+---------------------+--------------------------------------------+
| 112 | 2015-05-20 | 2015-06-02 |
+-----------+---------------------+--------------------------------------------+
所以我的问题是如何使用 select 语句创建报告? 我可以通过编写一个循环遍历视图并将结果写入报告 table 的存储过程来弄清楚如何做到这一点。 但我更愿意避免使用存储过程,并有一个 select 语句可以即时给我报告。
我正在使用 SQL Server 2005。
这是一种方法:
;WITH CTErn AS (
SELECT activity_client_id, activity_type,
activity_start_date, activity_end_date,
ROW_NUMBER() OVER (PARTITION BY activity_client_id
ORDER BY activity_start_date) AS rn
FROM activities
),
CTEdiff AS (
SELECT c1.activity_client_id, c1.activity_type,
x.activity_start_date, c1.activity_end_date,
DATEDIFF(mi, x.activity_start_date, c1.activity_end_date) AS diff,
ROW_NUMBER() OVER (PARTITION BY c1.activity_client_id
ORDER BY x.activity_start_date) AS seq
FROM CTErn AS c1
LEFT JOIN CTErn AS c2 ON c1.rn = c2.rn + 1
CROSS APPLY (SELECT CASE
WHEN c1.activity_start_date < c2.activity_end_date
THEN c2.activity_end_date
ELSE c1.activity_start_date
END) x(activity_start_date)
)
SELECT TOP 1 client_id, client_sign_up_date, activity_start_date,
hoursOfActivicty
FROM CTEdiff AS c1
INNER JOIN clients AS c2 ON c1.activity_client_id = c2.client_id
CROSS APPLY (SELECT SUM(diff) / 60.0
FROM CTEdiff AS c3
WHERE c3.seq <= c1.seq) x(hoursOfActivicty)
WHERE hoursOfActivicty >= 5
ORDER BY seq
常用 Table 表达式 和 ROW_NUMBER()
是在 SQL Server 2005 中引入的,因此上述查询应该适用于该版本。
第一个 CTE
,即 CTErn
,产生以下输出:
client_id activity_type start_date end_date rn
112 Interview 2015-06-01 09:00 2015-06-01 11:00 1
112 CV updating 2015-06-01 09:30 2015-06-01 11:30 2
112 Course 2015-06-02 09:00 2015-06-02 16:00 3
112 Interview 2015-06-03 09:00 2015-06-03 10:00 4
第二个CTE
,即CTEdiff
,使用上面的table表达式来计算每条记录的时间差,同时考虑到与前一条记录的任何重叠:
client_id activity_type start_date end_date diff seq
112 Interview 2015-06-01 09:00 2015-06-01 11:00 120 1
112 CV updating 2015-06-01 11:00 2015-06-01 11:30 30 2
112 Course 2015-06-02 09:00 2015-06-02 16:00 420 3
112 Interview 2015-06-03 09:00 2015-06-03 10:00 60 4
最终查询计算时差累计和,选择第一个超过activity5小时的记录。
以上查询适用于 简单 间隔重叠,即当 activity 的结束日期与下一个 activity 的开始日期重叠时.
参见SQLFiddlehere。
with tbl as (
-- this will generate daily merged ovelaping time
select distinct
a.id
,(
select min(x.starttime)
from act x
where x.id=a.id and ( x.starttime between a.starttime and a.endtime
or a.starttime between x.starttime and x.endtime )
) start1
,(
select max(x.endtime)
from act x
where x.id=a.id and ( x.endtime between a.starttime and a.endtime
or a.endtime between x.starttime and x.endtime )
) end1
from act a
), tbl2 as
(
-- this will add minute and total minute column
select
*
,datediff(mi,t.start1,t.end1) mi
,(select sum(datediff(mi,x.start1,x.end1)) from tbl x where x.id=t.id and x.end1<=t.end1) totalmi
from tbl t
), tbl3 as
(
-- now final query showing starttime and endtime for 5 hours other wise null in case not completed 5(300 minutes) hours
select
t.id
,min(t.start1) starttime
,min(case when t.totalmi>300 then t.end1 else null end) endtime
from tbl2 t
group by t.id
)
-- final result
select *
from tbl3
where endtime is not null
几何方法
对于 another issue,迄今为止我采用了几何方法
包装。也就是说,我将日期和时间转换为 sql 几何
键入并使用 geometry::UnionAggregate
合并范围。
我认为这在 sql-server 2005 中行不通。但是你的 问题是如此有趣的谜题,我想看看 几何方法是否有效。所以任何未来 用户 运行 进入此问题后可以访问 版本可以考虑。
代码说明
在'numbers'中:
- 我构建了一个 table 表示一个序列
- 用你最喜欢的方式交换它来制作数字table。
- 对于联合操作,您永远不需要比 你原来的table,所以我只是用它作为基础来构建它。
在'mergeLines'中:
- 我将日期转换为浮点数并使用这些浮点数 创建几何点。
- 然后我通过 STUnion 和 STEnvelope 连接这些点。
- 最后,我通过 UnionAggregate 合并了所有这些行。所结果的 'lines' 几何对象可能包含多条线,但如果它们 重叠,他们变成一条线。
在'redate'中:
- 我使用数字 CTE 来提取 'lines' 中的各个行。
- 我将这些行包起来,这确保了这些行被存储 仅作为它的两个端点。
- 我读取端点 x 值并将它们转换回它们的时间 表示(这通常是最终目标,但您需要更多)。
- 我计算 activity 开始和之间的分钟差 结束日期(我先在几秒钟内完成,然后除以 60 为了精度问题)。
- 我计算每行这些分钟的累计总和。
在外部查询中:
- 我将之前的累计分钟数总和与每个当前行对齐
- 我过滤了达到 5 小时目标但 前几分钟显示前一行的 5 小时目标 没有遇到。
- 然后我计算用户在当前行范围内的位置 遇见了5个小时,不仅到了5个小时的约会 目标已达到,但时间准确。
代码
with
numbers as (
select row_number() over (order by (select null)) i
from @activities -- where I put your data
),
mergeLines as (
select activity_client_id,
lines = geometry::UnionAggregate(line)
from @activities
cross apply (select
startP = geometry::Point(convert(float,activity_start_date), 0, 0),
stopP = geometry::Point(convert(float,activity_end_date), 0, 0)
) pointify
cross apply (select line = startP.STUnion(stopP).STEnvelope()) lineify
group by activity_client_id
),
redate as (
select client_id = activity_client_id,
activities_start_date,
activities_end_date,
minutes,
rollingMinutes = sum(minutes) over(
partition by activity_client_id
order by activities_start_date
rows between unbounded preceding and current row
)
from mergeLines ml
join numbers n on n.i between 1 and ml.lines.STNumGeometries()
cross apply (select line = ml.lines.STGeometryN(i).STEnvelope()) l
cross apply (select
activities_start_date = convert(datetime, l.line.STPointN(1).STX),
activities_end_date = convert(datetime, l.line.STPointN(3).STX)
) unprepare
cross apply (select minutes =
round(datediff(s, activities_start_date, activities_end_date) / 60.0,0)
) duration
)
select client_id,
activities_start_date,
activities_end_date,
met_5hr_goal = dateadd(minute, (60 * 5) - prevRoll, activities_start_date)
from (
select *,
prevRoll = lag(rollingMinutes) over (
partition by client_id
order by rollingMinutes
)
from redate
) ranker
where rollingMinutes >= 60 * 5
and prevRoll < 60 * 5;