select first in and last out time - different date - from data finger - sql

Here is my data finger table, [dbo].[tFPLog]
CardID Date Time TransactionCode
100 2020-09-01 08:00 IN
100 2020-09-01 17:00 OUT
100 2020-09-01 17:10 OUT
200 2020-09-01 16:00 IN
200 2020-09-02 02:00 OUT
200 2020-09-02 02:15 OUT
100 2020-09-02 07:00 IN
100 2020-09-02 16:00 OUT
200 2020-09-02 09:55 IN
200 2020-09-02 10:00 IN
200 2020-09-02 21:00 OUT
Conditions
Assume Employees will be IN and OUT in same day/next day.
Assume There will be multiple IN and OUT for same day/next day for employees. So need first IN and Last Out.
Duration = (FirstInTime - LastOutTime)
The current result i get using the query:
WITH CTE AS(
SELECT CardID,
[Date] AS DateIn,
MIN(CASE TransactionCode WHEN 'In' THEN [time] ELSE '23:59:59.999' END) AS TimeIn, --'23:59:59.999' as we are after the MIN, and NULL is the lowest value
[Date] AS DateOut,
MAX(CASE TransactionCode WHEN 'Out' THEN [time] END) AS TimeOut
FROM YourTable
GROUP BY CardID, [Date])
SELECT C.DateIn,
C.TimeIn,
C.DateOut,
C.TimeOut,
DATEADD(MINUTE,DATEDIFF(MINUTE,C.TimeIn,C.TimeOut),CONVERT(time(0),'00:00:00')) AS Duration
FROM CTE C;
=====The Current Result======
CardID DateIN TimeIN DateOUT TimeOUT Duration
100 2020-09-01 08:00 2020-09-01 17:10 09:10
200 2020-09-01 16:00 ? ? ?
100 2020-09-02 07:00 2020-09-02 16:00 09:00
200 2020-09-02 09:55 2020-09-02 21:00 11:05
=====The Result Needed=====
I want this result.
CardID DateIN TimeIN DateOUT TimeOUT Duration
100 2020-09-01 08:00 2020-09-01 17:10 09:10
200 2020-09-01 16:00 2020-09-02 02:15 10:15
100 2020-09-02 07:00 2020-09-02 16:00 09:00
200 2020-09-02 09:55 2020-09-02 21:00 11:05
How to get the DateOUT and TimeOUT in the nextday? with the condition FIRST IN AND LAST OUT. Please help, thank you in advance.

