SQL服务器的夜班时间如何计算?
How to calculate the night working shift in SQL server?
我正在使用指纹系统记录所有员工在4个班次中的进出日志。正常班次(08:00-> 17:00),班次1:(06-> 14:00), 班次 2: (14:00-> 22:00) 班次 3:(22:00-> 06:00 明天) 。我有 2 个主要 table :
当我使用左连接时:
select e.Id as EmpID,CAST(PunchTime as DATE)CheckDate,MIN(cast(a.PunchTime as Time))[TimeIN], max(cast(a.PunchTime as Time))[Time_OUT]
from Employee e
left join AttLog a
on a.EnrollNumber=e.EnrollNumber
group by e.Id,CAST(PunchTime as DATE)
当我使用内部连接时:
select e.Id as EmpID,CAST(PunchTime as DATE)CheckDate,MIN(cast(a.PunchTime as Time))[TimeIN], max(cast(a.PunchTime as Time))[Time_OUT]
from Employee e
inner join AttLog a
on a.EnrollNumber=e.EnrollNumber
group by e.Id,CAST(PunchTime as DATE)
然后,您会看到在使用左连接时我们得到了所有员工,包括空时间。
当我们使用 inner join 时,如果员工在轮班 3 中工作(22:00 今天到 06:00 明天),我们只会得到 Time IN = Time OUT。
所以我的问题是如何计算 Shift 3 的 IN 和 OUT 时间。
如果员工只打卡 IN 然后 Time OUT = Time IN ,那么在这种情况下如何将时间 OUT 显示为 00:00:00 。
我想这样输出:
EmpID CheckDate TimeIN Time_OUT
5 2015-08-19 2015-08-19 07:51:29.000 2015-08-20 07:43:57.000
14 2015-08-19 2015-08-19 06:52:26.000 2015-08-19 00:00:00.000
EmpID 5 正常工作:08:00->17:00 但他必须上夜班所以必须在公司待到明天08:00。
EmpID 14 在正常班次工作,但她忘记打卡了。
目前上面的数据,输出是这样的:
EmpID CheckDate TimeIN Time_OUT
5 2015-08-19 2015-08-19 07:51:29.000 2015-08-19 07:51:29.000
5 2015-08-20 2015-08-20 07:43:57.000 2015-08-20 07:43:57.000
14 2015-08-19 2015-08-19 06:52:26.000 2015-08-19 06:52:26.000
这是一种对打卡时间进行排序的方法,然后使用递归 CTE 自连接将时间拼接在一起。像这样的系统很难控制错过的拳头等,但我试着向你展示了一种方法,你可以通过添加一个 HoursWorked 阈值来做到这一点。
/* Create some sample Employee punch data to test*/
IF OBJECT_ID('tempdb..#AttLogTest') IS NOT NULL
DROP TABLE #AttLogTest
CREATE TABLE #AttLogTest (EnrollNumber INT NOT NULL, PunchTime DATETIME NOT NULL)
INSERT INTO #AttLogTest (EnrollNumber, PunchTime)
SELECT 10, '2015-08-01 08:01:03' UNION ALL
SELECT 10, '2015-08-02 07:57:35' UNION ALL
SELECT 10, '2015-08-01 16:15:23' UNION ALL
SELECT 10, '2015-08-02 16:17:46' UNION ALL
SELECT 12, '2015-08-01 21:59:31' UNION ALL
SELECT 12, '2015-08-02 05:59:02' UNION ALL
SELECT 12, '2015-08-02 22:02:28' UNION ALL
SELECT 12, '2015-08-03 06:01:24' UNION ALL
SELECT 14, '2015-08-01 07:59:01' UNION ALL
SELECT 14, '2015-08-02 07:58:16' UNION ALL
SELECT 14, '2015-08-02 16:02:48'
/* Employee time query logic below */
/* First, create a temp table that sequences the punch times for each employee */
IF OBJECT_ID('tempdb..#EmployeeTimeSequence') IS NOT NULL
DROP TABLE #EmployeeTimeSequence
SELECT
EnrollNumber
,PunchTime
,PunchSequence = ROW_NUMBER() OVER(PARTITION BY EnrollNumber ORDER BY PunchTime)
INTO #EmployeeTimeSequence
FROM #AttLogTest --Replace this with your dbo.AttLog table if this solution works for you
/*WHERE clause could be added here to filter for specific dates or EnrollNumbers */
/* If time between punches is greater than this threashold, then it will be treated as a missed punch
in logic below. Remove this or modify as needed. */
DECLARE @MissedPunchThreshold int
SET @MissedPunchThreshold = 20
/* Next, create a recursive CTE which will stitch together the punch times and ensure punch times don't overlap when
self-joining to #EmployeeTimeSequence. */
;WITH EmployeeTimeCTE (EnrollNumber, CheckDate, Time_In, Time_Out, HoursBetweenPunch, PunchOutSequence)
AS (
/* Anchor member */
SELECT
ETS_In.EnrollNumber
,CAST(ETS_In.PunchTime AS DATE) AS CheckDate
,ETS_In.PunchTime AS Time_In
,ETS_Out.PunchTime AS Time_Out
,DateDiff(hour, ETS_In.PunchTime, ETS_Out.PunchTime) AS HoursBetweenPunch
,ETS_Out.PunchSequence AS PunchOutSequence
FROM #EmployeeTimeSequence AS ETS_In
LEFT OUTER JOIN #EmployeeTimeSequence AS ETS_Out
ON ETS_In.EnrollNumber = ETS_Out.EnrollNumber
AND ETS_Out.PunchSequence = ETS_In.PunchSequence + 1
WHERE ETS_In.PunchSequence = 1
UNION ALL
/* Recursive memebr - build on top of anchor */
SELECT
ETS_In.EnrollNumber
,CAST(ETS_In.PunchTime AS DATE) AS CheckDate
,ETS_In.PunchTime AS Time_In
,ETS_Out.PunchTime AS Time_Out
,DateDiff(hour, ETS_In.PunchTime, ETS_Out.PunchTime) AS HoursBetweenPunch
,ETS_Out.PunchSequence AS PunchOutSequence
FROM #EmployeeTimeSequence AS ETS_In --get the time for the in punch
INNER JOIN EmployeeTimeCTE ET
ON ET.EnrollNumber = ETS_In.EnrollNumber
AND ETS_In.PunchSequence =
CASE
WHEN ET.HoursBetweenPunch > @MissedPunchThreshold -- if more than threshold, then treat as missed punch
THEN ET.PunchOutSequence -- then treat the previous out punch as the next in punch instead
ELSE ET.PunchOutSequence + 1 -- else join as usual to get the next punch in sequence
END
INNER JOIN #EmployeeTimeSequence AS ETS_Out -- get the time for the out punch
ON ETS_In.EnrollNumber = ETS_Out.EnrollNumber
AND ETS_Out.PunchSequence = ETS_In.PunchSequence + 1
)
/* Now query the CTE */
SELECT
EnrollNumber AS EmpID
,CheckDate
,Time_In
,CASE WHEN HoursBetweenPunch > @MissedPunchThreshold THEN NULL ELSE Time_Out END AS Time_Out
,CASE WHEN HoursBetweenPunch > @MissedPunchThreshold THEN NULL ELSE HoursBetweenPunch END AS HoursBetweenPunch
FROM EmployeeTimeCTE
ORDER BY EnrollNumber, CheckDate
OPTION (MAXRECURSION 1000)
您可以参考以下我经过大量研究得到的解决方案。此解决方案仅适用于 SQL 2012 或更高版本。
;WITH Level1
AS ( SELECT
EmpName (or EmpID)
,TrnName (Exit or Entrance)
,EntryDateTime
,LAG (TrnName, 1, 'N/A') OVER ( PARTITION BY EmpName ORDER BY EntryDateTime) AS LastEvent
,LEAD(TrnName, 1, 'N/A') OVER ( PARTITION BY EmpName ORDER BY EventDateTime ) AS NextEvent
FROM #TempData
),
Level2
AS ( SELECT
EmpName
,TrnName
,EntryDateTime
,LastEvent
,NextEvent
FROM Level1
WHERE
NOT ( TrnName = 'Entrance' AND NextEvent = 'Entrance' )
AND NOT ( TrnName = 'Entrance' AND LastEvent = 'Entrance' )
AND NOT ( TrnName = 'Exit' AND LastEvent = 'Exit' )
AND NOT ( TrnName = 'Entrance' AND NextEvent = 'N/A' )
AND NOT (TrnName = 'Exit' AND LastEvent = 'N/A' )
),
Level3
AS ( SELECT
EmpName
,TrnName
,EntryDateTime
,DATEDIFF(second, EntryDateTime,LEAD(EntryDateTime) OVER ( PARTITION BY EmpName ORDER BY EntryDateTime )) AS Seconds
FROM Level2
)
SELECT
EmpName
,EntryDateTime
,(EntryDateTime+convert(DateTime,TIMEFROMPARTS((Seconds%(3600*24)/3600), ((Seconds%(3600*24)%3600)/60), ((Seconds%(3600*24)%3600)%60),0, 0))) AS ExitDateTime
,Seconds
,TIMEFROMPARTS((Seconds%(3600*24)/3600), ((Seconds%(3600*24)%3600)/60), ((Seconds%(3600*24)%3600)%60),0, 0) AS WorkTime
FROM Level3
WHERE
TrnName = 'Entrance'
这可能会有所帮助,并且可以通过添加由 PeterDNCO 提供的阈值代码来改进。
我正在使用指纹系统记录所有员工在4个班次中的进出日志。正常班次(08:00-> 17:00),班次1:(06-> 14:00), 班次 2: (14:00-> 22:00) 班次 3:(22:00-> 06:00 明天) 。我有 2 个主要 table :
当我使用左连接时:
select e.Id as EmpID,CAST(PunchTime as DATE)CheckDate,MIN(cast(a.PunchTime as Time))[TimeIN], max(cast(a.PunchTime as Time))[Time_OUT]
from Employee e
left join AttLog a
on a.EnrollNumber=e.EnrollNumber
group by e.Id,CAST(PunchTime as DATE)
当我使用内部连接时:
select e.Id as EmpID,CAST(PunchTime as DATE)CheckDate,MIN(cast(a.PunchTime as Time))[TimeIN], max(cast(a.PunchTime as Time))[Time_OUT]
from Employee e
inner join AttLog a
on a.EnrollNumber=e.EnrollNumber
group by e.Id,CAST(PunchTime as DATE)
然后,您会看到在使用左连接时我们得到了所有员工,包括空时间。 当我们使用 inner join 时,如果员工在轮班 3 中工作(22:00 今天到 06:00 明天),我们只会得到 Time IN = Time OUT。 所以我的问题是如何计算 Shift 3 的 IN 和 OUT 时间。 如果员工只打卡 IN 然后 Time OUT = Time IN ,那么在这种情况下如何将时间 OUT 显示为 00:00:00 。 我想这样输出:
EmpID CheckDate TimeIN Time_OUT
5 2015-08-19 2015-08-19 07:51:29.000 2015-08-20 07:43:57.000
14 2015-08-19 2015-08-19 06:52:26.000 2015-08-19 00:00:00.000
EmpID 5 正常工作:08:00->17:00 但他必须上夜班所以必须在公司待到明天08:00。 EmpID 14 在正常班次工作,但她忘记打卡了。 目前上面的数据,输出是这样的:
EmpID CheckDate TimeIN Time_OUT
5 2015-08-19 2015-08-19 07:51:29.000 2015-08-19 07:51:29.000
5 2015-08-20 2015-08-20 07:43:57.000 2015-08-20 07:43:57.000
14 2015-08-19 2015-08-19 06:52:26.000 2015-08-19 06:52:26.000
这是一种对打卡时间进行排序的方法,然后使用递归 CTE 自连接将时间拼接在一起。像这样的系统很难控制错过的拳头等,但我试着向你展示了一种方法,你可以通过添加一个 HoursWorked 阈值来做到这一点。
/* Create some sample Employee punch data to test*/
IF OBJECT_ID('tempdb..#AttLogTest') IS NOT NULL
DROP TABLE #AttLogTest
CREATE TABLE #AttLogTest (EnrollNumber INT NOT NULL, PunchTime DATETIME NOT NULL)
INSERT INTO #AttLogTest (EnrollNumber, PunchTime)
SELECT 10, '2015-08-01 08:01:03' UNION ALL
SELECT 10, '2015-08-02 07:57:35' UNION ALL
SELECT 10, '2015-08-01 16:15:23' UNION ALL
SELECT 10, '2015-08-02 16:17:46' UNION ALL
SELECT 12, '2015-08-01 21:59:31' UNION ALL
SELECT 12, '2015-08-02 05:59:02' UNION ALL
SELECT 12, '2015-08-02 22:02:28' UNION ALL
SELECT 12, '2015-08-03 06:01:24' UNION ALL
SELECT 14, '2015-08-01 07:59:01' UNION ALL
SELECT 14, '2015-08-02 07:58:16' UNION ALL
SELECT 14, '2015-08-02 16:02:48'
/* Employee time query logic below */
/* First, create a temp table that sequences the punch times for each employee */
IF OBJECT_ID('tempdb..#EmployeeTimeSequence') IS NOT NULL
DROP TABLE #EmployeeTimeSequence
SELECT
EnrollNumber
,PunchTime
,PunchSequence = ROW_NUMBER() OVER(PARTITION BY EnrollNumber ORDER BY PunchTime)
INTO #EmployeeTimeSequence
FROM #AttLogTest --Replace this with your dbo.AttLog table if this solution works for you
/*WHERE clause could be added here to filter for specific dates or EnrollNumbers */
/* If time between punches is greater than this threashold, then it will be treated as a missed punch
in logic below. Remove this or modify as needed. */
DECLARE @MissedPunchThreshold int
SET @MissedPunchThreshold = 20
/* Next, create a recursive CTE which will stitch together the punch times and ensure punch times don't overlap when
self-joining to #EmployeeTimeSequence. */
;WITH EmployeeTimeCTE (EnrollNumber, CheckDate, Time_In, Time_Out, HoursBetweenPunch, PunchOutSequence)
AS (
/* Anchor member */
SELECT
ETS_In.EnrollNumber
,CAST(ETS_In.PunchTime AS DATE) AS CheckDate
,ETS_In.PunchTime AS Time_In
,ETS_Out.PunchTime AS Time_Out
,DateDiff(hour, ETS_In.PunchTime, ETS_Out.PunchTime) AS HoursBetweenPunch
,ETS_Out.PunchSequence AS PunchOutSequence
FROM #EmployeeTimeSequence AS ETS_In
LEFT OUTER JOIN #EmployeeTimeSequence AS ETS_Out
ON ETS_In.EnrollNumber = ETS_Out.EnrollNumber
AND ETS_Out.PunchSequence = ETS_In.PunchSequence + 1
WHERE ETS_In.PunchSequence = 1
UNION ALL
/* Recursive memebr - build on top of anchor */
SELECT
ETS_In.EnrollNumber
,CAST(ETS_In.PunchTime AS DATE) AS CheckDate
,ETS_In.PunchTime AS Time_In
,ETS_Out.PunchTime AS Time_Out
,DateDiff(hour, ETS_In.PunchTime, ETS_Out.PunchTime) AS HoursBetweenPunch
,ETS_Out.PunchSequence AS PunchOutSequence
FROM #EmployeeTimeSequence AS ETS_In --get the time for the in punch
INNER JOIN EmployeeTimeCTE ET
ON ET.EnrollNumber = ETS_In.EnrollNumber
AND ETS_In.PunchSequence =
CASE
WHEN ET.HoursBetweenPunch > @MissedPunchThreshold -- if more than threshold, then treat as missed punch
THEN ET.PunchOutSequence -- then treat the previous out punch as the next in punch instead
ELSE ET.PunchOutSequence + 1 -- else join as usual to get the next punch in sequence
END
INNER JOIN #EmployeeTimeSequence AS ETS_Out -- get the time for the out punch
ON ETS_In.EnrollNumber = ETS_Out.EnrollNumber
AND ETS_Out.PunchSequence = ETS_In.PunchSequence + 1
)
/* Now query the CTE */
SELECT
EnrollNumber AS EmpID
,CheckDate
,Time_In
,CASE WHEN HoursBetweenPunch > @MissedPunchThreshold THEN NULL ELSE Time_Out END AS Time_Out
,CASE WHEN HoursBetweenPunch > @MissedPunchThreshold THEN NULL ELSE HoursBetweenPunch END AS HoursBetweenPunch
FROM EmployeeTimeCTE
ORDER BY EnrollNumber, CheckDate
OPTION (MAXRECURSION 1000)
您可以参考以下我经过大量研究得到的解决方案。此解决方案仅适用于 SQL 2012 或更高版本。
;WITH Level1
AS ( SELECT
EmpName (or EmpID)
,TrnName (Exit or Entrance)
,EntryDateTime
,LAG (TrnName, 1, 'N/A') OVER ( PARTITION BY EmpName ORDER BY EntryDateTime) AS LastEvent
,LEAD(TrnName, 1, 'N/A') OVER ( PARTITION BY EmpName ORDER BY EventDateTime ) AS NextEvent
FROM #TempData
),
Level2
AS ( SELECT
EmpName
,TrnName
,EntryDateTime
,LastEvent
,NextEvent
FROM Level1
WHERE
NOT ( TrnName = 'Entrance' AND NextEvent = 'Entrance' )
AND NOT ( TrnName = 'Entrance' AND LastEvent = 'Entrance' )
AND NOT ( TrnName = 'Exit' AND LastEvent = 'Exit' )
AND NOT ( TrnName = 'Entrance' AND NextEvent = 'N/A' )
AND NOT (TrnName = 'Exit' AND LastEvent = 'N/A' )
),
Level3
AS ( SELECT
EmpName
,TrnName
,EntryDateTime
,DATEDIFF(second, EntryDateTime,LEAD(EntryDateTime) OVER ( PARTITION BY EmpName ORDER BY EntryDateTime )) AS Seconds
FROM Level2
)
SELECT
EmpName
,EntryDateTime
,(EntryDateTime+convert(DateTime,TIMEFROMPARTS((Seconds%(3600*24)/3600), ((Seconds%(3600*24)%3600)/60), ((Seconds%(3600*24)%3600)%60),0, 0))) AS ExitDateTime
,Seconds
,TIMEFROMPARTS((Seconds%(3600*24)/3600), ((Seconds%(3600*24)%3600)/60), ((Seconds%(3600*24)%3600)%60),0, 0) AS WorkTime
FROM Level3
WHERE
TrnName = 'Entrance'
这可能会有所帮助,并且可以通过添加由 PeterDNCO 提供的阈值代码来改进。