MS SQL get aggregate datetime difference by status - sql

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.

Related

How to calculate Total working hours of employee by each session

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

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

SQL: selecting rows where column value changed last time

I need to get the latest date where number is changed I have this SQL statement
Select
a.group, a.date a.number
From
xx.dbo.list a
Where
a.group in ('10, '10NC', '210')
And a.date >= '2018-06-01'
And a.number > 0
And a. number <> (Select Top 1 b.number
From xxx.dbo.list b
Where b.group = a.group
And b.date >= '2018-06-01'
And b.number > 0
And b.date < a.date
Order by b.date desc)
order by a.date desc
I have a table that looks like this
Group date Number
--------------------------
10 2018-02-06 4
10 2018-04-06 4
10 2018-06-12 4
10NC 2018-02-06 68
10NC 2018-04-06 35
10NC 2018-06-11 35
10NC 2018-06-12 68
10NC 2018-06-13 35
210 2018-06-02 94
210 2018-06-04 100
210 2018-06-06 100
210 2018-06-07 93
I get this output now, but I only want to get the rows with X
Group date Number
------------------------------
10NC 2018-06-12 68
10NC 2018-06-13 35 X
210 2018-06-04 100
210 2018-06-07 93 X
Can anyone help?
You would use lag():
select a.*
from (select a.group, a.date, a.number, lag(a.number) over (partition by group order by date) as prev_number
From xx.dbo.list a
where a.group in ('10', '10NC', '210') And
a.date >= '2018-06-01' And
a.number > 0
) a
where prev_number <> number;
Is this what is Expected?
DECLARE #List TABLE ([Group] VARCHAR(100), [Date] DATE, Number INT)
INSERT INTO #List
SELECT '10','2018-02-06',4
UNION ALL
SELECT '10','2018-04-06',4
UNION ALL
SELECT '10','2018-06-12',4
UNION ALL
SELECT '10NC','2018-02-06',68
UNION ALL
SELECT '10NC','2018-04-06',35
UNION ALL
SELECT '10NC','2018-06-11',35
UNION ALL
SELECT '10NC','2018-06-12',68
UNION ALL
SELECT '10NC','2018-06-13',35
UNION ALL
SELECT '210','2018-06-02',94
UNION ALL
SELECT '210','2018-06-04',100
UNION ALL
SELECT '210','2018-06-06',100
UNION ALL
SELECT '210','2018-06-07',93
;WITH CTE AS
(
SELECT
*
,RN = ROW_NUMBER() OVER (Partition by [Group] ORDER BY [DATE] DESC)
FROM #List
WHERE
[Date] >= '2018-06-01'
AND [Group] in ('10', '10NC', '210')
And Number > 0
)
SELECT * FROM CTE WHERE RN = 1
Note: I am posting it directly in answer as i don't have enough reputation to ask questions in comments.

get all record with minimum date in every month but the latest time

Let say I have this kind of data:
Date Category Amount
01/10/2014 20:04 2 12212
01/11/2014 0:00 3 11043.38
01/11/2014 16:03 2 12082
01/11/2014 16:32 3 110.43
01/12/2014 20:41 2 12196
01/12/2014 20:42 3 103.22
31/12/2014 14:20 2 12440
31/12/2014 14:21 3 104.25
I wish to get below result:
Date Category Amount
01/10/2014 20:04 2 12212
01/11/2014 16:03 2 12082
01/11/2014 16:32 3 110.43
01/12/2014 20:41 2 12196
01/12/2014 20:42 3 103.22
So far, I am able to make this query:
select t.date, t.Category, t.Amount
from mytable t
inner join (
select Category,MONTH(date) MONTHH,YEAR(date) YEARR, max(date) as MaxDate
from mytable
group by Category,MONTH(date) MONTHH,YEAR(date) YEARR
) tm on t.date = tm.MaxDate and t.Category = tm.Category
But it returns a wrong result if there is more than 1 date in 1 month. This is the result:
Date Category Amount
01/10/2014 20:04 2 12212
01/11/2014 16:03 2 12082
01/11/2014 16:32 3 110.43
31/12/2014 14:20 2 12440
31/12/2014 14:21 3 104.25
Could anyone help please? thanks
Use row_number() and rank():
select t.*
from (select t.*,
dense_rank() over (partition by year(date), month(date) order by day(date)) as seqnum,
row_number() over (partition by year(date), month(date), day(date) order by date desc) as seqnum_d
from mytable t
) t
where seqnum = 1 and seqnum_d = 1;
Try This
;WITH CTE
AS
(
SELECT
RN = ROW_NUMBER() OVER(PARTITION BY MONTH([Date]),YEAR([Date]),Category ORDER BY [Date]),
*
FROM YourTable
)
SELECT
Date,
Category,
Amount
FROM CTE
WHERE RN = 1
Do not use reserved words as columns (i.e. Date).
No need to group, you are not using any summation/averaging:
select *
from tda t
where not exists(
-- not exists: same date, later time, cat is same
select 1
from tda b
where 1 = 1
and cast(t.dDate as date) = cast(b.dDate as date)
and t.ddate > b.ddate
and t.cat = b.cat
)
and not exists(
-- not exists: same month, earlier date
select 1
from tda b
where month(b.ddate) = month(t.ddate)
and day(b.ddate)<day(t.ddate)
)
to get (reformat the date to your liking):
ddate Cat Amount
2014-10-01T20:04:00Z 2 12212
2014-11-01T00:00:00Z 3 11043.38
2014-11-01T16:03:00Z 2 12082
2014-12-01T20:41:00Z 2 12196
2014-12-01T20:42:00Z 3 103.22
by
CREATE TABLE tda ([ddate] datetime,[Cat] int, [Amount] numeric(10,2));
INSERT INTO tda (dDate, Cat, Amount)
VALUES
('2014/10/01 20:04', 2, 12212),
('2014/11/01 0:00', 3, 11043.38),
('2014/11/01 16:03', 2, 12082),
('2014/11/01 16:32', 3, 110.43),
('2014/12/01 20:41', 2, 12196),
('2014/12/01 20:42', 3, 103.22),
('2014/12/31 14:20', 2, 12440),
('2014/12/31 14:21', 3, 104.25)
;