views:

222

answers:

6

I have

  TABLE EMPLOYEE - ID,DATE,IsPresent

I want to calculate longest streak for a employee presence.The Present bit will be false for days he didnt come..So I want to calculate the longest number of days he came to office for consecutive dates..I have the Date column field is unique...So I tried this way -

Select Id,Count(*) from Employee where IsPresent=1

But the above doesnt work...Can anyone guide me towards how I can calculate streak for this?....I am sure people have come across this...I tried searching online but...didnt understand it well...please help me out..

A: 

you probably have to write stored procedure/function for this

Guard
+1  A: 

Try this:

select 
    e.Id,
    e.date,
    (select 
       max(e1.date) 
     from 
       employee e1 
     where 
       e1.Id = e.Id and
       e1.date < e.date and 
       e1.IsPresent = 0) StreakStartDate,
    (select 
       min(e2.date) 
     from 
       employee e2 
     where 
       e2.Id = e.Id and
       e2.date > e.date and
       e2.IsPresent = 0) StreakEndDate           
from 
    employee e
where
    e.IsPresent = 1

Then finds out the longest streak for each employee:

select id, max(datediff(streakStartDate, streakEndDate))
from (<use subquery above>)
group by id

I'm not fully sure this query has correct syntax because I havn't database just now. Also notice streak start and streak end columns contains not the first and last day when employee was present, but nearest dates when he was absent. If dates in table have approximately equal distance, this does not means, otherwise query become little more complex, because we need to finds out nearest presence dates. Also this improvements allow to handle situation when the longest streak is first or last streak.

The main idea is for each date when employee was present find out streak start and streak end.

For each row in table when employee was present, streak start is maximum date that is less then date of current row when employee was absent.

STO
A: 

I did this once to determine consecutive days that a fire fighter had been on shift at least 15 minutes.

Your case is a bit more simple.

If you wanted to assume that no employee came more than 32 consecutive times, you could just use a Common Table Expression. But a better approach would be to use a temp table and a while loop.

You will need a column called StartingRowID. Keep joining from your temp table to the employeeWorkDay table for the next consecutive employee work day and insert them back into the temp table. When @@Row_Count = 0, you have captured the longest streak.

Now aggregate by StartingRowID to get the first day of the longest streak. I'm running short on time, or I would include some sample code.

sympatric greg
+1  A: 

EDIT Here's a SQL Server version of the query:

with LowerBound as (select second_day.EmployeeId
        , second_day."DATE" as LowerDate
        , row_number() over (partition by second_day.EmployeeId 
            order by second_day."DATE") as RN
    from T second_day
    left outer join T first_day
        on first_day.EmployeeId = second_day.EmployeeId
        and first_day."DATE" = dateadd(day, -1, second_day."DATE")
        and first_day.IsPresent = 1
    where first_day.EmployeeId is null
    and second_day.IsPresent = 1)
, UpperBound as (select first_day.EmployeeId
        , first_day."DATE" as UpperDate
        , row_number() over (partition by first_day.EmployeeId 
            order by first_day."DATE") as RN
    from T first_day
    left outer join T second_day
        on first_day.EmployeeId = second_day.EmployeeId
        and first_day."DATE" = dateadd(day, -1, second_day."DATE")
        and second_day.IsPresent = 1
    where second_day.EmployeeId is null
    and first_day.IsPresent = 1)
select LB.EmployeeID, max(datediff(day, LowerDate, UpperDate) + 1) as LongestStreak
from LowerBound LB
inner join UpperBound UB
    on LB.EmployeeId = UB.EmployeeId
    and LB.RN = UB.RN
group by LB.EmployeeId

SQL Server Version of the test data:

create table T (EmployeeId int
    , "DATE" date not null
    , IsPresent bit not null 
    , constraint T_PK primary key (EmployeeId, "DATE")
)


insert into T values (1, '2000-01-01', 1);
insert into T values (2, '2000-01-01', 0);
insert into T values (3, '2000-01-01', 0);
insert into T values (3, '2000-01-02', 1);
insert into T values (3, '2000-01-03', 1);
insert into T values (3, '2000-01-04', 0);
insert into T values (3, '2000-01-05', 1);
insert into T values (3, '2000-01-06', 1);
insert into T values (3, '2000-01-07', 0);
insert into T values (4, '2000-01-01', 0);
insert into T values (4, '2000-01-02', 1);
insert into T values (4, '2000-01-03', 1);
insert into T values (4, '2000-01-04', 1);
insert into T values (4, '2000-01-05', 1);
insert into T values (4, '2000-01-06', 1);
insert into T values (4, '2000-01-07', 0);
insert into T values (5, '2000-01-01', 0);
insert into T values (5, '2000-01-02', 1);
insert into T values (5, '2000-01-03', 0);
insert into T values (5, '2000-01-04', 1);
insert into T values (5, '2000-01-05', 1);
insert into T values (5, '2000-01-06', 1);
insert into T values (5, '2000-01-07', 0);

Sorry, this is written in Oracle, so substitute the appropriate SQL Server date arithmetic.