This seems like you were really overly complicating the problem. Just use some conditional aggregation, and then get the difference in minutes:
WITH CTE AS(
SELECT CardID,
[Date] AS DateIn,
MIN(CASE TransactionCode WHEN 'In' THEN [time] ELSE '23:59:59.999' END) AS TimeIn, --'23:59:59.999' as we are after the MIN, and NULL is the lowest value
[Date] AS DateOut,
MAX(CASE TransactionCode WHEN 'Out' THEN [time] END) AS TimeOut
FROM YourTable
GROUP BY CardID, [Date])
SELECT C.DateIn,
C.TimeIn,
C.DateOut,
C.TimeOut,
DATEADD(MINUTE,DATEDIFF(MINUTE,C.TimeIn,C.TimeOut),CONVERT(time(0),'00:00:00')) AS Duration
FROM CTE C;
This assumes that [date] is a date and [time] is a time (because, after all, that is what they are called...).
Side Note: it seems some what redundant have a DateIn and DateOut column when they will always have the same value. Might as well just have a [Date] Column.
Or perhaps, you are actually after this?
WITH CTE AS(
SELECT CardID,
[Date] AS DateIn,
[Time] AS TimeIn,
LEAD([Date]) OVER (PARTITION BY CardID ORDER BY [Date], [Time]) AS DateOut,
LEAD([Time]) OVER (PARTITION BY CardID ORDER BY [Date], [Time]) AS TimeOut,
TransactionCode
FROM dbo.YourTable)
SELECT C.DateIn,
C.TimeIn,
C.DateOut,
C.TimeOut
FROM CTE C
WHERE TransactionCode = 'IN';
Note that if that is the case, you would actually be better off storing the values [date] and [time] in a single column as a datetime/datetime2, not separate ones; as the values are clearly not distinct from each other.
Based on the (hopefully) final goal posts:
WITH VTE AS(
SELECT *
FROM (VALUES(100,CONVERT(date,'20200901'),CONVERT(time(0),'08:00:00'),'IN'),
(100,CONVERT(date,'20200901'),CONVERT(time(0),'17:00:00'),'OUT'),
(100,CONVERT(date,'20200901'),CONVERT(time(0),'17:10:00'),'OUT'),
(200,CONVERT(date,'20200901'),CONVERT(time(0),'16:00:00'),'IN'),
(200,CONVERT(date,'20200902'),CONVERT(time(0),'02:00:00'),'OUT'),
(200,CONVERT(date,'20200902'),CONVERT(time(0),'02:15:00'),'OUT'),
(100,CONVERT(date,'20200902'),CONVERT(time(0),'07:00:00'),'IN'),
(100,CONVERT(date,'20200902'),CONVERT(time(0),'16:00:00'),'OUT'),
(200,CONVERT(date,'20200902'),CONVERT(time(0),'09:55:00'),'IN'),
(200,CONVERT(date,'20200902'),CONVERT(time(0),'10:00:00'),'IN'),
(200,CONVERT(date,'20200902'),CONVERT(time(0),'21:00:00'),'OUT'))V(CardID,[Date],[Time],TransactionCode)),
Changes AS(
SELECT CardID,
DATEADD(MINUTE,DATEDIFF(MINUTE, '00:00:00',[time]),CONVERT(datetime2(0),[date])) AS Dt2, --Way easier to work with later
TransactionCode,
CASE TransactionCode WHEN LEAD(TransactionCode) OVER (PARTITION BY CardID ORDER BY [Date],[Time]) THEN 0 ELSE 1 END AS CodeChange
FROM VTE V),
Groups AS(
SELECT CardID,
dt2,
TransactionCode,
ISNULL(SUM(CodeChange) OVER (PARTITION BY CardID ORDER BY dt2 ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING),0) AS Grp
FROM Changes),
MinMax AS(
SELECT CardID,
TransactionCode,
CASE TransactionCode WHEN 'IN' THEN MIN(dt2) WHEN 'Out' THEN MAX(dt2) END AS GrpDt2
FROM Groups
GROUP BY CardID,
TransactionCode,
Grp),
--And now original Logic
CTE AS(
SELECT CardID,
GrpDt2 AS DatetimeIn,
LEAD([GrpDt2]) OVER (PARTITION BY CardID ORDER BY GrpDt2) AS DateTimeOut,
TransactionCode
FROM MinMax)
SELECT C.CardID,
CONVERT(date,DatetimeIn) AS DateIn,
CONVERT(time(0),DatetimeIn) AS TimeIn,
CONVERT(date,DatetimeOut) AS DateOtt,
CONVERT(time(0),DatetimeOut) AS TimeOut,
DATEADD(MINUTE, DATEDIFF(MINUTE,DatetimeIn, DateTimeOut), CONVERT(time(0),'00:00:00')) AS Duration
FROM CTE C
WHERE TransactionCode = 'IN';

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.

SQL calculate the day difference between current row and next row based on GroupId

Original table is as below, the table has been ordered by GroupId and Date in ascending order. I'd like to calculate the day difference between current and next row for the same GroupId:
GroupId
Date
1
2022-02-20 00:00:00.000
1
2022-02-27 00:00:00.000
1
2022-03-02 00:00:00.000
2
2022-02-03 00:00:00.000
2
2022-02-17 00:00:00.000
The target output should be like this:
GroupId
Date
Previous_Date
Day_Difference
1
2022-02-20 00:00:00.000
null
null
1
2022-02-27 00:00:00.000
2022-02-20 00:00:00.000
7
1
2022-03-02 00:00:00.000
2022-02-27 00:00:00.000
3
2
2022-02-03 00:00:00.000
null
null
2
2022-02-17 00:00:00.000
2022-02-03 00:00:00.000
14
I got the script as below, but it's getting the Previous_Date from the last row and does the calculation, but I would like to keep the Previous_Date and Day_Difference as NULL for the first row as the target table above.
Can someone please help?
My script is:
SELECT
GroupId,
[Date],
LAG([Date]) OVER (ORDER BY [Date]) AS Previous_Date,
DATEDIFF(DAY, LAG([Date]) OVER (ORDER BY [Date]), [Date]) AS Day_Difference
FROM
TestTable
ORDER BY
GroupId
You can try to use PARTITION BY GroupId in OVER clause. PARTITION BY that divides the query result set into partitions.
SELECT
GroupId,
[Date],
lag([Date]) OVER (PARTITION BY GroupId ORDER BY [Date]) as Previous_Date,
DATEDIFF(day, lag([Date]) OVER (PARTITION BY GroupId ORDER BY [Date]), [Date]) AS Day_Difference
FROM TestTable
order by GroupId
sqlfiddle

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

