Saturday, February 25, 2012

Recording Gaps

hi,

I have a view that contains details of when people are allocated to certain task, relevant fields as follows:

RES_ID int(4) (The Id no for that member of staff)
PK TASK_ID int(4) (the code for the task)
PK START_DATE datetime(8) (The date and time that they started on the task
END_DATE datetime(8) (when the task finishes)

I have been asked to create a report that shows when people are NOT working on a particular date (so they can reassign people if a someone calls in sick). Sounds easy until you think about it.

e.g. say Brian works the following hours on 02.01.2007
3.00am - 4.00am
4.30am - 6.00am
7.00am - 11.00am

I need to show the following in report:
Brian free 12.00am - 2.59am
Brian free 4.01am - 4.29am
Brian free 6.01am - 6.59am
Brian free 11.01am - 11.59pm

(I need this info for each member of staff)

I cannot work out how to do this. I am relatively new to SQL server but have a good understanding of relational databases.

Any help would be much appreciated as I am totally stuck.

Thanks,
Chris

The best way to accomplish this type of goal is to have a 'Calendar' table. Then you simply JOIN against the Calendar table to interact with your data in a temporal fashion.

Read about using a Calendar table here:

Datetime -Calendar Table
http://www.aspfaq.com/show.asp?id=2519

|||

Try:

Code Snippet

createtable #t (

RES_ID int,

TASK_ID int,

[START_DATE] datetime,

END_DATE datetime

)

go

insertinto #t values(1, 1,'2007-06-18T03:00:00','2007-06-18T04:00:00')

insertinto #t values(1, 1,'2007-06-18T04:30:00','2007-06-18T06:00:00')

insertinto #t values(1, 1,'2007-06-18T07:00:00','2007-06-18T11:00:00')

insertinto #t values(2, 1,'2007-06-18T03:20:00','2007-06-18T04:30:00')

insertinto #t values(2, 1,'2007-06-18T04:31:00','2007-06-18T06:15:00')

insertinto #t values(2, 1,'2007-06-18T07:00:00','2007-06-18T12:00:00')

go

declare @.d datetime

set @.d ='20070618'

;with cte

as

(

select

a.RES_ID,

dateadd(minute, 1, a.END_DATE)as [START_DATE_1],

dateadd(minute,-1,

(

selectmin(b.[START_DATE])

from #t as b

where b.RES_ID = a.RES_ID and b.[START_DATE] > a.END_DATE

)

)as [END_DATE_1]

from

#t as a

where

a.[START_DATE] >= @.d and a.[END_DATE] <dateadd(day, 1, @.d)

andnotexists(

select*

from #t as b

where

b.RES_ID = a.RES_ID

and b.[START_DATE] >= @.d and b.[END_DATE] <dateadd(day, 1, @.d)

and b.[START_DATE] =dateadd(minute, 1, a.END_DATE)

)

and a.END_DATE <(

selectmax(c.END_DATE)

from #t as c

where

c.RES_ID = a.RES_ID

and c.[START_DATE] >= @.d and c.[END_DATE] < dateadd(day, 1, @.d)

)

)

select

*

from

cte

union

select

RES_ID,

@.d,

dateadd(minute, -1, min([START_DATE]))

from

#t

where

[START_DATE] >= @.d and [END_DATE] < dateadd(day, 1, @.d)

group by

RES_ID

having

dateadd(minute, -1, min([START_DATE])) > @.d

union

select

RES_ID,

dateadd(minute, 1, max([START_DATE])),

dateadd(minute, 1439, @.d)

from

#t

where

[START_DATE] >= @.d and [END_DATE] < dateadd(day, 1, @.d)

group by

RES_ID

having

dateadd(minute, 1, max([START_DATE])) < dateadd(minute, 1439, @.d)

order by

RES_ID,

[START_DATE_1]

go

drop table #t

go

The idea behind this possible solution, is to find the gaps for each RES_ID for a specific date.

AMB

|||

Hi,

