Related
I have a list of tasks,
for each I have a TaskID, startTime and StopTime as milliseconds from 1-1-1970 and a list of users (#Tasks).
I need to calculate the time spent by each user on the task splitted by day/night time, week or weekend, regular/overtime considering nighttime hours from 10:00 PM to 06:00 AM.
Surely there's a better solution but so far I got this:
IF OBJECT_ID('tempdb..#Tasks') IS NULL
BEGIN
create table #Tasks
(
TaskID nvarchar(50),
DateStart bigint,
DateStop bigint,
Staff nvarchar(100)
)
insert into #Tasks values
('C001',1554181200000,1554190200000,'john,jack'),
('C002',1554202800000,1554212700000,'tom,john'),
('C003',1554228000000,1554246900000,'john,franck'),
('C004',1554613200000,1554626700000,'john')
END
GO
declare
#UserName nvarchar(50)='john',
#DateFrom datetime='2019-04-01',
#DateTo datetime='2019-04-30',
#nStart time='06:00:00',
#nStop time='22:00:00'
select
startday as [Day],
sum([WeekDay]) as [WeekDay],
sum([WeekNight]) as [WeekNight],
sum([WeekendDay]) as [WeekendDay],
sum([WeekendNight]) as [WeekendNight],
sum([TotalMinutes]) as [TotalMinutes],
0 WeekDayOverTime,
0 WeekNightOvertime,
0 WeekendDayOvertime,
0 WeekendNightOvertime,
[UserName]
,timeframe
from
(
select
iif(isWeekend=1,NightMinutes,0) WeekendNight,
iif(isWeekend=0,NightMinutes,0) WeekNight,
iif(isWeekend=1,DayMinutes,0) WeekendDay,
iif(isWeekend=0,DayMinutes,0) [WeekDay],
TotalMinutes,
username,
startday,
timeframe
from
(
select
iif(Before6>0,Before6,0)+ iif(After22>0,After22,0) NightMinutes,
TotalMinutes-iif(Before6>0,Before6,0)- iif(After22>0,After22,0) DayMinutes,
TotalMinutes,
startday,
isWeekend,
UserName,
timeframe
from
(
Select
(t.datestop-t.datestart)/60000 TotalMinutes,
datediff(n,convert(time,DATEADD(SECOND,t.DateStart/1000,'1970-01-01')),#nStart) Before6,
datediff(n,#nStop,convert(time,DATEADD(SECOND,t.DateStop/1000,'1970-01-01'))) After22,
iif((((DATEPART(DW, convert(datetime,DATEADD(SECOND,t.DateStart/1000,'1970-01-01'))) - 1 ) + ##DATEFIRST ) % 7) IN (0,6),1,0) isWeekend,
convert(varchar(10),DATEADD(SECOND,t.DateStart/1000,'1970-01-01'),126) startday,
STUFF(( SELECT distinct ' ' + convert(varchar(5),DATEADD(SECOND,t.DateStart/1000,'1970-01-01'),108)+'-'+convert(varchar(5),DATEADD(SECOND,t.DateStop/1000,'1970-01-01'),108) AS [text()]
FROM #Tasks tt
--WHERE tt.taskID=t.TaskID
FOR XML PATH('') ), 1, 1, '' ) AS [timeframe],
#UserName UserName
FROM #Tasks t
WHERE t.Staff like '%'+#UserName+'%'
and DATEADD(SECOND,t.DateStart/1000,'1970-01-01') between #DateFrom and #DateTo
) z
) zz
) zzz
group by startday,username,timeframe
order by startday
I need now to :
1) group result by day, summing up WeekDay,WeekNight,WeekendDay,WeekendNight and TotalMinutes, and concatenating timeframe so to have for example on 2nd April "05:00-07:30|11:00-13:45|18:00-23:00"
2) Not sum up time between 12:00 and 12:30 (if applicable) since it is lunch time
3) considering that after 8 hours daily it has to be calculated as overtime, I have to split the total minutes between in time and overtime, but depending if overtime is by daytime or nighttime or on the weekend
4) eventually using a holiday table
in other words we should have this:
Day TotalMinutes WeekDay WeekNight WeekendDay WeekendNight WeekDayOverTime WeekNightOvertime WeekendDayOvertime WeekendNightOvertime UserName timeframe
02/04/2019 630 420 60 0 0 45 75 0 0 john 05:00-07:30|11:00-13:45|18:00-23:00
07/04/2019 225 0 0 165 60 0 0 0 0 john 05:00-08:45
because (on 2nd April) we have:
First Task:
60 minutes of Regular NightTime
90 minutes of Regular DayTime
Second Task:
165 minutes of Regular DayTime, but have to count only 135 due to lunch time
Third Task:
240 DayTime
75 NightTime
but since with Task 1 and 2 we sum up 285 minutes, only the first 185 Minutes of Third task are Regular DayTime: the remaining 45 are Overtime DayTime, and the following 75 of NightTime are actually OvertimeNightTime
In this approach the first CTE (properDates) get the Start and Stop Datetimes, then you don't need to repeat that formula over the query.
The second CTE(splittedMinutes) is to get the same data you get in your current approach, except for the first CROSS APPLY, which is splitting the timeframes crossing with lunch time. The second CROSS APPLY gets the number of minutes and isWeekend value.
In the third CTE(qualifiedMinutes) I am using a window partition to get the accumulated minutes and generate the overtimes when applies.
At the end I used a selective SUM to separate weekdays and weekends in the aggregates
;with properDates AS (
SELECT TaskID, DATEADD(SECOND,t.DateStart/1000,'1970-01-01') as DateStart,DATEADD(SECOND,t.DateStop/1000,'1970-01-01') as DateStop, Staff
FROM #Tasks t
WHERE Staff LIKE '%' + #UserName + '%'
), splittedMinutes AS (
select
CAST(p.DateStart AS DATE) as [Day],
TotalMinutes,
SUM(TotalMinutes) OVER (PARTITION BY CAST(p.DateStart AS DATE) ORDER BY b.start) AS cumulate,
TotalMinutes - EarlyMinutes - LateMinutes as DayTime,
EarlyMinutes + LateMinutes as NightTime,
isWeekend,
CONVERT(VARCHAR(5),b.Start,108) + '-' + CONVERT(VARCHAR(5),b.Stop,108) as [timeframe]
from properdates p
cross apply (
select CAST(p.DateStart As TIME) AS Start, #bStart as Stop WHERE CAST(p.DateStart AS TIME) < #bStart and CAST(p.DateStop AS TIME) > #bStart
union
select #bStop as Start, CAST(DateStop AS TIME) AS Stop WHERE CAST(p.DateStop AS TIME) > #bStop and CAST(p.DateStart AS TIME) < #bStop
union
select CAST(p.DateStart AS TIME) AS Start, CAST(p.DateStop AS TIME) AS Stop WHERE NOT (CAST(p.DateStart AS TIME) < #bStart and CAST(p.DateStop AS TIME) > #bStart) AND NOT (CAST(p.DateStop AS TIME) > #bStop and CAST(p.DateStart AS TIME) < #bStop)
) b
cross apply (
select
DATEDIFF(Minute, b.Start, b.Stop) as TotalMinutes,
(DATEDIFF(Minute, CAST(b.Start AS TIME), #nStart) + ABS(DATEDIFF(Minute, CAST(b.Start AS TIME), #nStart))) / 2 as EarlyMinutes,
(DATEDIFF(Minute, #nStop, CAST(b.Stop AS TIME)) + ABS(DATEDIFF(Minute, #nStop, CAST(b.Stop AS TIME)))) / 2 as LateMinutes,
CASE WHEN DATEPART(DW, p.DateStart) IN (1,7) THEN 1 ELSE 0 END AS isWeekend
) c
), qualifiedMinutes As (
SELECT Day, TotalMinutes, RegularDay, RegularNight, OvertimeDay, OvertimeNight, isWeekend, timeframe
FROM splittedMinutes
OUTER APPLY (
SELECT RegularDay = CASE WHEN cumulate <= #maxTime THEN DayTime WHEN DayTime - (cumulate - TotalMinutes - #maxTime) > 0 THEN ABS(cumulate - TotalMinutes - #maxTime) ELSE 0 END
) RD
OUTER APPLY (
SELECT OvertimeDay = DayTime - RegularDay
) OWD
OUTER APPLY (
SELECT RegularNight = CASE WHEN cumulate <= #maxTime THEN NightTime WHEN (cumulate - TotalMinutes - #maxTime + RegularDay) < 0 THEN NightTime + (cumulate - TotalMinutes - #maxTime + RegularDay) ELSE 0 END
) RWN
OUTER APPLY (
SELECT OvertimeNight = NightTime - RegularNight
) OWN
)
SELECT
Day,
#UserName and UserName,
SUM(TotalMinutes) AS TotalMinutes,
SUM(CASE WHEN isWeekend = 0 THEN RegularDay ELSE 0 END) AS WeekDay,
SUM(CASE WHEN isWeekend = 0 THEN RegularNight ELSE 0 END) AS WeekNight,
SUM(CASE WHEN isWeekend = 1 THEN RegularDay ELSE 0 END) AS WeekendDay,
SUM(CASE WHEN isWeekend = 1 THEN RegularNight ELSE 0 END) AS WeekendNight,
SUM(CASE WHEN isWeekend = 0 THEN OvertimeDay ELSE 0 END) AS WeekDayOverTime,
SUM(CASE WHEN isWeekend = 0 THEN OvertimeNight ELSE 0 END) AS WeekNightOvertime,
SUM(CASE WHEN isWeekend = 1 THEN OvertimeDay ELSE 0 END) AS WeekendDayOverTime,
SUM(CASE WHEN isWeekend = 1 THEN OvertimeNight ELSE 0 END) AS WeekendNightOvertime,
STUFF((SELECT '|' + timeframe FROM qualifiedMinutes tt WHERE tt.Day = q.Day ORDER BY timeframe FOR XML PATH('') ), 1, 1, '' ) AS [timeframe]
FROM qualifiedMinutes q
GROUP BY Day
So I am working on creating a payroll import file for our accounting department and I am having trouble calculating overtime properly in my report.
Here is my query
declare #week1start date
declare #week1end date
declare #week2start date
declare #week2end date
--Week 1 Start and End
set #week1start = CAST('10/15/2018' as date)
set #week1end = CAST('10/21/2018' as date)
--Week 2 Start and End
set #week2start = CAST('10/22/2018' as date)
set #week2end = CAST('10/28/2018' as date)
--Week 1 Hours Worked
(select Employee.EMPLOYEE_NUM as [Employee Num],Employee.NAME as [Employee Name],Time_Clk.StoreNumber as [Store],
[Week 1 Hours Worked]= case
when (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) - 40 > 0
then 40
else (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2))))
end,
[Week 1 OT Hours] = case
when (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) - 40 > 0
then (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) -40
else 0
end,
0 as [Week 2 Hours Worked],
0 as [Week 2 OT Hours]
from Time_Clk
join Employee on Time_Clk.EMPLOYEE_ID = Employee.EMPLOYEE_NUM
where CAST(out_time as DATE) >= #week1start and CAST(out_time as date) <= #week1end and Time_Clk.OUT_TYPE = 1
group by Employee.EMPLOYEE_NUM,Employee.NAME,Time_Clk.StoreNumber)
union all
--Week 2 hours worked
(select Employee.EMPLOYEE_NUM as [Employee Num],Employee.NAME as [Employee Name],Time_Clk.StoreNumber as [Store],
0 as [Week 1 Hours Worked],
0 as [Week 1 OT Hours],
[Week 2 Hours Worked] = case
when (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) - 40 > 0
then 40
else (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2))))
end,
[Week 2 OT Hours] = case
when (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) - 40 > 0
then (sum(cast(cast(DATEDIFF(MINUTE,IN_TIME,OUT_TIME) as decimal (10,2))/60 as decimal (10,2)))) -40
else 0
end
from Time_Clk
join Employee on Time_Clk.EMPLOYEE_ID = Employee.EMPLOYEE_NUM
where CAST(out_time as DATE) >= #week2start and CAST(out_time as date) <= #week2end and Time_Clk.OUT_TYPE = 1
group by Employee.EMPLOYEE_NUM,Employee.NAME,Time_Clk.StoreNumber)
order by [Employee Num]
So most of my output is fine but here is a select output for a particular user
EmpNum EmpName Store Week 1 Hrs Week 1 OT Week 2 Hours Week 2 OT
1 Name 1 0.00 0.00 40.00 0.88
1 Name 1 39.20 0.00 0.00 0.00
1 Name 2 5.23 0.00 0.00 0.00
So in the problem that I am facing is that Week 1 technically has overtime hours because they worked more than 40 hours that week, but at two different locations. Ideally the result would look like
EmpNum EmpName Store Week 1 Hrs Week 1 OT Week 2 Hours Week 2 OT
1 Name 1 0.00 0.00 40.00 0.88
1 Name 1 39.20 0.00 0.00 0.00
1 Name 2 0.80 4.43 0.00 0.00
I considered doing a case statement but to determine that the person had a total over 40 hours between all 3 lines in week 1, but I dont believe I can do that with the way the data is.
Hopefully all of the formatting comes through ok, I am new to posting on these forums.
Try using only start and end date for the total period all weeks.
If given start date is always period begin date you can compute week number and group hours by resulting week number. You can use datepart or just mod 7 (%) if you use some fiscal calendar.
Once you have that in a vertical form you can do some pivoting magic to get the result you are looking for.
maybe something like this...
if object_id('employee') is not null drop table employee
if object_id('timeclock') is not null drop table timeclock
Go
CREATE TABLE employee (employeeid int identity(1,1), name varchar(50)) ;
GO
CREATE TABLE timeclock (id int identity(1,1), employeeid int, IN_TIME datetime, OUT_TIME datetime);
Go
insert into employee
(name)
values (CHAR(ABS(CHECKSUM(NEWID()))%26+65))
GO 5
;WITH [days] AS
(
SELECT 1 AS [day]
UNION ALL
SELECT [day]+1
FROM [days]
WHERE [day] < 20
)
INSERT INTO timeclock
Select employeeid, GetDate()+[day], Dateadd(hh,8, GETDATE()+[day])
from employee
CROSS APPLY [days]
declare #start datetime = '11/12/2018';
declare #end datetime = #start + 15
;with aggregatedata
as
(
select
employee.employeeid
, employee.name
,weeknum = ROW_NUMBER() over (partition by employee.employeeid order by employee.employeeid, DATEPART(week, In_time) )
,[hours] = sum(DATEDIFF(Minute,IN_TIME,OUT_TIME))
from employee
inner join timeclock on employee.employeeid = timeclock.employeeid
WHERE
in_time > #start and OUT_TIME < #end
group by employee.employeeid, employee.name, DATEPART(week, In_time)
)
select
employeeid, [name], [1], [2], [3], [4]
from aggregatedata
PIVOT
(
sum([hours])
for weeknum in ([1], [2], [3], [4])
) as pvt
If you remove Time_Clk.StoreNumber from your SELECT and from the GROUP BY, then you would get the results you are after. If accounting is using this to determine what accounts to charge to, then naturally you have to keep it in. In that case, you may want to use a window function.
sum(<your statement>) over (partition by EmpNum)
But this would place the value in both store 1 and store 2 row.
Sample data would really make this easier.
I'm writing a function in SQL Server 2012 that will need to know the number of 3 specific days of the month that have passed since a given date. I can do this with a while loop, but its slow and I was looking for a better way.
Here is what I have so far:
Let's assume that GETDATE() = '11/14/2016' and #productDate = '10/1/2016'
--Get the number of "units" that have passed since the date on the label
DECLARE #unitCount INT = 0;
DECLARE #countingDate DATE
SET #countingDate = DATEADD(DAY,1,#productDate);--add 1 to prevent counting the date on the label as the first unit
WHILE (#countingDate < CAST(GETDATE() As date ))
BEGIN
SELECT #unitCount = #unitCount +
CASE
WHEN DAY(#countingDate) = 1 OR DAY(#countingDate) = 10 OR DAY(#countingDate) = 20 THEN 1
ELSE 0
END
SET #countingDate = DATEADD(DAY,1,#countingDate);
END
This will result in #unitCount = 4
GETDATE() of '11/20/2016' would result in #unitCount = 5
Without using a numbers table
create function dbo.fn_DateCounter
(
#datefrom date,
#dateto date
)
returns int
as
begin
return
-- number of complete months
3 *
(
(DATEPART(YYYY, #dateto) * 12 + DATEPART(MM, #dateto))
-(DATEPART(YYYY, #datefrom) * 12 + DATEPART(MM, #datefrom))
- 1
)
-- add on the extras from the first month
+ case when DATEPART(DD, #datefrom) < 10 then 2
when DATEPART(DD, #datefrom) < 20 then 1
else 0
end
-- add on the extras from the last month
+ case when DATEPART(DD, #dateto) > 20 then 3
when DATEPART(DD, #dateto) > 10 then 2
else 1
end
end
go
select dbo.fn_DateCounter('01-jan-2000','01-jan-2000') -- 0
select dbo.fn_DateCounter('01-jan-2000','10-jan-2000') -- 0
select dbo.fn_DateCounter('01-jan-2000','11-jan-2000') -- 1
select dbo.fn_DateCounter('01-jan-2000','20-jan-2000') -- 1
select dbo.fn_DateCounter('01-jan-2000','21-jan-2000') -- 2
select dbo.fn_DateCounter('11-jan-2000','21-jan-2000') -- 1
select dbo.fn_DateCounter('11-jan-2000','21-feb-2000') -- 4
select dbo.fn_DateCounter('01-jan-2000','01-jan-2001') -- 36
select dbo.fn_DateCounter('01-jan-2000','11-jan-2001') -- 37
You can use a combination of sum, case, and the dbo.spt_values table:
declare #productDate datetime = '11/01/2016',
#unitCount int
;with nums as ( -- use a CTE to build a number list
select top 1000 number from master..spt_values
)
select #unitCount = sum(
case when day(dateadd(day, n, #productDate)) in (1, 10, 20)
then 1 else 0 end
) -- add 1 for each 1,10,20 we find
from (
select n = row_number() over (order by nums.number)
from nums cross join nums as num -- 1000*1000 = 1 million rows
) n
where dateadd(day, n, #productDate) < getdate()
select #unitCount
This will grab each date between #productDate and getdate(). The case statement will select 1 for each 1/10/20, and 0 for every other date. Finally, we take the sum of the result.
For 11/1 - 11/11, it returns 1.
For 1/1 - 11/11, the result is 31.
EDIT: In the CTE (with nums as...), we select 1-1000, and then we do a cross join which gives us a million records to work with. The answer is still limited, but now you can go ~2700 years with this.
I have a SQL Server table (tbl) with 2 columns: ts (timestamp) and val (value). I want to make a selection that gives back four columns: first is the day, the second is the average of the values from 10pm the preceding day till 6am, the third row is the average of the values from 6am till 2pm, and the fourth row contains the average of the stored values from 2pm till 10pm. So averages for 8-hour periods in a day which instead of midnight starts at the previous day at 10pm.
This is the query I have so far: http://sqlfiddle.com/#!3/41334/2
I have the average for the whole 24-hour periods (from 10pm), but now I'm stuck. I was thinking that I could make 3 selections for the 8 hours periods and then join them on the day, but I don't know how or if at all I'm on the right track. Please help.
The result I would like to get using my example data:
DAY | AVG_NITE | AVG_MORN | AVG_AFTN
2014.12.07 | 3.75 | 5.6667 | 4.5714
2014.12.08 | 4.6 | 5.6 | 5.4
2014.12.09 | 5.5 | (null) | (null)
the code below produces desired ouput. it uses CTEs but you can change them to subqueries or Views.
WITH tbl2 AS (
SELECT DATEADD(hour, 2, ts) AS ts2
,val
FROM tbl
)
, tbl_hours AS (
SELECT convert(varchar, ts2,102) AS [day]
,ROUND(DATEDIFF(hour, cast(ts2 AS DATE), ts2)/8,0) AS period
,val
FROM tbl2
)
SELECT
[day]
,AVG( case when period = 0 then val else null end) AS [avg_nite]
,AVG( case when period = 1 then val else null end) AS [avg_morn]
,AVG( case when period = 2 then val else null end) AS [avg_aftn]
FROM tbl_hours
GROUP BY [day]
select convert(varchar, dateadd(hour,2,ts), 102) as day,
avg(val) as avg_fullday
,avg(case when dateadd(day, -1, datepart(hour, ts)) in (22,23)
or DATEPART(hour, ts) in (0,1,2,3,4,5)
then (val) end)'Nite'
,avg(case when DATEPART(hour, ts) in( 6,7,8,9,10,11,12,13)
then (val) end) 'Morn'
,avg(case when DATEPART(hour, ts) in (14,15,16,17,18,19,20,21) then (val) end) 'Aftn'
from #tbl
group by convert(varchar, dateadd(hour,2,ts), 102);
drop table #tbl
I have a data from database Like this :
HDiffrence MDiffrence Interv
2 14 2 Hours 14 Minutes
0 4 0 Hours 4 Minutes
so i need to convert both of H and M into time format and I do some logic thing in other query
I do some check like this, here's to check if HH:MM is more than 15 minutes:
count((select(case when((select convert(time,(HDiffrence +':'+MDiffrence),114)) > ((select convert(time,('00' +':'+'15'),114))) )then 1 when ((select convert(time,(HDiffrence +':'+MDiffrence),114)) is null) then null else 0 end)))
and I put the check into :
select contractor , COUNT(pm.PantauID) as total ,
count((select(case when((select convert(time,(HDiffrence +':'+MDiffrence),114)) > ((select convert(time,('00' +':'+'15'),114))) )then 1 when ((select convert(time,(HDiffrence +':'+MDiffrence),114)) is null) then null else 0 end)))
from Pantau p
left join PantauMSG pm
on p.PantauID = pm.PantauID
where PantauType = 'PT2' and PantauStatus <> 'PS1' and
(CAST(SCH_DATE AS DATE) = (SELECT CONVERT (DATE, GETDATE(), 103) AS Expr1))
group by CONTRACTOR
but yes, absoulutely get error :
Cannot perform an aggregate function on an expression containing an aggregate or a subquery.
so based on my logic to check the time , is it other way more simplified to count value more than 15 minutes
If you're just working with integer values for Hours and Minutes, just use basic maths instead of converting it to a Time value:
declare #hours int = 1
declare #mins int = 35
declare #total_mins int = (#hours * 60) + #mins
select #total_mins - 15
Then use it something like this:
select contractor, COUNT(pm.PantauID) as total, sum(HDiffrence) as Hours,
sum(MDiffrence) as Minutes, sum(HDiffrence) * 60 + sum(MDiffrence) as TotalMinutes
from Pantau p
left join PantauMSG pm
on p.PantauID = pm.PantauID
where PantauType = 'PT2' and PantauStatus <> 'PS1' and
(CAST(SCH_DATE AS DATE) = (SELECT CONVERT (DATE, GETDATE(), 103) AS Expr1))
group by CONTRACTOR
Okay based on Tanner Idea I change my query and yes, finally it works:
select contractor , COUNT(pm.PantauID) as total ,
count(case when ((convert(int, HDiffrence)* 60 )+convert(int,MDiffrence)) > 15 then ((convert(int, HDiffrence)* 60 )+convert(int,MDiffrence)) else null end) as Morethan15,
count(case when ((convert(int, HDiffrence)* 60 )+convert(int,MDiffrence)) < 15 then ((convert(int, HDiffrence)* 60 )+convert(int,MDiffrence)) else null end) as lessthan15
from Pantau p
left join PantauMSG pm
on p.PantauID = pm.PantauID
where PantauType = 'PT2' and PantauStatus <> 'PS1' and
(CAST(SCH_DATE AS DATE) = (SELECT CONVERT (DATE, GETDATE(), 103) AS Expr1))
group by CONTRACTOR