Select first in and last out time - different date and null condition - from data finger

Here is my data finger table [dbo].[tFPLog]
CardID Date Time TransactionCode
100 2020-09-01 08:00 IN
100 2020-09-01 17:00 OUT
100 2020-09-01 17:10 OUT
200 2020-09-02 02:00 OUT
200 2020-09-02 02:15 OUT
100 2020-09-02 07:00 IN
100 2020-09-02 16:00 OUT
200 2020-09-02 09:55 IN
200 2020-09-02 10:00 IN
Conditions
Assume Employees will be IN and OUT in same day/next day.
Assume There will be multiple IN and OUT for same day/next day for employees. So need first IN and Last Out.
Duration = (FirstInTime - LastOutTime)
The current result I get using the query:
WITH VTE AS(
SELECT *
FROM (VALUES(100,CONVERT(date,'20200901'),CONVERT(time(0),'08:00:00'),'IN'),
(100,CONVERT(date,'20200901'),CONVERT(time(0),'17:00:00'),'OUT'),
(100,CONVERT(date,'20200901'),CONVERT(time(0),'17:10:00'),'OUT'),
(200,CONVERT(date,'20200902'),CONVERT(time(0),'02:00:00'),'OUT'),
(200,CONVERT(date,'20200902'),CONVERT(time(0),'02:15:00'),'OUT'),
(100,CONVERT(date,'20200902'),CONVERT(time(0),'07:00:00'),'IN'),
(100,CONVERT(date,'20200902'),CONVERT(time(0),'16:00:00'),'OUT'),
(200,CONVERT(date,'20200902'),CONVERT(time(0),'09:55:00'),'IN'),
(200,CONVERT(date,'20200902'),CONVERT(time(0),'10:00:00'),'IN'))V(CardID,[Date],[Time],TransactionCode)),
Changes AS(
SELECT CardID,
DATEADD(MINUTE,DATEDIFF(MINUTE, '00:00:00',[time]),CONVERT(datetime2(0),[date])) AS Dt2, --Way easier to work with later
TransactionCode,
CASE TransactionCode WHEN LEAD(TransactionCode) OVER (PARTITION BY CardID ORDER BY [Date],[Time]) THEN 0 ELSE 1 END AS CodeChange
FROM VTE V),
Groups AS(
SELECT CardID,
dt2,
TransactionCode,
ISNULL(SUM(CodeChange) OVER (PARTITION BY CardID ORDER BY dt2 ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING),0) AS Grp
FROM Changes),
MinMax AS(
SELECT CardID,
TransactionCode,
CASE TransactionCode WHEN 'IN' THEN MIN(dt2) WHEN 'Out' THEN MAX(dt2) END AS GrpDt2
FROM Groups
GROUP BY CardID,
TransactionCode,
Grp),
--And now original Logic
CTE AS(
SELECT CardID,
GrpDt2 AS DatetimeIn,
LEAD([GrpDt2]) OVER (PARTITION BY CardID ORDER BY GrpDt2) AS DateTimeOut,
TransactionCode
FROM MinMax)
SELECT C.CardID,
CONVERT(date,DatetimeIn) AS DateIn,
CONVERT(time(0),DatetimeIn) AS TimeIn,
CONVERT(date,DatetimeOut) AS DateOtt,
CONVERT(time(0),DatetimeOut) AS TimeOut,
DATEADD(MINUTE, DATEDIFF(MINUTE,DatetimeIn, DateTimeOut), CONVERT(time(0),'00:00:00')) AS Duration
FROM CTE C
WHERE TransactionCode = 'IN';
=====The Current Result======
CardID DateIN TimeIN DateOUT TimeOUT Duration
100 2020-09-01 08:00 2020-09-01 17:10 09:10
100 2020-09-02 07:00 2020-09-02 16:00 09:00
200 2020-09-02 09:55 NULL NULL NULL
=====The Result Needed======
CardID DateIN TimeIN DateOUT TimeOUT Duration
100 2020-09-01 08:00 2020-09-01 17:10 09:10
100 2020-09-02 07:00 2020-09-02 16:00 09:00
200 NULL NULL 2020-09-02 02:15 NULL
200 2020-09-02 09:55 NULL NULL NULL
How to get the NULL Value For the Date IN and TimeIN? With the condition FIRST IN AND LAST OUT. Please help, thank you in advance.
This is a gaps-and-islands problem. Here is an approach using window functions:
select card_id,
min(case when transaction_code = 'IN' then dt end) dt_in,
max(case when transaction_code = 'OUT' then dt end) dt_out
from (
select t.*,
sum(case when transaction_code = 'IN' and (lag_transaction_code is null or lag_transaction_code <> 'IN') then 1 else 0 end)
over(partition by card_id order by dt) grp
from (
select t.*,
lag(transaction_code) over(partition by card_id order by dt) lag_transaction_code
from (
select t.*, cast(date as datetime) + cast(time as datetime) dt
from vte t
) t
) t
) t
group by card_id, grp
order by card_id, dt_in
The idea is to identify the first "IN"s (using lag() and a window sum()) and to use that to build groups of adjacent records. Then we can use conditional aggregation to retrieve the corresponding bounds each range.
Note that you should not be storing date date and time components in two different columns - this makes things more complicated, for no obvious benefit. I added another level of nesting to generate proper datetimes.
Demo on DB Fiddle:
card_id | dt_in | dt_out
------: | :---------------------- | :----------------------
100 | 2020-09-01 08:00:00.000 | 2020-09-01 17:10:00.000
100 | 2020-09-02 07:00:00.000 | 2020-09-02 16:00:00.000
200 | null | 2020-09-02 02:15:00.000
200 | 2020-09-02 09:55:00.000 | null