Assumptions:

  • Date is either a Date value or DateTime with time component of 00:00:00.
  • The primary key is (EmployeeId, Date)
  • All fields are not null
  • If a date is missing for the employee, they were not present. (Used to handle the beginning and ending of the data series, but also means that missing dates in the middle will break streaks. Could be a problem depending on requirements.

    with LowerBound as (select second_day.EmployeeId
            , second_day."DATE" as LowerDate
            , row_number() over (partition by second_day.EmployeeId 
                order by second_day."DATE") as RN
        from T second_day
        left outer join T first_day
            on first_day.EmployeeId = second_day.EmployeeId
            and first_day."DATE" = second_day."DATE" - 1
            and first_day.IsPresent = 1
        where first_day.EmployeeId is null
        and second_day.IsPresent = 1)
    , UpperBound as (select first_day.EmployeeId
            , first_day."DATE" as UpperDate
            , row_number() over (partition by first_day.EmployeeId 
                order by first_day."DATE") as RN
        from T first_day
        left outer join T second_day
            on first_day.EmployeeId = second_day.EmployeeId
            and first_day."DATE" = second_day."DATE" - 1
            and second_day.IsPresent = 1
        where second_day.EmployeeId is null
        and first_day.IsPresent = 1)
    select LB.EmployeeID, max(UpperDate - LowerDate + 1) as LongestStreak
    from LowerBound LB
    inner join UpperBound UB
        on LB.EmployeeId = UB.EmployeeId
        and LB.RN = UB.RN
    group by LB.EmployeeId
    

Test Data:

    create table T (EmployeeId number(38) 
        , "DATE" date not null check ("DATE" = trunc("DATE"))
        , IsPresent number not null check (IsPresent in (0, 1))
        , constraint T_PK primary key (EmployeeId, "DATE")
    )
    /

    insert into T values (1, to_date('2000-01-01', 'YYYY-MM-DD'), 1);
    insert into T values (2, to_date('2000-01-01', 'YYYY-MM-DD'), 0);
    insert into T values (3, to_date('2000-01-01', 'YYYY-MM-DD'), 0);
    insert into T values (3, to_date('2000-01-02', 'YYYY-MM-DD'), 1);
    insert into T values (3, to_date('2000-01-03', 'YYYY-MM-DD'), 1);
    insert into T values (3, to_date('2000-01-04', 'YYYY-MM-DD'), 0);
    insert into T values (3, to_date('2000-01-05', 'YYYY-MM-DD'), 1);
    insert into T values (3, to_date('2000-01-06', 'YYYY-MM-DD'), 1);
    insert into T values (3, to_date('2000-01-07', 'YYYY-MM-DD'), 0);
    insert into T values (4, to_date('2000-01-01', 'YYYY-MM-DD'), 0);
    insert into T values (4, to_date('2000-01-02', 'YYYY-MM-DD'), 1);
    insert into T values (4, to_date('2000-01-03', 'YYYY-MM-DD'), 1);
    insert into T values (4, to_date('2000-01-04', 'YYYY-MM-DD'), 1);
    insert into T values (4, to_date('2000-01-05', 'YYYY-MM-DD'), 1);
    insert into T values (4, to_date('2000-01-06', 'YYYY-MM-DD'), 1);
    insert into T values (4, to_date('2000-01-07', 'YYYY-MM-DD'), 0);
    insert into T values (5, to_date('2000-01-01', 'YYYY-MM-DD'), 0);
    insert into T values (5, to_date('2000-01-02', 'YYYY-MM-DD'), 1);
    insert into T values (5, to_date('2000-01-03', 'YYYY-MM-DD'), 0);
    insert into T values (5, to_date('2000-01-04', 'YYYY-MM-DD'), 1);
    insert into T values (5, to_date('2000-01-05', 'YYYY-MM-DD'), 1);
    insert into T values (5, to_date('2000-01-06', 'YYYY-MM-DD'), 1);
    insert into T values (5, to_date('2000-01-07', 'YYYY-MM-DD'), 0);
Shannon Severance
+3  A: 

groupby is missing.

To select total man-days (for everyone) attendance of the whole office.

Select Id,Count(*) from Employee where IsPresent=1

To select man-days attendance per employee.

Select Id,Count(*)
from Employee
where IsPresent=1
group by id;

But that is still not good because it counts the total days of attendance and NOT the length of continuous attendance.

What you need to do is construct a temp table with another date column date2. date2 is set to today. The table is the list of all days an employee is absent.

create tmpdb.absentdates as
Select id, date, today as date2
from EMPLOYEE
where IsPresent=0
order by id, date;

So the trick is to calculate the date difference between two absent days to find the length of continuously present days. Now, fill in date2 with the next absent date per employee. The most recent record per employee will not be updated but left with value of today because there is no record with greater date than today in the database.

update tmpdb.absentdates
set date2 = 
  select min(a2.date)
  from
   tmpdb.absentdates a1,
   tmpdb.absentdates a2
  where a1.id = a2.id
    and a1.date < a2.date

The above query updates itself by performing a join on itself and may cause deadlock query so it is better to create two copies of the temp table.

create tmpdb.absentdatesX as
Select id, date
from EMPLOYEE
where IsPresent=0
order by id, date;

create tmpdb.absentdates as
select *, today as date2
from tmpdb.absentdatesX;

You need to insert the hiring date, presuming the earliest date per employee in the database is the hiring date.

insert into tmpdb.absentdates a
select a.id, min(e.date), today
from EMPLOYEE e
where a.id = e.id

Now update date2 with the next later absent date to be able to perform date2 - date.

update tmpdb.absentdates
set date2 = 
  select min(x.date)
  from
   tmpdb.absentdates a,
   tmpdb.absentdatesX x
  where a.id = x.id
    and a.date < x.date

This will list the length of days an emp is continuously present:

select id, datediff(date2, date) as continuousPresence
from tmpdb.absentdates
group by id, continuousPresence
order by id, continuousPresence

But you only want to longest streak:

select id, max(datediff(date2, date) as continuousPresence)
from tmpdb.absentdates
group by id
order by id

However, the above is still problematic because datediff does not take into account holidays and weekends.

So we depend on the count of records as the legitimate working days.

create tmpdb.absentCount as
Select a.id, a.date, a.date2, count(*) as continuousPresence
from EMPLOYEE e, tmpdb.absentdates a
where e.id = a.id
  and e.date >= a.date
  and e.date < a.date2
group by a.id, a.date
order by a.id, a.date;

Remember, every time you use an aggregator like count, ave yo need to groupby the selected item list because it is common sense that you have to aggregate by them.

Now select the max streak

select id, max(continuousPresence)
from tmpdb.absentCount
group by id

To list the dates of streak:

select id, date, date2, continuousPresence
from tmpdb.absentCount
group by id
having continuousPresence = max(continuousPresence);

There may be some mistakes (sql server tsql) above but this is the general idea.

Blessed Geek
For Some reason i could not create temp table...shows me error for unknown object...so i used a select into statement..
Misnomer
+1  A: 

Here is an alternate version, to handle missing days differently. Say that you only record a record for work days, and being at work Monday-Friday one week and Monday-Friday of the next week counts as ten consecutive days. This query assumes that missing dates in the middle of a series of rows are non-work days.

with LowerBound as (select second_day.EmployeeId
        , second_day."DATE" as LowerDate
        , row_number() over (partition by second_day.EmployeeId 
            order by second_day."DATE") as RN
    from T second_day
    left outer join T first_day
        on first_day.EmployeeId = second_day.EmployeeId
        and first_day."DATE" = dateadd(day, -1, second_day."DATE")
        and first_day.IsPresent = 1
    where first_day.EmployeeId is null
    and second_day.IsPresent = 1)
, UpperBound as (select first_day.EmployeeId
        , first_day."DATE" as UpperDate
        , row_number() over (partition by first_day.EmployeeId 
            order by first_day."DATE") as RN
    from T first_day
    left outer join T second_day
        on first_day.EmployeeId = second_day.EmployeeId
        and first_day."DATE" = dateadd(day, -1, second_day."DATE")
        and second_day.IsPresent = 1
    where second_day.EmployeeId is null
    and first_day.IsPresent = 1)
select LB.EmployeeID, max(datediff(day, LowerDate, UpperDate) + 1) as LongestStreak
from LowerBound LB
inner join UpperBound UB
    on LB.EmployeeId = UB.EmployeeId
    and LB.RN = UB.RN
group by LB.EmployeeId

go

with NumberedRows as (select EmployeeId
        , "DATE"
        , IsPresent
        , row_number() over (partition by EmployeeId
            order by "DATE") as RN
--        , min("DATE") over (partition by EmployeeId, IsPresent) as MinDate
--        , max("DATE") over (partition by EmployeeId, IsPresent) as MaxDate
    from T)
, LowerBound as (select SecondRow.EmployeeId
        , SecondRow.RN
        , row_number() over (partition by SecondRow.EmployeeId 
            order by SecondRow.RN) as LowerBoundRN
    from NumberedRows SecondRow
    left outer join NumberedRows FirstRow
        on FirstRow.IsPresent = 1
        and FirstRow.EmployeeId = SecondRow.EmployeeId
        and FirstRow.RN + 1 = SecondRow.RN
    where FirstRow.EmployeeId is null
    and SecondRow.IsPresent = 1)
, UpperBound as (select FirstRow.EmployeeId
       , FirstRow.RN
       , row_number() over (partition by FirstRow.EmployeeId
            order by FirstRow.RN) as UpperBoundRN
    from NumberedRows FirstRow
    left outer join NumberedRows SecondRow
        on SecondRow.IsPresent = 1
        and FirstRow.EmployeeId = SecondRow.EmployeeId
        and FirstRow.RN + 1 = SecondRow.RN
    where SecondRow.EmployeeId is null
    and FirstRow.IsPresent = 1)
select LB.EmployeeId, max(UB.RN - LB.RN + 1)
from LowerBound LB 
inner join UpperBound UB
    on LB.EmployeeId = UB.EmployeeId
    and LB.LowerBoundRN = UB.UpperBoundRN
group by LB.EmployeeId
Shannon Severance