thanks for the responses. Hunchback, how would I turn this into a stored procedure with the date as a parameter? I have tried but get lots of syntax errors. I'm sorry but I have only created basic SPs in the past.

Thanks,

Chris

|||

Code Snippet

create procedure dbo.usp_p1

@.d datetime

as

set nocount on

;with cte

as

(

select

a.RES_ID,

dateadd(minute, 1, a.END_DATE)as [START_DATE_1],

dateadd(minute,-1,

(

selectmin(b.[START_DATE])

from #t as b

where b.RES_ID = a.RES_ID and b.[START_DATE] > a.END_DATE

)

)as [END_DATE_1]

from

#t as a

where

a.[START_DATE] >= @.d and a.[END_DATE] <dateadd(day, 1, @.d)

andnotexists(

select*

from #t as b

where

b.RES_ID = a.RES_ID

and b.[START_DATE] >= @.d and b.[END_DATE] <dateadd(day, 1, @.d)

and b.[START_DATE] =dateadd(minute, 1, a.END_DATE)

)

and a.END_DATE <(

selectmax(c.END_DATE)

from #t as c

where

c.RES_ID = a.RES_ID

and c.[START_DATE] >= @.d and c.[END_DATE] < dateadd(day, 1, @.d)

)

)

select

*

from

cte

union

select

RES_ID,

@.d,

dateadd(minute, -1, min([START_DATE]))

from

#t

where

[START_DATE] >= @.d and [END_DATE] < dateadd(day, 1, @.d)

group by

RES_ID

having

dateadd(minute, -1, min([START_DATE])) > @.d

union

select

RES_ID,

dateadd(minute, 1, max([START_DATE])),

dateadd(minute, 1439, @.d)

from

#t

where

[START_DATE] >= @.d and [END_DATE] < dateadd(day, 1, @.d)

group by

RES_ID

having

dateadd(minute, 1, max([START_DATE])) < dateadd(minute, 1439, @.d)

order by

RES_ID,

[START_DATE_1]

go

exec dbo.usp_p1 '19960704'

go

AMB

|||

hi,

I keep getting the following message when I try to save the SP

ADO error:Incorrect syntax near the word 'with'

the server is SQL SERVER 2000 and I am trying to save the SP in an Access .adp file. Do I need a slightly different syntax?

Thanks,

Chris

|||

Commom table expressions were introduced with SS 2005, so we have to change the statement.

create procedure dbo.usp_p1

@.d datetime

as

set nocount on

select

a.RES_ID,

dateadd(minute, 1, a.END_DATE)as [START_DATE_1],

dateadd(minute,-1,

(

selectmin(b.[START_DATE])

from #t as b

where b.RES_ID = a.RES_ID and b.[START_DATE] > a.END_DATE

)

)as [END_DATE_1]

from

#t as a

where

a.[START_DATE] >= @.d and a.[END_DATE] <dateadd(day, 1, @.d)

andnotexists(

select*

from #t as b

where

b.RES_ID = a.RES_ID

and b.[START_DATE] >= @.d and b.[END_DATE] <dateadd(day, 1, @.d)

and b.[START_DATE] =dateadd(minute, 1, a.END_DATE)

)

and a.END_DATE <(

selectmax(c.END_DATE)

from #t as c

where

c.RES_ID = a.RES_ID

and c.[START_DATE] >= @.d and c.[END_DATE] < dateadd(day, 1, @.d)

)

union

select

RES_ID,

@.d,

dateadd(minute, -1, min([START_DATE]))

from

#t

where

[START_DATE] >= @.d and [END_DATE] < dateadd(day, 1, @.d)

group by

RES_ID

having

dateadd(minute, -1, min([START_DATE])) > @.d

union

select

RES_ID,

dateadd(minute, 1, max([START_DATE])),

dateadd(minute, 1439, @.d)

from

#t

where

[START_DATE] >= @.d and [END_DATE] < dateadd(day, 1, @.d)

group by

RES_ID

having

dateadd(minute, 1, max([START_DATE])) < dateadd(minute, 1439, @.d)

order by

RES_ID,