TSQL- adding value of every row to the next

I have 2 columns and I want to add the value(hour) of every row to the next row.
Date Hour
2014-01-13 13:00 0
2014-01-13 14:00 3
2014-01-13 16:00 2
and I want to have a new date column that shows like this:
Date Hour **New_Date**
2014-01-13 13:00 0 2014-01-13 16:00
2014-01-13 14:00 3 2014-01-13 17:00
2014-01-13 16:00 2 2014-01-13 18:00
You can use a subquery with DATEADD:
SELECT [Date], [Hour],
DATEADD(hour, (SELECT TOP 1 t2.[Hour]
FROM dbo.Tablename t2
WHERE t2.[Date] > t1.[Date]
ORDER BY t2.[Date]), [Date])
AS [New_Date]
FROM dbo.TableName t1
ORDER BY [Date]
But as you can see in this sql-fiddle the seond row adds two hours since that is the third row's hour. Also, the last row gets no new-date because there is no next-row. Is your desired result wrong?
Try this..
CREATE TABLE #temp
(
[Date] DATETIME,
[Hour] INT
)
INSERT INTO #temp
([Date],[Hour])
VALUES ('2014-01-13 13:00:00',0),
('2014-01-13 14:00:00',3),
('2014-01-13 16:00:00',2)
;WITH cte
AS (SELECT Row_number()
OVER(
ORDER BY [date]) AS id,
*
FROM #temp)
SELECT a.Date,
a.Hour,
Dateadd(hh, a.Hour, CASE
WHEN a.id != 1 THEN a.[date]
ELSE (SELECT TOP 1 date
FROM cte
ORDER BY id DESC)
END) newdate
FROM cte a
LEFT JOIN cte b
ON a.id = b.id + 1
output
Date Hour newdate
2014-01-13 13:00:00.000 0 2014-01-13 16:00:00.000
2014-01-13 14:00:00.000 3 2014-01-13 17:00:00.000
2014-01-13 16:00:00.000 2 2014-01-13 18:00:00.000