How to calculate Total working hours of employee by each session - sql

I need to display employee login and logout hours for each session of the day and calculate Total Working hours. If the employee login and logout many times, each session and total should be displayed.
My table will be look like this
Id
EmpId
LocationId
LogDate
AccessType
OutType
1
4545_1
4545
2022-05-25 16:27:41.217
1
NULL
2
4545_1
4545
2022-05-25 17:27:26.673
2
1
4
4545_1
4545
2022-05-25 17:31:30.333
1
NULL
5
4545_1
4545
2022-05-25 19:31:38.973
2
1
6
1212_8
1212
2022-05-26 10:21:38.973
1
NULL
6
1212_8
1212
2022-05-26 12:21:38.973
2
2
Here
AccessType 1=IN 2=OUT
OutType 1=LogOut 2=Session Out
I want the output like this
EmpId
LocationId
SessionStart
SessionEnd
Hours
4545_1
4545
2022-05-25 16:27:41.217
2022-05-25 17:27:26.673
1:00
4545_1
4545
2022-05-25 17:31:30.333
2022-05-25 19:31:38.973
2:00
1212_8
1212
2022-05-26 10:21:38.973
2022-05-26 12:21:38.973
2:00
This is what I tried
select[EmpId],
[LocationId],
ShiftDate,
SessionStartTime,
SessionEndTime
, Total_Time = right(concat('0', Total_Time / 3600), 2) + ':' + right(concat('0', Total_Time % 3600 / 60), 2)
from (
select
[EmpId],[LocationId], ShiftDate = cast(min(LogDate) as date)
, SessionStartTime = min(LogDate)
, SessionEndTime = max(LogDate)
, Total_Time = sum(ss)
from (
select
*, ss = datediff(ss, LogDate, lead(LogDate) over (partition by [EmpId], grp order by LogDate))
from (
select
*, grp = sum(diff) over (partition by [EmpId] order by LogDate)
from (
select
*, diff = iif(datediff(mi, lag(LogDate) over (partition by [EmpId] order by LogDate), LogDate) > 300 and [AccessType] = 1, 1, 0)
from
[tblEmployeeAttendance] where cast(LogDate as date) >= '2022-05-25' and cast(LogDate as date) <= '2022-05-26'
) t
) t
) t
group by [EmpId],[LocationId], grp) t
I got result like this
EmpId
LocationId
SessionStart
SessionEnd
Hours
4545_1
4545
2022-05-25 16:27:41.217
2022-05-25 19:31:38.973
3:00
1212_8
1212
2022-05-26 10:21:38.973
2022-05-26 12:21:38.973
2:00
Here the problem is I get only min login and max logout for the day in one row. But I need each login and logout session of the user on the day and total for each.
I request somebody to help on this.

You can do this with an outer apply.
I don't know what the OutType is used for, what the difference is with AccessType. You did not explain that or any other logic so I just assumed it should work on AccessType.
If that is not the case you can easy adapt the logic in the subquery below.
select e.EmpId,
e.LocationID,
e.logdate as SessionStart,
d.logdate as SessionEnd,
datediff(hour, e.logdate, d.logdate) as Hours
from emp e
outer apply ( select top 1 emp.logdate
from emp
where emp.empid = e.empid
and emp.accesstype = 2
and emp.logdate > e.logdate
order by emp.logdate
) d
where e.accesstype = 1
See the DBFiddle here
Result
EmpId
LocationID
SessionStart
SessionEnd
Hours
4545_1
4545
2022-05-25 16:27:41.217
2022-05-25 17:27:26.673
1
4545_1
4545
2022-05-25 17:31:30.333
2022-05-25 19:31:38.973
2
1212_8
1212
2022-05-26 10:21:38.973
2022-05-26 12:21:38.973
2

Related

How can I create a "start" "end" time table from a timestamp list