[START_DATE_1]

go

exec dbo.usp_p1 '19960704'

go

|||here's, another approach, this may or may not perform better than hunchbacks post, although they both have the same logic, btw, thanks hunchback for the test data that i piggy back with

[edited]
create table #t (
RES_ID int,
TASK_ID int,
[START_DATE] datetime,
END_DATE datetime
)

insert into #t values(1, 1, '2007-06-18T03:00:00', '2007-06-18T04:00:00')
insert into #t values(1, 1, '2007-06-18T04:30:00', '2007-06-18T06:00:00')
insert into #t values(1, 1, '2007-06-18T07:00:00', '2007-06-18T11:00:00')
insert into #t values(2, 1, '2007-06-18T03:20:00', '2007-06-18T04:30:00')
insert into #t values(2, 1, '2007-06-18T04:31:00', '2007-06-18T06:15:00')
insert into #t values(2, 1, '2007-06-18T07:00:00', '2007-06-18T12:00:00')

declare @.d datetime

set @.d = '20070618'

select *
from #t

select identity(int,1,1) as row
, *
, dateadd(mi,-1,start_date) as free_end
, dateadd(mi,1,end_date) as free_start
into #r
from #t
order by
res_id
, start_date

select res_id
, task_id
, min(dateadd(mi,-1,start_date)) as min_start
, max(dateadd(mi,1,end_date)) as max_end
into #minmax
from #t
group by
res_id
, task_id

select distinct
t.res_id
, t.task_id
, dateadd(mi,1,dateadd(mi,-1,@.d)) as free_start
, r.min_start as free_end
from #t t inner join
#minmax r on t.res_id = r.res_id
and t.task_id = r.task_id
union
select r1.res_id
, r1.task_id
, r1.free_start as free_start
, r2.free_end as free_end
from #r r1 inner join
#r r2 on r1.row = r2.row - 1
and r1.res_id = r2.res_id
and r1.task_id = r2.task_id
union
select distinct
t.res_id
, t.task_id
, r.max_end as free_start
, dateadd(mi,-1,dateadd(day,1,@.d)) as free_end
from #t t inner join
#minmax r on t.res_id = r.res_id
and t.task_id = r.task_id

drop table #t
drop table #r
drop table #minmax|||

hi Hunchback,

thanks very much this is great, but having had a close look at the data that the report has to deal with, it is not quite there - not your fault, mine for not being specific enough.

These are the extra conditions it has to deal with:

a task can start on the day before but end on the day of the report,e.g.

Chris task1 01/01/2007 23:50 - 02/01/2007 01.00

for the above, the report would have to say the freetime started at 01.01am.

Also, tasks can overlap ,e.g.

Chris task1 02/01/2007 06.50 - 02/01/2007 07.30

Chris task2 02/01/2007 07.15 - 02/01/2007 07.40

Chris task4 02/01/2007 07.20 - 02/01/2007 07.35

Chris task3 02/01/2007 07.50 - 02/01/2007 23.59

in the above the freetime is

00.00 - 06.49,

07.41 - 07.49

And finally,

if someone does not have any tasks that day then their freetime should show 00.00 - 23.59.

I realise that these extra conditions make it that much harder, but they were the reason that I originally thought I had no chance of working it out. It is my fault that I overlooked mentioning them.

Thanks,

Chris

|||You will probably find that cursors run much quicker here when you have a realistic amount of data.

How does this code grab you? I'd use a proper resources table in the first statement though. This should handle overlaps and people who are missing.

