按小时分组的开始和结束时间的差异

时间:2013-01-07 13:10:11

标签: sql oracle oracle10g group-by date-arithmetic

我的表格有类似下面的信息

Emp  Date       START_TIME            END_TIME             Code     Minutes
---  --------   -------------------   -------------------  ----     -------
 E1  11/1/2012  11/1/2012 6:55:00 AM  11/1/2012 7:01:00 AM  C1       6
 E1  11/1/2012  11/1/2012 6:57:00 AM  11/1/2012 8:01:00 AM  C2       64
 E2  11/1/2012  11/1/2012 6:57:00 AM  11/1/2012 8:00:00 AM  C2       63
 E1  11/2/2012  11/2/2012 7:35:00 AM  11/2/2012 8:01:00 AM  C1       26

预期输出

Date       Code  Range                        Minutes
---------  ----  -----------------------      ------- 
11/1/2012   C1   6:30:00 AM-7:00:00 AM           5
11/1/2012   C1   7:00:00 AM-7:30:00 AM           1
11/1/2012   C2   6:30:00 AM-7:00:00 AM           6   
11/1/2012   C2   7:00:00 AM-7:30:00 AM           60
11/1/2012   C2   7:30:00 AM-8:00:00 AM           60
11/1/2012   C2   8:00:00 AM-8:30:00 AM           1
11/2/2012   C1   7:30:00 AM-8:00:00 AM           25
11/2/2012   C1   8:00:00 AM-8:30:00 AM           1

离开Emp字段,我希望按日期分组,并且代码的总时间分别为30分钟。我所拥有的限制是使用select语句实现这一点,即仅通过SQL查询而不允许使用PL / SQL 。提前谢谢!

5 个答案:

答案 0 :(得分:2)

涉及模型条款的解决方案。

首先计算每个条目我们需要的30分钟块数。

SQL> select emp, start_time, end_time, code,
  2                      trunc(start_time, 'mi')
  3                      - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440) start_block,
  4                      ceil(2*24*(end_time-(trunc(start_time, 'mi')
  5                      - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440)))) blocks
  6                 from tab f
  7  /

EM START_TIME             END_TIME               CO START_BLOCK                BLOCKS
-- ---------------------- ---------------------- -- ---------------------- ----------
E1 11/01/2012 06:55:00 am 11/01/2012 07:01:00 am C1 11/01/2012 06:30:00 am          2
E1 11/01/2012 06:57:00 am 11/01/2012 08:01:00 am C2 11/01/2012 06:30:00 am          4
E2 11/01/2012 06:57:00 am 11/01/2012 08:00:00 am C2 11/01/2012 06:30:00 am          3
E1 11/02/2012 07:35:00 am 11/02/2012 08:01:00 am C1 11/02/2012 07:30:00 am          2

现在,我们使用model子句生成行以将其分解为30分钟。

SQL> with foo as (select rownum id, emp, start_time, end_time, code,
  2                      trunc(start_time, 'mi')
  3                      - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440) start_block,
  4                      ceil(2*24*(end_time-(trunc(start_time, 'mi')
  5                      - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440)))) blocks
  6                 from tab f)
  7  select trunc(start_time) thedate, code, emp, range, minutes
  8    from foo
  9   model partition by(id)
 10         dimension by(0 as f)
 11         measures(code, emp, start_time, end_time, start_block, blocks,
 12                  sysdate as start_range,
 13                  sysdate as end_range,
 14                  cast(0 as number) minutes,
 15                  cast('' as varchar2(50)) range)
 16         rules (start_range [for f from 0 to blocks[0]-1  increment 1]  = start_block[0] + (30*cv(f)/1440),
 17                end_range[any]  =  start_range[cv()] + (30/1440),
 18                code[any]  =  code[0],
 19                emp[any]   =  emp[0],
 20                start_time[any]  =  start_time[0],
 21                end_time[any]  =  end_time[0],
 22                range [any] = to_char(start_range[cv()], 'dd/mm/yyyy hh:mi:ss am') || ' - ' || to_char(end_range[cv()], 'dd/mm/yyyy hh24:mi:ss am'),
 23                minutes [any]   = case
 24                                    when start_time[0] between start_range[cv()] and end_range[cv()]
 25                                    then 1440 *(end_range[cv()] - start_time[0])
 26                                    when end_time[0] between start_range[cv()] and end_range[cv()]
 27                                    then 1440 *(end_time[0] - start_range[cv()])
 28                                    else 1440 * (end_range[cv()] - start_range[cv()])
 29                                  end );