I am trying to create a view that displays the time of employee stamps.
This is what the table looks like now:
Person
Person_Number
Date
Stamp_number
Time_Stamp
Paul
1
22-10-24
1
8:00
Paul
1
22-10-24
2
10:00
Paul
1
22-10-24
3
10:30
Paul
1
22-10-24
4
12:00
Jimmy
2
22-10-23
1
9:00
Jimmy
2
22-10-23
2
11:00
Jimmy
2
22-10-23
3
12:00
And I would like it to look like this using only a select query
Person
Person_Number
Date
Start
End
Duration
Paul
1
22-10-24
8:00
10:00
2:00
Paul
1
22-10-24
10:30
12:00
1:30
Jimmy
2
22-10-23
9:00
11:00
2:00
Jimmy
1
22-10-23
12:00
null
null
Is it possible ?
We can use conditional aggregation along with a ROW_NUMBER trick:
WITH cte AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY Person_Number, Date
ORDER BY Stamp_number) - 1 rn
FROM yourTable
)
SELECT Person, Person_Number, Date,
MAX(CASE WHEN rn % 2 = 0 THEN Time_Stamp END) AS [Start],
MAX(CASE WHEN rn % 2 = 1 THEN Time_Stamp END) AS [End],
DATEDIFF(MINUTE,
MAX(CASE WHEN rn % 2 = 0 THEN Time_Stamp END),
MAX(CASE WHEN rn % 2 = 1 THEN Time_Stamp END)) AS Duration
FROM cte
GROUP BY Person, Person_Number, Date, rn / 2
ORDER BY 2, 4;
Here is a working demo.
Try the following:
SELECT Person, Person_Number, Date, [Start], [End],
CONVERT(TIME(0), CONVERT(DATETIME, [End]) - CONVERT(DATETIME, [Start])) AS Duration
FROM
(
SELECT Person, Person_Number, Date, MIN(Time_Stamp) AS [Start],
CASE
WHEN MAX(Time_Stamp) <> MIN(Time_Stamp)
THEN MAX(Time_Stamp)
END AS [End] /* To select End as null when there is no End for a Start */
FROM table_name
GROUP BY Person, Person_Number, Date, (Stamp_number+1)/2
) T
ORDER BY Person_Number, Date, [Start]
See a demo.

How to filter unique records depending to some condition by SQL

I have a SQL Server database with a table that gets data from Time and Attendance devices.
It looks like this:
ID
UserID
Date
Time
SignDirection
1988781
25000
2022/01/11
10:02
1
1988782
25000
2022/01/11
10:03
1
1988783
25000
2022/01/11
10:04
1
1988784
25000
2022/01/11
12:30
2
1988785
25000
2022/01/11
12:31
2
1988786
25001
2022/01/11
10:00
1
1988787
25001
2022/01/11
12:30
2
1988788
25002
2022/01/11
10:15
1
1988789
25002
2022/01/11
10:16
1
1988790
25002
2022/01/11
12:19
2
How to filter the data for each User ID to have lower time for SignDirection = 1, and the higher time for SignDirection = 2 ?
To be like :
ID
UserID
Date
Time
SignDirection
1988781
25000
2022/01/11
10:02
1
1988785
25000
2022/01/11
12:31
2
1988786
25001
2022/01/11
10:00
1
1988787
25001
2022/01/11
12:30
2
1988788
25002
2022/01/11
10:15
1
1988790
25002
2022/01/11
12:19
2
I have tried
select distinct
but had no success with that.
The answer given by #LukStorms is good, but it requires another sort for the second row-number.
You can instead use LEAD with the same sort to detect the last row
SELECT
ID,
UserID,
[Date],
[Time],
SignDirection
FROM
(
SELECT *
, ROW_NUMBER() OVER (PARTITION BY UserID, SignDirection, [Date] ORDER BY [Time]) rn_asc
, LEAD(Time) OVER (PARTITION BY UserID, SignDirection, [Date] ORDER BY [Time]) nextTime
FROM YourTable t
) q
WHERE ((SignDirection = 1 AND rn_asc = 1) OR
(SignDirection = 2 AND nextTime IS NULL))
ORDER BY
ID,
UserID,
[Date],
[Time],
SignDirection;
db<>fiddle
You can calculate 2 row_number, up & down.
Then filter on them.
SELECT ID, UserID, [Date], [Time], SignDirection
FROM
(
SELECT *
, ROW_NUMBER() OVER (PARTITION BY UserID, SignDirection, [Date] ORDER BY [Time] ASC) rn_asc
, ROW_NUMBER() OVER (PARTITION BY UserID, SignDirection, [Date] ORDER BY [Time] DESC) rn_desc
FROM your_time_and_attendance_table t
) q
WHERE ((SignDirection = 1 AND rn_asc = 1) OR
(SignDirection = 2 AND rn_desc = 1))
ORDER BY ID, UserID, [Date], [Time], SignDirection