--First populate everyone
select
r.res_id, @.thedate as start_date, dateadd(day,1,dateadd(minute,-1,@.thedate)) as end_date
into #tmp
from (select distinct res_id from #t) r

--Now run through the time worked
declare csrCsr cursor for
select res_id, start_date, end_date
from #t
where start_date >= @.thedate
and start_date < dateadd(day,1,@.thedate)
order by res_id, start_date
for read only

declare @.res int, @.start datetime, @.end datetime

open csrCsr
fetch csrCsr into @.res, @.start, @.end
while (@.@.fetch_status = 0)
begin
-- For each overlap, put records in either side, and then get rid of the original
insert into #tmp
select @.res, start_date, dateadd(minute,-1,@.start)
from #tmp
where res_id = @.res
and start_date < @.end
and @.start < end_date
and start_date < @.start

insert into #tmp
select @.res, dateadd(minute,1,@.end), end_date
from #tmp
where res_id = @.res
and start_date < @.end
and @.start < end_date
and @.end < end_date

delete
from #tmp
where res_id = @.res
and start_date < @.end
and @.start < end_date

fetch csrCsr into @.res, @.start, @.end
end
close csrCsr
deallocate csrCsr

--Now query the table, and drop the temporary resource
select * from #tmp
drop table #tmp

--Rob|||Oh, throw that into a stored procedure, like this:

create procedure dbo.fetch_times (@.thedate datetime)
as
...

--and of course, use the proper table name instead of #t.|||

hi,

I'd like to thank everyone for their help. After a day or too of navel-gazing, I admitted to myself that my SQL is not that great and resorted to.......CURSORS!

Sorry, but once I did that, it was pretty easy (see below). It is all well and good saying everything should be done with set-base solutions, but I don't have the time or skill for the more extreme examples of these.

anyway, here's my solution:

ALTER PROCEDURE GAP_Report
(
@.d DATETIME
)

AS
SET NOCOUNT ON


DECLARE @.resname VARCHAR(50), @.startdate DATETIME, @.enddate DATETIME, @.rname VARCHAR(50), @.edate DATETIME


SET @.rname = ''

DECLARE @.FreeTime TABLE
(
Res_name VARCHAR(50),
free_start DATETIME,
free_end DATETIME
)

DECLARE cCursor CURSOR FAST_FORWARD
FOR

SELECT
RES_NAME,TASK_START_DATE, TASK_FINISH_DATE
FROM
view_TasksPerUser
WHERE
((DATEDIFF(d,TASK_START_DATE,@.d) = 0) OR (DATEDIFF(d,TASK_FINISH_DATE,@.d) = 0))
ORDER BY
RES_NAME,TASK_START_DATE

OPEN cCursor
FETCH cCursor INTO @.resname, @.startdate, @.enddate

WHILE @.@.Fetch_Status = 0
BEGIN
IF @.rname <> @.resname
BEGIN
IF DATEDIFF(d,@.d,@.startdate) = 0
BEGIN
INSERT INTO @.FreeTime VALUES (@.resname,@.d,DATEADD(s,-1,@.startdate))
END

IF @.rname <> ''
BEGIN
IF DATEDIFF(d,@.d,@.enddate) = 0
BEGIN
INSERT INTO @.FreeTime VALUES (@.rname,DATEADD(s,1,@.edate),DATEADD(s,-1,DATEADD(d,1,@.d)))
END
END
SET @.edate = @.enddate
END
ELSE
BEGIN
IF DATEDIFF(s,@.edate,@.Startdate) > 0
BEGIN

INSERT INTO @.FreeTime VALUES (@.resname,DATEADD(s,1,@.edate), DATEADD(s,-1,@.startdate))
END
END

SET @.rname = @.resname

IF @.enddate > @.edate

SET @.edate = @.enddate

FETCH cCursor INTO @.resname, @.startdate, @.enddate

END


CLOSE cCursor
DEALLOCATE cCURSOR

INSERT INTO
@.FreeTime
SELECT
RES_NAME as res_name,
@.d as Free_start,
DATEADD(s,-1,DATEADD(d,1,@.d)) as Free_end
FROM
(SELECT
DISTINCT RES_NAME
FROM
MSP_RESOURCES
WHERE
RES_NAME <> ''
UNION ALL
SELECT
DISTINCT res_name
FROM
@.Freetime) U
GROUP BY res_name
HAVING COUNT(*) = 1

SELECT
*
FROM
@.Freetime
ORDER BY res_name, free_start, free_end

RETURN 0

thanks again,

Chris

No comments:

Post a Comment