CO EM RANGE                                                 MINUTES
-- -- -------------------------------------------------- ----------
C2 E2 11/01/2012 06:30:00 am - 11/01/2012 07:00:00 am             3
C2 E2 11/01/2012 07:00:00 am - 11/01/2012 07:30:00 am            30
C2 E2 11/01/2012 07:30:00 am - 11/01/2012 08:00:00 am            30
C1 E1 11/01/2012 06:30:00 am - 11/01/2012 07:00:00 am             5
C1 E1 11/01/2012 07:00:00 am - 11/01/2012 07:30:00 am             1
C1 E1 11/02/2012 07:30:00 am - 11/02/2012 08:00:00 am            25
C1 E1 11/02/2012 08:00:00 am - 11/02/2012 08:30:00 am             1
C2 E1 11/01/2012 06:30:00 am - 11/01/2012 07:00:00 am             3
C2 E1 11/01/2012 07:00:00 am - 11/01/2012 07:30:00 am            30
C2 E1 11/01/2012 07:30:00 am - 11/01/2012 08:00:00 am            30
C2 E1 11/01/2012 08:00:00 am - 11/01/2012 08:30:00 am             1

11 rows selected.

所以我们分区:

partition by(id)

即通过唯一的参考。那么我们将用维度

生成行
dimension by(0 as f)

与部分规则相结合:

for f from 0 to blocks[0]-1  increment 1

因此生成了start_range列 start_range [对于f从0到块[0] -1增量1] = start_block [0] +(30 * cv(f)/ 1440),

start_block [0]位于第一个查询中,例如:

EM START_TIME             END_TIME               CO START_BLOCK                BLOCKS
-- ---------------------- ---------------------- -- ---------------------- ----------
E1 11/01/2012 06:55:00 am 11/01/2012 07:01:00 am C1 11/01/2012 06:30:00 am          2

因此对于此行,它的计算结果为

start_range[0 to 1] = 11/01/2012 06:30:00 am + (30minutes * the value of f)

start_range[0] = 11/01/2012 06:30:00 am + (30min*0) = 11/01/2012 06:30:00 am
start_range[1] = 11/01/2012 06:30:00 am + (30min*1) = 11/01/2012 07:00:00 am

其余的很直接:

end_range[any]  =  start_range[cv()] + (30/1440),

表示对于当前行的end-range,我们会start_range并添加30分钟。

range列是start_range和end_range的串联:

range [any] = to_char(start_range[cv()], 'dd/mm/yyyy hh:mi:ss am') || ' - ' || to_char(end_range[cv()], 'dd/mm/yyyy hh24:mi:ss am'),

最后为了计算该范围内的分钟数:

minutes [any]   = case
                    when start_time[0] between start_range[cv()] and end_range[cv()]
                    then 1440 *(end_range[cv()] - start_time[0])
                    when end_time[0] between start_range[cv()] and end_range[cv()]
                    then 1440 *(end_time[0] - start_range[cv()])
                    else 1440 * (end_range[cv()] - start_range[cv()])
                  end );
  1. 如果start_time位于范围内,则取范围的结尾 - 开始 时间
  2. 如果end_time位于范围内,请采用end_time - 范围的开始
  3. 否则为end_range - start_range。
  4. 1440只是得到答案的分钟。

    现在我们可以将所有内容分组:

    SQL> with foo as (select rownum id, emp, start_time, end_time, code,
      2                      trunc(start_time, 'mi')
      3                      - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440) start_block,
      4                      ceil(2*24*(end_time-(trunc(start_time, 'mi')
      5                      - (mod(to_char(trunc(start_time, 'mi'), 'mi'), 30) / 1440)))) blocks
      6                 from tab f)
      7  select thedate, code, range, sum(minutes) minutes
      8    from (select trunc(start_time) thedate, code, emp, range, minutes
      9            from foo
     10           model partition by(id)
     11                 dimension by(0 as f)
     12                 measures(code, emp, start_time, end_time, start_block, blocks,
     13                          sysdate as start_range,
     14                          sysdate as end_range,
     15                          cast(0 as number) minutes,
     16                          cast('' as varchar2(50)) range)
     17                 rules (start_range [for f from 0 to blocks[0]-1  increment 1]  = start_block[0] + (30*cv(f)/1440),
     18                        code[any]  =  code[0],
     19                        emp[any]  =  emp[0],
     20                        end_range[any]  =  start_range[cv()] + (30/1440),
     21                        start_time[any]  =  start_time[0],
     22                        end_time[any]  =  end_time[0],
     23                        range [any] = to_char(start_range[cv()], 'dd/mm/yyyy hh:mi:ss am') || ' - ' || to_char(end_range[cv()], 'dd/mm/yyyy hh24:mi:ss am'),
     24                        minutes [any]   = case
     25                                            when start_time[0] between start_range[cv()] and end_range[cv()]
     26                                            then 1440 *(end_range[cv()] - start_time[0])
     27                                            when end_time[0] between start_range[cv()] and end_range[cv()]
     28                                            then 1440 *(end_time[0] - start_range[cv()])
     29                                            else 1440 * (end_range[cv()] - start_range[cv()])
     30                                          end ))
     31   group by thedate, code, range
     32   order by thedate, code, range;
    
    THEDATE    CO RANGE                                                 MINUTES
    ---------- -- -------------------------------------------------- ----------
    11/01/2012 C1 11/01/2012 06:30:00 am - 11/01/2012 07:00:00 am             5
    11/01/2012 C1 11/01/2012 07:00:00 am - 11/01/2012 07:30:00 am             1
    11/01/2012 C2 11/01/2012 06:30:00 am - 11/01/2012 07:00:00 am             6
    11/01/2012 C2 11/01/2012 07:00:00 am - 11/01/2012 07:30:00 am            60
    11/01/2012 C2 11/01/2012 07:30:00 am - 11/01/2012 08:00:00 am            60
    11/01/2012 C2 11/01/2012 08:00:00 am - 11/01/2012 08:30:00 am             1
    11/02/2012 C1 11/02/2012 07:30:00 am - 11/02/2012 08:00:00 am            25
    11/02/2012 C1 11/02/2012 08:00:00 am - 11/02/2012 08:30:00 am             1
    

答案 1 :(得分:1)

我很确定这可以清理,并且更清晰,更有效,因为Oracle不是我的强项之一,但是它可以工作并且应该知道如何完成任务。

这里的关键是加入一个数字列表,将你的记录分成半小时。

SELECT  "Date",
        "Code",
        "RangeStart" + ((r - 1) / 48.0) AS "RangeStart",
        "RangeStart" + (r / 48.0) AS "RangeEnd",
        SUM(CASE WHEN r = 1 THEN "StartMinutes"
                WHEN "END_TIME" >= "RangeStart" + ((r - 1) / 48.0) AND "END_TIME" < "RangeStart" + (r / 48.0) THEN "EndMinutes"
                ELSE 30 
            END) AS "TotalMinutes"
FROM    (   SELECT  "Emp",
                    "Date",
                    "START_TIME",
                    "END_TIME",
                    "Code",
                    CASE WHEN EXTRACT(MINUTE from "START_TIME") > 30 THEN 60 ELSE 30 END - EXTRACT(MINUTE from "START_TIME") AS "StartMinutes",
                    EXTRACT(MINUTE from END_TIME) - CASE WHEN EXTRACT(MINUTE from "END_TIME") > 30 THEN 30 ELSE 0 END AS "EndMinutes",
                    "START_TIME" - (EXTRACT(MINUTE from "START_TIME") - CASE WHEN EXTRACT(MINUTE from "START_TIME") > 30 THEN 30 ELSE 0 END) / (60 * 24.0) AS "RangeStart"
            FROM    T
        ) T
        INNER JOIN
        (   SELECT  Rownum r
            FROM    dual
            CONNECT BY Rownum <= 100
        ) r
            ON "END_TIME" > ("RangeStart" + ((r - 1) / 48.0))
GROUP BY "Date", "Code", "RangeStart" + ((r - 1) / 48.0), "RangeStart" + (r / 48.0)
ORDER BY "Code", "Date", "RangeStart";

<强> EXAMPLE ON SQL FIDDLE

答案 2 :(得分:1)