Get users attendance entry and exit in one row SQL Server

I have a table with all entries for employees. I need to get all the working hours and the entry and exit time of the user in one record.
The table is like this:
How can I do that and also in case there is some missing entries or exit. Like one employee will have entry with no exit in some odd cases.
Assuming that the ins and outs line up (that is, are strictly interleaved), you can use lead() and some filtering:
select t.empId, convert(date, datetime) as date, datetime as timein,
next_datetime as timeout,
datediff(minute, datetime, next_datetime) / 60.0 as decimal_hours
from (select t.*,
lead(datetime) over (partition by empid order by datetime) as next_datetime
from t
) t
where entrytype = 'IN';
Note that this formats the duration as decimal hours rather than as a time. That part does not seem relevant to the actual question and just complicates the query.
This adds LEAD entrytype to make sure there is a corresponding OUT row. Also, it divides the date difference in minutes by 60.0 (added decimal)
select t.empId EmpID, cast(datetime as date) [Day], datetime [Timein], next_datetime [Timeout],
datediff(mi, datetime, next_datetime)/60.0 TotalHours
from (select t.*,
lead(datetime) over (partition by empid order by datetime) as next_datetime,
lead(entrytype) over (partition by empid order by datetime) as next_entrytype
from t
) t
where entrytype = 'IN'
and next_entrytype='Out';
Using Row_number to identify IN and OUT related to which employee:
SELECT EMPID, CAST([DATEUPDT] AS DATE) AS Date,
MAX(CASE WHEN ENTRYTYPE = 'IN' THEN CAST([DATEUPDT] AS TIME) END) AS TIMEIN,
MAX(CASE WHEN ENTRYTYPE = 'OUT' THEN CAST([DATEUPDT] AS TIME) END) AS TIMEOUT,
ABS(DATEDIFF(MINUTE, MAX(CASE WHEN ENTRYTYPE = 'OUT' THEN CAST([DATEUPDT] AS TIME) END), MAX(CASE WHEN ENTRYTYPE = 'IN' THEN CAST([DATEUPDT] AS TIME) END)))/60 AS DURATION
FROM
(
SELECT A.*,
ROW_NUMBER() OVER(PARTITION BY EMPID, [ENTRYTYPE] ORDER BY [DATEUPDT]) RN1
FROM EMPLOYEE_LOG A
) X
GROUP BY EMPID, RN1, CAST([DATEUPDT] AS DATE)
ORDER BY EMPID, RN1;
You can also "sessionize" in SQL Server - by using OLAP queries: With a counter that is at 1 when a new session begins and at 0 otherwise
WITH
input(id,empid,dttime,entrytype) AS (
SELECT 1,125,CAST('2020-08-13 08:10:00.000' AS DATETIME),'IN'
UNION ALL SELECT 1,157,CAST('2020-08-13 08:01:00.000' AS DATETIME),'IN'
UNION ALL SELECT 1,125,CAST('2020-08-13 15:21:00.000' AS DATETIME),'OUT'
UNION ALL SELECT 1,125,CAST('2020-08-13 15:24:00.000' AS DATETIME),'IN'
UNION ALL SELECT 1,125,CAST('2020-08-13 17:24:00.000' AS DATETIME),'OUT'
UNION ALL SELECT 1,157,CAST('2020-08-13 15:01:00.000' AS DATETIME),'OUT'
UNION ALL SELECT 1,125,CAST('2020-08-14 08:10:00.000' AS DATETIME),'IN'
UNION ALL SELECT 1,157,CAST('2020-08-14 08:01:00.000' AS DATETIME),'IN'
UNION ALL SELECT 1,125,CAST('2020-08-14 15:21:00.000' AS DATETIME),'OUT'
UNION ALL SELECT 1,125,CAST('2020-08-14 15:24:00.000' AS DATETIME),'IN'
UNION ALL SELECT 1,125,CAST('2020-08-14 17:24:00.000' AS DATETIME),'OUT'
UNION ALL SELECT 1,157,CAST('2020-08-14 15:01:00.000' AS DATETIME),'OUT'
)
,
with_session AS (
SELECT
*
, SUM(CASE entrytype WHEN 'IN' THEN 1 ELSE 0 END) OVER(
PARTITION BY empid ORDER BY dttime
) AS sessid
FROM input
)
SELECT
id
, empid
, sessid
, CAST(MAX(CASE entrytype WHEN 'IN' THEN dttime END) AS DATE) AS day
, CAST(MAX(CASE entrytype WHEN 'IN' THEN dttime END) AS TIME) AS indtm
, CAST(MAX(CASE entrytype WHEN 'OUT' THEN dttime END) AS TIME) AS outdtm
, CAST(
MAX(CASE entrytype WHEN 'OUT' THEN dttime END)
- MAX(CASE entrytype WHEN 'IN' THEN dttime END)
AS TIME
) AS totalhours
FROM with_session
GROUP BY
id
, empid
, sessid
ORDER BY
id
, 4
, empid
, sessid
;
-- out id | empid | sessid | day | indtm | outdtm | totalhours
-- out ----+-------+--------+------------+----------+----------+------------
-- out 1 | 125 | 1 | 2020-08-13 | 08:10:00 | 15:21:00 | 07:11:00
-- out 1 | 125 | 2 | 2020-08-13 | 15:24:00 | 17:24:00 | 02:00:00
-- out 1 | 157 | 1 | 2020-08-13 | 08:01:00 | 15:01:00 | 07:00:00
-- out 1 | 125 | 3 | 2020-08-14 | 08:10:00 | 15:21:00 | 07:11:00
-- out 1 | 125 | 4 | 2020-08-14 | 15:24:00 | 17:24:00 | 02:00:00
-- out 1 | 157 | 2 | 2020-08-14 | 08:01:00 | 15:01:00 | 07:00:00

