SQLite query to find datetime difference between multiple rows - sql

Here are my two tables' structures in SQLite
CREATE TABLE user
(
id integer PRIMARY KEY,
name TEXT
);
CREATE TABLE attendanceTable
(
id Integer,
mydate datetime,
startJob boolean
);
if startJob is 1 it implies that the employee is starting the job and if startJob is 0 it means employee is stopping the job.
attendanceTable is sorted by mydate column
I want output as worked hour by individual employees.
Input of query can be two different dates e.g. 2021-08-20 and 2021-08-22
From which I want to know "How much each person has worked?"
Output should be:
[id, name, userWorkedTime]
[1, Alice, 09:00]
[2, Bob, 07:00]
12:00 to 16:00 + 22:00 to 03:00 = 9 hours
13:00 to 17:00 + 12:00 to 15:00 = 7 hours
Input of query 2021-08-20 and 2021-08-21 - output should be:
[id, name, userWorkedTime]
[1, Alice, 09:00]
[2, Bob, 04:00]
12:00 to 16:00 + 22:00 to 03:00 = 9 hours
13:00 to 17:00 = 4 hours
It may possible that Alice starts her job at 11 PM and end her job at 3 AM on next day[So working hour would be 4 hours]

I believe that the following will accomplish the results you desire:-
WITH
/* The date selection parameters - change as necessary */
cte_selection(selection_start,selection_end) AS (SELECT '2020-08-20','2020-08-22'),
/* Extract data per shift - aka combine start and end
note that extract is 1 day befor and 1 day after actual selection criteria
as previous/subsequent days may be relevant
*/
cte_part1(userid,name,periodstart,periodend,duration) AS
(
SELECT
user.id,
name,
strftime('%s',mydate),
strftime('%s',
(
SELECT mydate
FROM attendancetable
WHERE id = at.id
AND NOT startjob
AND mydate > at.mydate
ORDER BY mydate ASC
LIMIT 1
)
) AS endjob,
(strftime('%s',
(
SELECT mydate
FROM attendancetable
WHERE id = at.id
AND NOT startjob
AND mydate > at.mydate
ORDER BY mydate ASC
LIMIT 1
)
) - strftime('%s',at.mydate)) AS duration
FROM attendancetable AS at
JOIN user ON at.id = user.id
WHERE startjob
AND mydate
BETWEEN date
(
(SELECT selection_start FROM cte_selection)
,'-1 day'
)
AND date
(
(SELECT selection_end FROM cte_selection)
,'+1 day'
)
),
/* split times if period crosses a day*/
cte_part2(userid,name,periodstart,startdate,periodend,enddate,duration,startday_duration,nextday_duration) AS
(
SELECT
userid,
name,
periodstart,
date(periodstart,'unixepoch') AS startdate,
periodend,
date(periodend,'unixepoch') AS enddate,
duration,
CASE
WHEN date(periodstart,'unixepoch') = date(periodend,'unixepoch') THEN duration
ELSE strftime('%s',date(periodstart,'unixepoch')||'24:00:00') - periodstart
END AS startday_duration,
CASE
WHEN date(periodstart,'unixepoch') = date(periodend,'unixepoch') THEN 0
ELSE periodend - strftime('%s',date(periodend,'unixepoch')||'00:00:00')
END AS nextday_duration
FROM cte_part1
),
/* generate new rows for following days */
cte_part3(userid,name,periodstart,startdate,periodend,enddate,duration,startday_duration,nextday_duration) AS
(
SELECT
userid,
name,
strftime('%s',date(periodend,'unixepoch')||'00:00:00'),
date(periodend,'unixepoch'),
periodend,
enddate,
nextday_duration,
nextday_duration,
0
FROM cte_part2
WHERE nextday_duration
),
/* combine both sets */
cte_part4 AS (SELECT * FROM cte_part2 UNION ALL SELECT * FROM cte_part3)
/* Group the final data */
SELECT *,time(sum(startday_duration),'unixepoch') AS time_worked
FROM cte_part4
WHERE startdate BETWEEN (SELECT selection_start FROM cte_selection) AND (SELECT selection_end FROM cte_selection) GROUP BY userid
;
e.g. :-
and :-
Note All results with the exception of the time_worked are arbitrary values from the underlying data. However, userid and name will be correct as they would be the same for each group. The other values will be a value from the group.
you can easily apply changes to the final query to include or exclude columns.
The full testing SQL being :-
DROP TABLE IF EXISTS user;
CREATE TABLE IF NOT EXISTS user (id integer PRIMARY KEY,name TEXT);
DROP TABLE IF EXISTS attendanceTable ;
CREATE TABLE attendanceTable(id Integer,mydate datetime,startJob boolean);
INSERT INTO user VALUES (1,'Alice'),(2,'Bob');
INSERT INTO attendanceTable VALUES
(1,'2020-08-20 12:00:00',1),
(2,'2020-08-20 13:00:00',1),
(1,'2020-08-20 16:00:00',0),
(2,'2020-08-20 17:00:00',0),
(1,'2020-08-20 22:00:00',1),
(1,'2020-08-21 03:00:00',0),
(2,'2020-08-22 12:00:00',1),
(2,'2020-08-22 15:00:00',0)
;
WITH
/* The date selection parameters - change as necessary */
cte_selection(selection_start,selection_end) AS (SELECT '2020-08-20','2020-08-22'),
/* Extract data per shift - aka combine start and end
note that extract is 1 day befor and 1 day after actual selection criteria
as previous/subsequent days may be relevant
*/
cte_part1(userid,name,periodstart,periodend,duration) AS
(
SELECT
user.id,
name,
strftime('%s',mydate),
strftime('%s',
(
SELECT mydate
FROM attendancetable
WHERE id = at.id
AND NOT startjob
AND mydate > at.mydate
ORDER BY mydate ASC
LIMIT 1
)
) AS endjob,
(strftime('%s',
(
SELECT mydate
FROM attendancetable
WHERE id = at.id
AND NOT startjob
AND mydate > at.mydate
ORDER BY mydate ASC
LIMIT 1
)
) - strftime('%s',at.mydate)) AS duration
FROM attendancetable AS at
JOIN user ON at.id = user.id
WHERE startjob
AND mydate
BETWEEN date
(
(SELECT selection_start FROM cte_selection)
,'-1 day'
)
AND date
(
(SELECT selection_end FROM cte_selection)
,'+1 day'
)
),
/* split times if period crosses a day*/
cte_part2(userid,name,periodstart,startdate,periodend,enddate,duration,startday_duration,nextday_duration) AS
(
SELECT
userid,
name,
periodstart,
date(periodstart,'unixepoch') AS startdate,
periodend,
date(periodend,'unixepoch') AS enddate,
duration,
CASE
WHEN date(periodstart,'unixepoch') = date(periodend,'unixepoch') THEN duration
ELSE strftime('%s',date(periodstart,'unixepoch')||'24:00:00') - periodstart
END AS startday_duration,
CASE
WHEN date(periodstart,'unixepoch') = date(periodend,'unixepoch') THEN 0
ELSE periodend - strftime('%s',date(periodend,'unixepoch')||'00:00:00')
END AS nextday_duration
FROM cte_part1
),
/* generate new rows for following days */
cte_part3(userid,name,periodstart,startdate,periodend,enddate,duration,startday_duration,nextday_duration) AS
(
SELECT
userid,
name,
strftime('%s',date(periodend,'unixepoch')||'00:00:00'),
date(periodend,'unixepoch'),
periodend,
enddate,
nextday_duration,
nextday_duration,
0
FROM cte_part2
WHERE nextday_duration
),
/* combine both sets */
cte_part4 AS (SELECT * FROM cte_part2 UNION ALL SELECT * FROM cte_part3)
/* Group the final data */
SELECT *,time(sum(startday_duration),'unixepoch') AS time_worked
FROM cte_part4
WHERE startdate BETWEEN (SELECT selection_start FROM cte_selection) AND (SELECT selection_end FROM cte_selection) GROUP BY userid
;
DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS attendanceTable ;