这是另一种解决方案(它不是很优雅,并且使用硬编码的日期文字来获取存储桶的边界 - 应该用子查询替换它们来获取它们):

  with v_data as (
    select 1 pk, 'E1' emp, to_date('2012-11-01', 'YYYY-MM-DD') as date1, to_date('2012-11-01 06:55:00', 'YYYY-MM-DD hh24:mi:ss') as start_time, to_date('2012-11-01 07:01:00', 'YYYY-MM-DD hh24:mi:ss') as end_time, 'C1' as code, 6 as minutes from dual union all 
    select 2 pk, 'E1' emp, to_date('2012-11-01', 'YYYY-MM-DD') as date1, to_date('2012-11-01 06:57:00', 'YYYY-MM-DD hh24:mi:ss') as start_time, to_date('2012-11-01 08:01:00', 'YYYY-MM-DD hh24:mi:ss') as end_time, 'C2' as code, 64 as minutes from dual union all 
    select 3 pk, 'E2' emp, to_date('2012-11-01', 'YYYY-MM-DD') as date1, to_date('2012-11-01 06:57:00', 'YYYY-MM-DD hh24:mi:ss') as start_time, to_date('2012-11-01 08:00:00', 'YYYY-MM-DD hh24:mi:ss') as end_time, 'C2' as code, 63 as minutes from dual union all 
    select 4 pk, 'E1' emp, to_date('2012-11-02', 'YYYY-MM-DD') as date1, to_date('2012-11-02 07:35:00', 'YYYY-MM-DD hh24:mi:ss') as start_time, to_date('2012-11-02 08:01:00', 'YYYY-MM-DD hh24:mi:ss') as end_time, 'C1' as code, 26 as minutes from dual), 
v_buckets as (          
  select 
    to_date('2012-11-01', 'YYYY-MM-DD') + (rownum-1)/48 as bucket_start,
    to_date('2012-11-01', 'YYYY-MM-DD') + rownum/48 as bucket_end  
    from dual
  connect by rownum <96
)
select v3.date1, v3.bucket_start, v3.bucket_end, v3.code, sum(v3.time_spent_in_bucket) as minutes 
from (
select v2.*, (least(end_time, bucket_end) - greatest(start_time, bucket_start))*1440 as time_spent_in_bucket from 
(
select buck.*,
       v1.*
  from v_buckets buck
  join v_data v1 
    on (
       -- time slot completely contained in one bucket
        (v1.start_time >= buck.bucket_start and v1.start_time < buck.bucket_end and
        v1.end_time >= buck.bucket_start and v1.end_time < buck.bucket_end)
       -- time slot starts in bucket, expands to next bucket
        or (v1.start_time >= buck.bucket_start and v1.start_time < buck.bucket_end and
        v1.end_time >= buck.bucket_end)
       -- time slot started in previous bucket, ends in this bucket)
        or (v1.start_time < buck.bucket_start and v1.end_time > buck.bucket_start and
        v1.end_time <= buck.bucket_end)
       -- time slot began in previous bucket, expands to next bucket
       or (v1.start_time < buck.bucket_start and v1.end_time >= buck.bucket_end) 
        )
) v2
) v3 
where start_time is not null
group by date1, bucket_start, bucket_end, code
order by bucket_start, code

答案 3 :(得分:1)

这是我的尝试:

select trunc(trunc_start) as datetime, code, range , sum(duration) minutes
from (
select code, end_time, start_time, TRUNC_START , 
  to_char(trunc_start,'hh:mi:ss AM')||'-'||to_char(trunc_start+1/48,'hh:mi:ss AM') as range,
  case 
    when end_time-trunc_start between 0 and 1/48 then (end_time-trunc_start)*1440 
    when start_time-trunc_start between 0 and 1/48 then (trunc_start-start_time)*1440+30
    else 30 
  end as duration
from(
  select  s.*, n , 
  trunc(start_time) + trunc((start_time-trunc(start_time))*48)/48 + (n-1)/48 as trunc_start
  from s
  join (select level n from dual connect by level <=48) a
  on n-2 <= (end_time-start_time)*100
  )b
)
where trunc_start < end_time --eliminating fake intervals
group by code, trunc(trunc_start), range
order by 1, 3
;

抱歉where:)

SQLFIDDLE

答案 4 :(得分:-1)

以下是您的范围的一些一般示例:

SELECT job
  , sum(decode(greatest(sal,2999), least(sal,6000), 1, 0)) "Range 3000-6000"
  , sum(decode(greatest(sal,1000), least(sal,2999), 1, 0)) "Range 1000-3000"
  , sum(decode(greatest(sal,0),    least(sal,999), 1, 0))  "Range 0-1000"
  FROM scott.emp
GROUP BY  job
/