MS SQL get aggregate datetime difference by status

I have below table in sql.
======================================================
UnitID Status DateTime Value
======================================================
101 A 01/12/2017 00:02:10 10
101 A 01/12/2017 00:02:40 25
101 A 01/12/2017 00:03:20 18
101 B 01/12/2017 00:03:55 30
101 B 01/12/2017 00:04:05 10
101 B 01/12/2017 00:04:30 20
101 B 01/12/2017 00:04:50 10
101 A 01/12/2017 00:05:00 28
101 A 01/12/2017 00:05:50 18
101 A 01/12/2017 00:06:20 18
102 A 01/12/2017 00:02:10 10
102 A 01/12/2017 00:02:40 25
102 A 01/12/2017 00:03:20 18
102 B 01/12/2017 00:03:55 30
102 B 01/12/2017 00:04:05 10
102 B 01/12/2017 00:04:30 20
102 B 01/12/2017 00:04:50 10
102 A 01/12/2017 00:05:00 28
102 A 01/12/2017 00:05:50 18
102 A 01/12/2017 00:06:20 18
From this table i need below mention output.
===========================================
UnitID StatusA StatusB MaxValue
===========================================
101 02:30 00:55 30
102 02:30 00:55 30
what i need is the total time difference by status. so how could i achieve this in mssql query. so here 02:30 is time duration for status "A" in the table.
Thank you in advanced.
As far as I know you cannot have status in different columns, only by row.
SELECT [UnitID], [Status], MAX([DateTime]) - MIN([DateTime]), MAX([Value])
FROM [theTable]
GROUP BY [UnitID], [Status]
Output would be like
101 A 02:30 30
101 B 00:55 30
102 A 02:30 30
102 B 00:55 30
If you have fixed states of A and B you can go messy and do this:
SELECT UnitID, A, B, MaxValue
FROM
(
SELECT [UnitID], MAX([DateTime]) - MIN([DateTime]) AS A, null AS B, MAX([Value]) AS MaxValue
FROM [theTable]
WHERE Status = 'A'
GROUP BY [UnitID]
UNION ALL
SELECT [UnitID], null, MAX([DateTime]) - MIN([DateTime]), MAX([Value])
FROM [theTable]
WHERE Status = 'B'
GROUP BY [UnitID]
) x
You can do what you need with the following query. I tried to separate each step on different CTE's so you can see step by step how to get to your result. LAG will retrieve the previous row value (spliting by the PARTITION BY columns and ordering by the ORDER BY).
;WITH LaggedValues AS
(
SELECT
M.UnitID,
M.Status,
M.DateTime,
LaggedDateTime = LAG(M.DateTime) OVER (PARTITION BY M.UnitID ORDER BY M.DateTime ASC),
LaggedStatus = LAG(M.Status) OVER (PARTITION BY M.UnitID ORDER BY M.DateTime ASC)
FROM
Measures AS M
),
TimeDifferences AS
(
SELECT
T.*,
SecondDifference = CASE
WHEN T.Status = T.LaggedStatus THEN DATEDIFF(SECOND, T.LaggedDateTime, T.DateTime) END
FROM
LaggedValues AS T
),
TotalsByUnitAndStatus AS
(
SELECT
T.UnitID,
T.Status,
SecondDifference = SUM(T.SecondDifference)
FROM
TimeDifferences AS T
GROUP BY
T.UnitID,
T.Status
),
TotalsByUnit AS -- Conditional aggregation (alternative to PIVOT)
(
SELECT
T.UnitID,
StatusA = MAX(CASE WHEN T.Status = 'A' THEN T.SecondDifference END),
StatusB = MAX(CASE WHEN T.Status = 'B' THEN T.SecondDifference END)
FROM
TotalsByUnitAndStatus AS T
GROUP BY
T.UnitID
)
SELECT
T.UnitID,
StatusA = CONVERT(VARCHAR(10), T.StatusA / 60) + ':' + CONVERT(VARCHAR(10), T.StatusA % 60),
StatusB = CONVERT(VARCHAR(10), T.StatusB / 60) + ':' + CONVERT(VARCHAR(10), T.StatusB % 60)
FROM
TotalsByUnit AS T
You can get the difference for each group:
select unitid, status, min(datetime) as mindt, max(datetime) as maxdt, max(value) as maxvalue
from (select t.*,
row_number() over (partition by unitid order by datetime) as seqnum,
row_number() over (partition by unitid, status order by datetime) as seqnum_s
from t
) t
group by unitid, status, (seqnum - seqnum_s);
This solves the "groups-and-islands" problem. Now you can get the information you want using conditional aggregation:
with t as (
select unitid, status, min(datetime) as mindt, max(datetime) as maxdt, max(value) as maxvalue
from (select t.*,
row_number() over (partition by unitid order by datetime) as seqnum,
row_number() over (partition by unitid, status order by datetime) as seqnum_s
from t
) t
group by unitid, status, (seqnum - seqnum_s)
)
select unitid,
sum(case when status = 'A' then datediff(minute, mindt, maxdt) end) as a_minutes,
sum(case when status = 'b' then datediff(minute, mindt, maxdt) end) as a_minutes,
max(maxvalue)
from t
group by unitid;
I'll leave it up to you to convert the minutes back to times.