Related

Querying the same column for 3 different values

I'm trying hard to extract the data in the format I need, but unsuccessful til now.
I have the following table
id_ticket, date_ticket, office_ticket, status_ticket
I need the query to return me, for EVERY MONTH, and always for the same OFFICE:
the number of tickets (COUNT) with any status
the number of tickets (COUNT) with status = 5
the number of tickets (COUNT) with status = 6
Month
Year
The query I made to return ONLY the total amount of tickets with any status was this. It worked!
SELECT
COUNT (id_ticket) as TotalTicketsPerMonth,
'sYear' = YEAR (date_ticket),
'sMonth' = MONTH (date_ticket)
FROM crm_vw_Tickets
WHERE office_ticket = 1
GROUP BY
YEAR (date_ticket), MONTH (date_ticket)
ORDER BY sYear ASC, sMonth ASC
Returning the total amount of ticket with status=5
SELECT
COUNT (id_ticket) as TotalTicketsPerMonth,
'sYear' = YEAR (date_ticket),
'sMonth' = MONTH (date_ticket)
FROM crm_vw_Tickets
WHERE office_ticket = 1 AND status_ticket = 5
GROUP BY
YEAR (date_ticket), MONTH (date_ticket)
ORDER BY sYear ASC, sMonth ASC
But I need the return to be something like:
Year Month Total Status5 Status6
2018 1 15 5 3
2018 2 14 4 5
2018 3 19 2 8
Thank you for your help.
You are close. You can use a CASE Expression to get what you need:
SELECT
COUNT (id_ticket) as TotalTicketsPerMonth,
SUM(CASE WHEN status_ticket = 5 THEN 1 END) as Status5,
SUM(CASE WHEN status_ticket = 6 THEN 1 END) as Status6,
'sYear' = YEAR (date_ticket),
'sMonth' = MONTH (date_ticket)
FROM crm_vw_Tickets
WHERE office_ticket = 1
GROUP BY YEAR (date_ticket), MONTH (date_ticket)
ORDER BY sYear ASC, sMonth ASC
The following code builds off JNevill's answer to include summary rows for "missing" months, i.e. those with no tickets, as well as months with tickets. The basic idea is to create a table of all of the months from the first to the last ticket, outer join the ticket data with the months and then summarize the data. (Tally table, numbers table and calendar table are more or less applicable terms.)
It is a Common Table Expression (CTE) that contains several queries that work step-by-step toward the result. You can see the results of the intermediate steps by replacing the final select statement with one of the ones commented out above it.
-- Sample data.
declare #crm_vw_Tickets as Table ( id_ticket Int Identity, date_ticket Date, office_ticket Int, status_ticket Int );
insert into #crm_vw_Tickets ( date_ticket, office_ticket, status_ticket ) values
( '20190305', 1, 6 ), -- Shrove Tuesday.
( '20190501', 1, 5 ), -- May Day.
( '20190525', 1, 5 ); -- Towel Day.
select * from #crm_vw_Tickets;
-- Summarize the data.
with
-- Get the minimum and maximum ticket dates for office_ticket 1.
Limits as (
select Min( date_ticket ) as MinDateTicket, Max( date_ticket ) as MaxDateTicket
from #crm_vw_Tickets
where office_ticket = 1 ),
-- 0 to 9.
Ten ( Number ) as ( select * from ( values (0), (1), (2), (3), (4), (5), (6), (7), (8), (9) ) as Digits( Number ) ),
-- 100 rows.
TenUp2 ( Number ) as ( select 42 from Ten as L cross join Ten as R ),
-- 10000 rows. We'll assume that 10,000 months should cover the reporting range.
TenUp4 ( Number ) as ( select 42 from TenUp2 as L cross join TenUp2 as R ),
-- 1 to the number of months to summarize.
Numbers ( Number ) as ( select top ( select DateDiff( month, MinDateTicket, MaxDateTicket ) + 1 from Limits ) Row_Number() over ( order by ( select NULL ) ) from TenUp4 ),
-- Starting date of each month to summarize.
Months as (
select DateAdd( month, N.Number - 1, DateAdd( day, 1 - Day( L.MinDateTicket ), L.MinDateTicket ) ) as StartOfMonth
from Limits as L cross join
Numbers as N ),
-- All tickets assigned to the appropriate month and a row with NULL ticket data
-- for each month without tickets.
MonthsAndTickets as (
select M.StartOfMonth, T.*
from Months as M left outer join
#crm_vw_Tickets as T on M.StartOfMonth <= T.date_ticket and T.date_ticket < DateAdd( month, 1, M.StartOfMonth ) )
-- Use one of the following select statements to see the intermediate or final results:
--select * from Limits;
--select * from Ten;
--select * from TenUp2;
--select * from TenUp4;
--select * from Numbers;
--select * from Months;
--select * from MonthsAndTickets;
select Year( StartOfMonth ) as SummaryYear, Month( StartOfMonth ) as SummaryMonth,
Count( id_ticket ) as TotalTickets,
Coalesce( Sum( case when status_ticket = 5 then 1 end ), 0 ) as Status5Tickets,
Coalesce( Sum( case when status_ticket = 6 then 1 end ), 0 ) as Status6Tickets
from MonthsAndTickets
where office_ticket = 1 or office_ticket is NULL -- Handle months with no tickets.
group by StartOfMonth
order by StartOfMonth;
Note that the final select uses Count( id_ticket ), Coalesce and an explicit check for NULL to produce appropriate output values (0) for months with no tickets.

Need to calc start and end date from single effective date

I am trying to write SQL to calculate the start and end date from a single date called effective date for each item. Below is a idea of how my data looks. There are times when the last effective date for an item will be in the past so I want the end date for that to be a year from today. The other two items in the table example have effective dates in the future so no need to create and end date of a year from today.
I have tried a few ways but always run into bad data. Below is an example of my query and the bad results
select distinct tb1.itemid,tb1.EffectiveDate as startdate
, case
when dateadd(d,-1,tb2.EffectiveDate) < getdate()
or tb2.EffectiveDate is null
then getdate() +365
else dateadd(d,-1,tb2.EffectiveDate)
end as enddate
from #test tb1
left join #test as tb2 on (tb2.EffectiveDate > tb1.EffectiveDate
or tb2.effectivedate is null) and tb2.itemid = tb1.itemid
left join #test tb3 on (tb1.EffectiveDate < tb3.EffectiveDate
andtb3.EffectiveDate <tb2.EffectiveDate or tb2.effectivedate is null)
and tb1.itemid = tb3.itemid
left join #test tb4 on tb1.effectivedate = tb4.effectivedate \
and tb1.itemid = tb4.itemid
where tb1.itemID in (62741,62740, 65350)
Results - there is an extra line for 62740
Bad Results
I expect to see below since the first two items have a future end date no need to create an end date of today + 365 but the last one only has one effective date so we have to calculate the end date.
I think I've read your question correctly. If you could provide your expected output it would help a lot.
Test Data
CREATE TABLE #TestData (itemID int, EffectiveDate date)
INSERT INTO #TestData (itemID, EffectiveDate)
VALUES
(62741,'2016-06-25')
,(62741,'2016-06-04')
,(62740,'2016-07-09')
,(62740,'2016-06-25')
,(62740,'2016-06-04')
,(65350,'2016-05-28')
Query
SELECT
a.itemID
,MIN(a.EffectiveDate) StartDate
,MAX(CASE WHEN b.MaxDate > GETDATE() THEN b.MaxDate ELSE CONVERT(date,DATEADD(yy,1,GETDATE())) END) EndDate
FROM #TestData a
JOIN (SELECT itemID, MAX(EffectiveDate) MaxDate FROM #TestData GROUP BY itemID) b
ON a.itemID = b.itemID
GROUP BY a.itemID
Result
itemID StartDate EndDate
62740 2016-06-04 2016-07-09
62741 2016-06-04 2016-06-25
65350 2016-05-28 2017-06-24
This should do it:
SELECT itemid
,effective_date AS "Start"
,(SELECT MIN(effective_date)
FROM effective_date_tbl
WHERE effective_date > edt.effective_date
AND itemid = edt.itemid) AS "End"
FROM effective_date_tbl edt
WHERE effective_date <
(SELECT MAX(effective_date) FROM effective_date_tbl WHERE itemid = edt.itemid)
UNION ALL
SELECT itemid
,effective_date AS "Start"
,(SYSDATE + 365) AS "End"
FROM effective_date_tbl edt
WHERE 1 = ( SELECT COUNT(*) FROM effective_date_table WHERE itemid = edt.itemid )
ORDER BY 1, 2, 3;
I did this exercise for Items that have multiple EffectiveDate in the table
you can create this view
CREATE view [VW_TESTDATA]
AS ( SELECT * FROM
(SELECT ROW_NUMBER() OVER (ORDER BY Item,CONVERT(datetime,EffectiveDate,110)) AS ID, Item, DATA
FROM MyTable ) AS Q
)
so use a select to compare the same Item
select * from [VW_TESTDATA] as A inner join [VW_TESTDATA] as B on A.Item = B.Item and A.id = B.id-1
in this way you always minor and major Date
I did not understand how to handle dates with only one Item , but it seems the simplest thing and can be added to this query with a UNION ALL, because the view not cover individual Item
You also need to figure out how to deal with Item with two equal EffectiveDate
you should use the case when statement..
[wrong query because a misunderstand of the requirements]
SELECT
ItemID AS Item,
StartDate,
CASE WHEN EndDate < Sysdate THEN Sysdate + 365 ELSE EndDate END AS EndDate
FROM
(
SELECT tabStartDate.ItemID, tabStartDate.EffectiveDate AS StartDate, tabEndDate.EffectiveDate AS EndDate
FROM TableItems tabStartDate
JOIN TableItems tabEndDate on tabStartDate.ItemID = tabEndDate.ItemID
) TableDatesPerItem
WHERE StartDate < EndDate
update after clarifications in the OP and some comments
I found a solution quite portable, because it doesn't make use of partioning but endorses on a sort of indexing rule that make to correspond the dates of each item with others with the same id, in order of time's succession.
The portability is obviously related to the "difficult" part of query, while row numbering mechanism and conversion go adapted, but I think that it isn't a problem.
I sended a version for MySql that it can try on SQL Fiddle..
Table
CREATE TABLE ITEMS
(`ItemID` int, `EffectiveDate` Date);
INSERT INTO ITEMS
(`ItemID`, `EffectiveDate`)
VALUES
(62741, DATE(20160625)),
(62741, DATE(20160604)),
(62740, DATE(20160709)),
(62740, DATE(20160625)),
(62740, DATE(20160604)),
(62750, DATE(20160528))
;
Query
SELECT
RESULT.ItemID AS ItemID,
DATE_FORMAT(RESULT.StartDate,'%m/%d/%Y') AS StartDate,
CASE WHEN RESULT.EndDate < CURRENT_DATE
THEN DATE_FORMAT((CURRENT_DATE + INTERVAL 365 DAY),'%m/%d/%Y')
ELSE DATE_FORMAT(RESULT.EndDate,'%m/%d/%Y')
END AS EndDate
FROM
(
SELECT
tabStartDate.ItemID AS ItemID,
tabStartDate.StartDate AS StartDate,
tabEndDate.EndDate
,tabStartDate.IDX,
tabEndDate.IDX AS IDX2
FROM
(
SELECT
tabStartDateIDX.ItemID AS ItemID,
tabStartDateIDX.EffectiveDate AS StartDate,
#rownum:=#rownum+1 AS IDX
FROM ITEMS AS tabStartDateIDX
ORDER BY tabStartDateIDX.ItemID, tabStartDateIDX.EffectiveDate
)AS tabStartDate
JOIN
(
SELECT
tabEndDateIDX.ItemID AS ItemID,
tabEndDateIDX.EffectiveDate AS EndDate,
#rownum:=#rownum+1 AS IDX
FROM ITEMS AS tabEndDateIDX
ORDER BY tabEndDateIDX.ItemID, tabEndDateIDX.EffectiveDate
)AS tabEndDate
ON tabStartDate.ItemID = tabEndDate.ItemID AND (tabEndDate.IDX - tabStartDate.IDX = ((select count(*) from ITEMS)+1) )
,(SELECT #rownum:=0) r
UNION
(
SELECT
tabStartDateSingleItem.ItemID AS ItemID,
tabStartDateSingleItem.EffectiveDate AS StartDate,
tabStartDateSingleItem.EffectiveDate AS EndDate
,0 AS IDX,0 AS IDX2
FROM ITEMS AS tabStartDateSingleItem
Group By tabStartDateSingleItem.ItemID
HAVING Count(tabStartDateSingleItem.ItemID) = 1
)
) AS RESULT
;

Combining records with overlapping date ranges in SQL

**EDIT: Our current server is SQL 2008 R2 so LAG/LEAD functions will not work.
I'm attempting to take multiple streams of data within a table and combine them into 1 stream of data. Given the 3 streams of data below I want the end result to be 1 stream that gives preference to the status 'on'. Recursion seems to be the best option but I've had no luck so far putting together a query that does what i want.
CREATE TABLE #Dates(
id INT IDENTITY,
status VARCHAR(4),
StartDate Datetime,
EndDate Datetime,
booth int)
INSERT #Dates
VALUES
( 'off','2015-01-01 08:00','2015-01-01 08:15',1),
( 'on','2015-01-01 08:15','2015-01-01 09:15',1),
( 'off','2015-01-01 08:50','2015-01-01 09:00',2),
( 'on','2015-01-01 09:00','2015-01-01 09:30',2),
( 'off','2015-01-01 09:30','2015-01-01 09:35',2),
( 'on','2015-01-01 09:35','2015-01-01 10:15',2),
( 'off','2015-01-01 09:30','2015-01-01 10:30',3),
( 'on','2015-01-01 10:30','2015-01-01 11:00',3)
status StartDate EndDate
---------------------------
off 08:00 08:15
on 08:15 09:15
off 08:50 09:00
on 09:00 09:30
off 09:30 09:35
on 09:35 10:15
off 09:30 10:30
on 10:30 11:00
End Result:
status StartDate EndDate
---------------------------
off 8:00 8:15
on 8:15 9:15
on 9:15 9:30
off 9:30 9:35
on 9:35 10:15
off 10:15 10:30
on 10:30 11:00
Essentially, anytime there is a status of 'on' it should override any concurrent 'off' status.
Source:
|----off----||---------on---------|
|---off--||------on----||---off---||--------on------|
|--------------off------------------||------on------|
Result (Either result would work):
|----off----||----------on--------||---on---||---off---||--------on------||-off--||------on------|
|----off----||----------on------------------||---off---||--------on------||-off--||------on------|
Here's the simplest version for 2008 that I was able to figure out:
; with Data (Date) as (
select StartDate from Dates
union
select EndDate from Dates),
Ranges (StartDate, Status) as (
select D.Date, D2.Status
from Data D
outer apply (
select top 1 D2.Status
from Dates D2
where D2.StartDate <= D.Date and D2.EndDate > D.Date
order by case when Status = 'on' then 1 else 2 end
) D2)
select R.StartDate,
(select min(D.Date) from Data D where D.Date > R.StartDate) as EndDate,
Status
from Ranges R
order by R.StartDate
It will return new row starting from each start / end point even if the status is the same as previous. Didn't find any simple way to combine them.
Edit: Changing the first CTE to this will combine the rows:
; with Data (Date) as (
select distinct StartDate from Dates D1
where not exists (Select 1 from Dates D2
where D2.StartDate < D1.StartDate and D2.EndDate > D1.StartDate and
Status = 'on')
union
select distinct EndDate from Dates D1
where not exists (Select 1 from Dates D2
where D2.StartDate < D1.EndDate and D2.EndDate > D1.EndDate and
Status = 'on')
),
So basically every time there's even one "on" record, it is on, otherwise off?
Here's a little different kind of approach to the issue, adding +1 every time an "on" cycle starts, and adding -1 when it ends. Then we can use a running total for the status, and when the status is 0, then it's off, and otherwise it is on:
select Date,
sum(oncounter) over (order by Date) as onstat,
sum(offcounter) over (order by Date) as offstat
from (
select StartDate as Date,
case when status = 'on' then 1 else 0 end oncounter,
case when status = 'off' then 1 else 0 end offcounter
from Dates
union all
select EndDate as Date,
case when status = 'on' then -1 else 0 end oncounter,
case when status = 'off' then -1 else 0 end offcounter
from Dates
) TMP
Edit: Added also counter for off -states. It works the same way as "on" counter and when both are 0, then status is neither on or off.
Final result, it seems it can be done, although it's not looking that nice anymore, but at least it's not recursive :)
select
Date as StartDate,
lead(Date, 1, '21000101') over (order by Date) as EndDate,
case onstat
when 0 then
case when offstat > 0 then 'Off' else 'N/A' end
else 'On' end as State
from (
select
Date,
onstat, prevon,
offstat, prevoff
from (
Select
Date,
onstat,
lag(onstat, 1, 0) over (order by Date) as prevon,
offstat,
lag(offstat, 1, 0) over (order by Date) as prevoff
from (
select
Date,
sum(oncounter) over (order by Date) as onstat,
sum(offcounter) over (order by Date) as offstat
from (
select
StartDate as Date,
case when status = 'on' then 1 else 0 end oncounter,
case when status = 'off' then 1 else 0 end offcounter
from
Dates
union all
select
EndDate as Date,
case when status = 'on' then -1 else 0 end oncounter,
case when status = 'off' then -1 else 0 end offcounter
from
Dates
) TMP
) TMP2
) TMP3
where (onstat = 1 and prevon = 0)
or (onstat = 0 and prevon = 1)
or (onstat = 0 and offstat = 1 and prevoff = 0)
or (onstat = 0 and offstat = 0 and prevoff = 1)
) TMP4
It has quite many derived tables for the window functions and getting only the status changes into the result set so lead can pick up correct dates. It might be possible to get rid of some of them.
SQL Fiddle: http://sqlfiddle.com/#!6/b5cfa/7

