SQL Server - 重叠数据的累积总和 - 获取总和达到给定值的日期

时间:2015-06-04 10:21:53

标签: sql-server sql-server-2005 sum overlapping

在我们公司,我们的客户执行各种活动,我们登录不同的表格 - 访谈出勤,课程出勤和其他一般活动。 我有一个数据库视图,来自所有这些表的联合数据,为我们提供了这样的ActivityView。 正如您所看到的,某些活动重叠 - 例如,在参加面试时,客户可能正在执行CV更新活动。

+----------------------+---------------+---------------------+-------------------+
| 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  |
+----------------------+---------------+---------------------+-------------------+

每个客户都有一个"注册日期",记录在客户端表上,即他们加入我们的程序时。这是我们的样本客户端:

+-----------+---------------------+
| client_id | client_sign_up_date |
+-----------+---------------------+
|       112 | 2015-05-20          |
+-----------+---------------------+

我需要创建一个显示以下列的报告:

+-----------+---------------------+--------------------------------------------+
| client_id | client_sign_up_date | date_client_completed_5_hours_of_activity |
+-----------+---------------------+--------------------------------------------+

我们需要此报告才能了解我们的计划是否有效。该计划的一个重要目标是让每个客户尽快完成至少5个小时的活动。 因此,本报告将告诉我们注册需要多长时间才能让每个客户实现这一数字。

这更加棘手的是,当我们计算5个小时的总活动时,我们必须打折重叠活动:

在上面的样本数据中,客户参加了09:00至11:00之间的访谈 同一天,他们还在09:30至11:30进行了CV更新活动。 对于我们的计算,这将为他们提供2.5小时(150分钟)当天的总活动 - 我们只计算30分钟的简历更新,因为面试将其重叠到11:00。

因此,我们的示例客户端的报告将给出以下结果:

+-----------+---------------------+--------------------------------------------+
| client_id | client_sign_up_date | date_client_completed_5_hours_of_activity |
+-----------+---------------------+--------------------------------------------+
|       112 | 2015-05-20          | 2015-06-02                                 |
+-----------+---------------------+--------------------------------------------+

所以我的问题是如何使用select语句创建报告? 我可以通过编写一个将遍历视图并将结果写入报表的存储过程来解决如何执行此操作的方法。 但是我更希望避免使用存储过程并使用select语句来直接提供报告。

我正在使用SQL Server 2005.

3 个答案:

答案 0 :(得分:2)

请参阅SQL Fiddle here

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

答案 1 :(得分:1)

这是一种方法:

;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
SQL Server 2005引入了

公用表表达式ROW_NUMBER(),因此上述查询应该适用于该版本。

Demo here

第一个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,使用上面的表格来计算每条记录的时差,同时考虑到与之前记录的任何重叠:

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

最终查询计算时间差的累积总和,并选择超过5小时活动的第一条记录。

上述查询适用于简单间隔重叠,即仅当活动的结束日期与下一个活动的开始日期重叠时。

答案 2 :(得分:0)

一种几何方法

对于another issue,到目前为止,我采用了几何方法 填料。即,我将日期和时间转换为sql几何 输入并利用geometry::UnionAggregate合并范围。

我认为这不能在sql-server 2005中使用。但是您的 这个问题真是一个有趣的难题,我想看看 几何方法是否可行。所以任何未来 遇到此问题的用户可以访问以后 版本可以考虑。

代码说明

在“数字”中:

  • 我建立了一个表示序列的表
  • 以您最喜欢的方式将其替换成数字表。
  • 对于联合操作,您将永远不需要更多的行 您原始的表格,所以我只是以它为基础来构建它。

在“ mergeLines”中:

  • 我将日期转换为浮点数并使用这些浮点数 创建几何点。
  • 然后我通过STUnion和STEnvelope连接这些点。
  • 最后,我通过UnionAggregate合并所有这些行。所结果的 “线”几何对象可能包含多条线,但是如果它们 重叠,它们变成一行。

在“重命名”中:

  • 我使用数字CTE提取“行”中的各个行。
  • 我将这些行包裹起来,以确保在此处存储行 仅作为其两个端点。
  • 我读取了端点x值并将其转换回其时间 表示形式(这通常是最终目标,但您需要更多)。
  • 我计算了活动开始与活动之间的分钟差 结束日期(我首先以秒为单位,然后除以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;