MsSql Compare specific datetimes in sequence based on ID

I have a table where we store our data from a call and it looks like this:
CallID Arrive_Seq DateTime ActivitytypeID
1 1 2018-01-01 05:00:00 1
1 2 2018-01-01 05:00:01 2
1 3 2018-01-01 06:00:00 21
1 4 2018-01-01 06:00:01 28
1 5 2018-01-01 06:00:02 13
1 6 2018-01-01 06:00:03 22
1 7 2018-01-01 06:00:05 29
1 8 2018-01-01 06:05:00 21
1 9 2018-01-01 06:05:01 28
1 10 2018-01-01 06:05:02 13
1 11 2018-01-01 06:05:03 22
1 12 2018-01-01 06:07:45 29
Now I want to select the datediff between ActivitytypeID 21 and 29 in the arrive_sew order. In this example they occur twice (on arrive_seq 3,8 and 7,12). This order is not specific and ActivitytypeID can occur both more and less times in the sequence but they are always connected with eachother. Think of it as ActivitytypeID 21 = 'call started' AND ActivitytypeID = 29 'Call ended'.
In the example the answer whould be:
SELECT DATEDIFF (SECOND, '2018-01-01 06:00:00', '2018-01-01 06:00:05') = 5 -- Compares datetime of arrive_seq 3 and 7
AND
SELECT DATEDIFF (SECOND, '2018-01-01 06:00:05', '2018-01-01 06:07:45') = 460 -- Compares datetime of arrive_seq 21 and 29
Total duration = 465
I have tried with this code but it doesn't work all the time due to row# can change based on arrive_seq and ActivitytypeID
;WITH CallbackDuration AS (
SELECT ROW_NUMBER() OVER(ORDER BY a.time_stamp ASC) AS RowNumber, DATEDIFF(second, a.time_stamp, b.time_stamp) AS 'Duration'
FROM Table a
JOIN Table b on a.call_id = b.call_id
WHERE a.call_id = 1 AND a.activity_type = 21 AND b.activity_type = 29
GROUP BY a.time_stamp, b.time_stamp,a.call_id)
SELECT SUM(Duration) AS 'Duration' FROM CallbackDuration WHERE RowNumber in (1,5,9)
I think this is what you want:
select
call_start,
call_end,
datediff (second, call_start, call_end) as duration
from
(
select
call_timestamp as call_end,
lag(call_timestamp) over (partition by call_id order by call_timestamp) as call_start,
activity_type as call_end_activity,
lag (activity_type) over (partition by call_id order by call_timestamp) as call_start_activity
from
call_log
where
activity_type in (21, 29)
) x
where
call_start_activity = 21;
Result:
call_start call_end duration
----------------------- ----------------------- -----------
2018-01-01 06:00:00.000 2018-01-01 06:00:05.000 5
2018-01-01 06:05:00.000 2018-01-01 06:07:45.000 165
(2 rows affected)
Note that the time of the second call is based on your sample data with start time 2018-01-01 06:05:00
This query seems to return your expected result
declare #x int = 21
declare #y int = 29
;with cte(CallID, Arrive_Seq, DateTime, ActivitytypeID) as (
select
a, b, cast(c as datetime), d
from (values
(1,1,'2018-01-01 05:00:00',1)
,(1,2,'2018-01-01 05:00:01',2)
,(1,3,'2018-01-01 06:00:00',21)
,(1,4,'2018-01-01 06:00:01',28)
,(1,5,'2018-01-01 06:00:02',13)
,(1,6,'2018-01-01 06:00:03',22)
,(1,7,'2018-01-01 06:00:05',29)
,(1,8,'2018-01-01 06:05:00',21)
,(1,9,'2018-01-01 06:05:01',28)
,(1,10,'2018-01-01 06:05:02',13)
,(1,11,'2018-01-01 06:05:03',22)
,(1,12,'2018-01-01 06:07:45',29)
) t(a,b,c,d)
)
select
sum(ss)
from (
select
*, ss = datediff(ss, DateTime, lead(datetime) over (order by Arrive_Seq))
, rn = row_number() over (order by Arrive_Seq)
from
cte
where
ActivitytypeID in (#x, #y)
) t
where
rn % 2 = 1