SQL Match employee intime (punch time) with employee shift

I have a Stored Procedure that retrieves employee daily summary intime - outtime:
SELECT ads.attendancesumid,
ads.employeeid,
ads.date,
ads.day, -- month day number
ads.intime,
ads.outtime
--employee shift intime and outtime
ss.intime,
ss.outtime
FROM employee_attendance_daily_summary ads
JOIN employee emp
ON emp.employeeid = ads.employeeid
JOIN setup_shift ss
ON ss.shiftcode = emp.shiftcode
AND DATEPART(dw, ads.date) = ss.day
WHERE ads.employeeid = 4 -- just to filter one employee
The result of the query is something like this:
Each day is repeated 3 times because table setup_shift (employee shifts) has:
Monday to Sunday for 3 different shift types: DAY, AFTERNOON and NIGHT.
Here is the same info but with the shift type column:
What I need is to ONLY get 1 row per day but with the closest employee shift depending on the intime and outtime.
So the desire result should looks like this:
Any clue on how to do this? Appreciate it in advance.
I have also these case where intime is 00:00:00 but outtime has a value:
UPDATE:
HERE IS THE SQL FIDDLE
http://sqlfiddle.com/#!6/791cb/7
select ads.attendancesumid,
ads.employeeid,
ads.date,
ads.day,
ads.intime,
ads.outtime,
ss.intime,
ss.outtime
from employee_attendance_daily_summary ads
join employee emp
on emp.employeeid = ads.employeeid
join setup_shift ss
on ss.shiftcode = emp.shiftcode
and datepart(dw, ads.date) = ss.day
where ads.employeeid = 4
and ((abs(datediff(hh,
cast(ads.intime as datetime),
cast(ss.intime as datetime))) between 0 and 2) or
(ads.intime = '00:00:00' and
ss.intime =
(select min(x.intime)
from setup_shift x
where x.shiftcode = ss.shiftcode
and x.intime > (select min(y.intime)
from setup_shift y
where y.shiftcode = x.shiftcode))))
This would be much easier if the times were in seconds after midnight, rather than in a time, datetime, or string format. You can convert them using the formula:
select datepart(hour, intime) * 3600 + datepart(minute, intime) * 60 + datepart(second, intime)
(Part of this is just my own discomfort with all the nested functions needed to handle other data types.)
So, let me assume that you have a series of similar columns measured in seconds. You can then approach this problem by taking the overlap with each shift and choosing the shift with the largest overlap.
with t as (
<your query here>
),
ts as (
select t.*,
(datepart(hour, ads.intime) * 3600 + datepart(minute, ads.intime) * 60 +
datepart(second, ads.intime)
) as e_intimes,
. . .
from t
),
tss as (
select ts.*,
(case when e_intimes >= s_outtimes then 0
when e_outtimes <= s_inttimes then 0
else (case when e_outtimes < s_outtimes then e_outtimes else s_outtimes end) -
(case when e_intimes > s_intimes then e_intimes else s_intimes end)
end) as overlap
from ts
)
select ts.*
from (select ts.*,
row_number() over (partition by employeeid, date
order by overlap desc
) as seqnum
from ts
) ts
where seqnum = 1;
Try this man,I just take the minimum time difference of the each set datediff(mi,intime,shift_intime)
Select * from
(select
row_number() over(partition by employeeid
order by datediff(mi,intime,shift_intime) asc) as id,
attendance,employeeid,date,day,intime,outime,shiftintime,shiftoutime from table
)
where id=1

Merge adjacent rows in SQL?

I'm doing some reporting based on the blocks of time employees work. In some cases, the data contains two separate records for what really is a single block of time.
Here's a basic version of the table and some sample records:
EmployeeID
StartTime
EndTime
Data:
EmpID Start End
----------------------------
#1001 10:00 AM 12:00 PM
#1001 4:00 PM 5:30 PM
#1001 5:30 PM 8:00 PM
In the example, the last two records are contiguous in time. I'd like to write a query that combines any adjacent records so the result set is this:
EmpID Start End
----------------------------
#1001 10:00 AM 12:00 PM
#1001 4:00 PM 8:00 PM
Ideally, it should also be able to handle more than 2 adjacent records, but that is not required.
This article provides quite a few possible solutions to your question
http://www.sqlmag.com/blog/puzzled-by-t-sql-blog-15/tsql/solutions-to-packing-date-and-time-intervals-puzzle-136851
This one seems like the most straight forward:
WITH StartTimes AS
(
SELECT DISTINCT username, starttime
FROM dbo.Sessions AS S1
WHERE NOT EXISTS
(SELECT * FROM dbo.Sessions AS S2
WHERE S2.username = S1.username
AND S2.starttime < S1.starttime
AND S2.endtime >= S1.starttime)
),
EndTimes AS
(
SELECT DISTINCT username, endtime
FROM dbo.Sessions AS S1
WHERE NOT EXISTS
(SELECT * FROM dbo.Sessions AS S2
WHERE S2.username = S1.username
AND S2.endtime > S1.endtime
AND S2.starttime <= S1.endtime)
)
SELECT username, starttime,
(SELECT MIN(endtime) FROM EndTimes AS E
WHERE E.username = S.username
AND endtime >= starttime) AS endtime
FROM StartTimes AS S;
If this is strictly about adjacent rows (not overlapping ones), you could try the following method:
Unpivot the timestamps.
Leave only those that have no duplicates.
Pivot the remaining ones back, coupling every Start with the directly following End.
Or, in Transact-SQL, something like this:
WITH unpivoted AS (
SELECT
EmpID,
event,
dtime,
count = COUNT(*) OVER (PARTITION BY EmpID, dtime)
FROM atable
UNPIVOT (
dtime FOR event IN (StartTime, EndTime)
) u
)
, filtered AS (
SELECT
EmpID,
event,
dtime,
rowno = ROW_NUMBER() OVER (PARTITION BY EmpID, event ORDER BY dtime)
FROM unpivoted
WHERE count = 1
)
, pivoted AS (
SELECT
EmpID,
StartTime,
EndTime
FROM filtered
PIVOT (
MAX(dtime) FOR event IN (StartTime, EndTime)
) p
)
SELECT *
FROM pivoted
;
There's a demo for this query at SQL Fiddle.
CTE with cumulative sum:
DECLARE #t TABLE(EmpId INT, Start TIME, Finish TIME)
INSERT INTO #t (EmpId, Start, Finish)
VALUES
(1001, '10:00 AM', '12:00 PM'),
(1001, '4:00 PM', '5:30 PM'),
(1001, '5:30 PM', '8:00 PM')
;WITH rowind AS (
SELECT EmpId, Start, Finish,
-- IIF returns 1 for each row that should generate a new row in the final result
IIF(Start = LAG(Finish, 1) OVER(PARTITION BY EmpId ORDER BY Start), 0, 1) newrow
FROM #t),
groups AS (
SELECT EmpId, Start, Finish,
-- Cumulative sum
SUM(newrow) OVER(PARTITION BY EmpId ORDER BY Start) csum
FROM rowind)
SELECT
EmpId,
MIN(Start) Start,
MAX(Finish) Finish
FROM groups
GROUP BY EmpId, csum
I have changed a lil' bit the names and types to make the example smaller but this works and should be very fast and it has no number of records limit:
with cte as (
select
x1.id
,x1.t1
,x1.t2
,case when x2.t1 is null then 1 else 0 end as bef
,case when x3.t1 is null then 1 else 0 end as aft
from x x1
left join x x2 on x1.id=x2.id and x1.t1=x2.t2
left join x x3 on x1.id=x3.id and x1.t2=x3.t1
where x2.id is null
or x3.id is null
)
select
cteo.id
,cteo.t1
,isnull(z.t2,cteo.t2) as t2
from cte cteo
outer apply (select top 1 *
from cte ctei
where cteo.id=ctei.id and cteo.aft=0 and ctei.t1>cteo.t1
order by t1) z
where cteo.bef=1
and the fiddle for it : http://sqlfiddle.com/#!3/ad737/12/0
Option with Inline User-Defined Function AND CTE
CREATE FUNCTION dbo.Overlap
(
#availStart datetime,
#availEnd datetime,
#availStart2 datetime,
#availEnd2 datetime
)
RETURNS TABLE
RETURN
SELECT CASE WHEN #availStart > #availEnd2 OR #availEnd < #availStart2
THEN #availStart ELSE
CASE WHEN #availStart > #availStart2 THEN #availStart2 ELSE #availStart END
END AS availStart,
CASE WHEN #availStart > #availEnd2 OR #availEnd < #availStart2
THEN #availEnd ELSE
CASE WHEN #availEnd > #availEnd2 THEN #availEnd ELSE #availEnd2 END
END AS availEnd
;WITH cte AS
(
SELECT EmpID, Start, [End], ROW_NUMBER() OVER (PARTITION BY EmpID ORDER BY Start) AS Id
FROM dbo.TableName
), cte2 AS
(
SELECT Id, EmpID, Start, [End]
FROM cte
WHERE Id = 1
UNION ALL
SELECT c.Id, c.EmpID, o.availStart, o.availEnd
FROM cte c JOIN cte2 ct ON c.Id = ct.Id + 1
CROSS APPLY dbo.Overlap(c.Start, c.[End], ct.Start, ct.[End]) AS o
)
SELECT EmpID, Start, MAX([End])
FROM cte2
GROUP BY EmpID, Start
Demo on SQLFiddle