SQL Server - Calculate time between dates with work shifts - sql-server

Problem: Calculate the amount of time between two dates with only using the shift dates. The shift dates are the only allowed time to count time. So in the data example below, I'd want to see the time difference for each of the #Rejections records independently. Ideally, I'd like to also take into account the lunch break but I don't if that makes this even harder to handle.
Data:
CREATE TABLE #Shifts
(
ShiftId INT
,StartTime DECIMAL(6,2)
,EndTime DECIMAL(6,2)
,LunchStart DECIMAL(6,2)
,LunchEnd DECIMAL(6,2)
);
INSERT INTO #Shifts VALUES (1, 6.00, 16.75, 11.75, 12.50) /*6am to 4:45pm*/
INSERT INTO #Shifts VALUES (2, 17.00, 3.75, 23.00, 23.75) /*5pm to 3:45am (next day)*/
INSERT INTO #Shifts VALUES (3, 5.00, 17.75, 12.00, 12.75) /*5am to 5:45pm*/
CREATE TABLE #Rejections
(
JobId INT
,WeldId INT
,IndicationNum INT
,FirstRejectedDate DATETIME
,LastAcceptedDate DATETIME
);
INSERT INTO #Rejections VALUES (500, 700, 2, '2017-01-03 22:35:31.000', '2017-01-04 01:38:16.000')
INSERT INTO #Rejections VALUES (500, 701, 3, '2017-01-04 01:48:55.000', '2017-01-06 09:21:11.000')
I am seeking some assistance on how to solve this problem. I am a novice at SQL Server and this problem has me totally stumped. I don't even know where to begin. I am using SQL Server 2008 R2 if that helps for available commands. Can someone please help me figure out how to achieve this?

It's really better to use datetime2 (not datetime as suggested in comments) if possible - though I understand that in your situation that it's not. If you do then the answer is simple & easy to read...
SELECT DATEDIFF(mi, StartTime, EndTime) - DATEDIFF(mi, LunchStart, LunchEnd)
FROM #shifts
I know it's not applicable to you, but I wanted to show it for others, in comparison to the monstrous code that follows. It's pretty ugly but possible to do these calculations in decimal format. This code will give you length of lunch break (minutes), total minutes of shift, and then that total broken down into hours & minutes, so you can use whatever you want.
SELECT (CASE WHEN LunchEnd < LunchStart THEN 24 ELSE 0 END +
LunchEnd - LunchStart) * 60 AS LunchMins
, (CASE WHEN EndTime < StartTime THEN 24 ELSE 0 END +
EndTime
- StartTime
- (CASE WHEN LunchEnd < LunchStart THEN 24 ELSE 0 END + LunchEnd - LunchStart))
* 60 AS TotalShiftMins
, ROUND((CASE WHEN EndTime < StartTime THEN 24 ELSE 0 END +
EndTime
- StartTime
- (CASE WHEN LunchEnd < LunchStart THEN 24 ELSE 0 END + LunchEnd - LunchStart))
,0) AS TotalShiftHoursOnly
, ((CASE WHEN EndTime < StartTime THEN 24 ELSE 0 END +
EndTime
- StartTime
- (CASE WHEN LunchEnd < LunchStart THEN 24 ELSE 0 END + LunchEnd - LunchStart))
* 60) % 60 AS TotalShiftMinsOnly
, *
FROM #Shifts
PS I'm presuming you meant the #Shifts table and not the #Rejections table? The data in the #Rejections table doesn't appear to match with what you're asking.

Related

Summing Times as Durations

I am trying to sum a time field stored in my database as an nvarchar like 'hh:mm', in minutes, however, the following does not work (likely because time cannot handle anything over '23:59:59'):
SELECT
SUM(DATEDIFF(MINUTE, '00:00:00', MyTimeField)) AS TotalMinutes
FROM MyTable
Here's my SQL fiddle on this. Any help appreciated
24:00 is not valid, and if you try 00:00, you have already entered the next day - but because you are not considering a full date when using datediff, that cannot happen. Because if you mention 00:00, then that points at the beginning of the current day rather than the next.
Consider storing time as datetime rather than varchar - and store the clean up times as full time stamp rather than just stating the hour.
Here's the solution:
CREATE TABLE MyTable
([MyTimeField] nvarchar(5))
;
INSERT INTO MyTable
([MyTimeField])
VALUES
('00:15'),
('00:30'),
('01:45'),
('23:15'),
('23:59'),
('24:00')
;
WITH IntMinutes AS
(
SELECT
CASE
WHEN RIGHT('0000' + MyTimeField, 5) < '24:00' THEN DATEDIFF(MINUTE, '00:00:00', MyTimeField)
ELSE
DATEDIFF(MINUTE, '00:00:00', '00:' + RIGHT(MyTimeField, 2)) +
DATEDIFF(MINUTE, '00:00:00', CAST((LEFT(MyTimeField, 2) - 24) AS VARCHAR(2)) + ':00') +
DATEDIFF(MINUTE, '00:00:00', '23:59') + 1
END AS Minutes
FROM MyTable
)
SELECT SUM(Minutes) AS TotalMinutes
FROM IntMinutes

Calculating Early and Late Dates for Performance Measurement

I am trying to calculate shipping performance in SSMS- Im getting stuck in several areas and I hope i can get some help!
I have an Estimated Ship Date, an Appt Date, and an Actual Ship date.
Im measuring warehouse performance, so in most cases shipments leave same day. But in others, they may leave a few days early or late.
The problem that i am having is the correct output. I want to show the Values in DD:HH:MM, but the syntax for DateDiff doesn't give me an accurate day to use:
For Example, a shipment was supposed to leave on 6/3/2019 # at 8 am, but didnt leave the warehouse until 7/22/2019 # 6:30 AM. In this case, DateDiff calcs 49 days, when really its 48 days 22 hours and 30 minutes late. Here is an example of some of the syntax i am using:
EstimatedShipDate datetime,
AppointmentShipDate datetime,
ActualShipDate datetime
);
insert into #test values ('2019-07-01 11:00', '2019-07-01 11:00','2019-06-30 10:30');
insert into #test values ('2019-07-08 13:45', null,'2019-07-01 22:00');
insert into #test values ('2019-07-09 15:00', null,'2019-07-10 15:00');
insert into #test values ('2019-07-03 15:00', null,'2019-07-04 15:00');
insert into #test values ('2019-07-08 15:00', null,'2019-07-08 15:00');
insert into #test values ('2019-07-08 15:00', null,'2019-07-08 22:00');
insert into #test values ('2019-07-03 08:00', null,'2019-07-04 15:00');
insert into #test values ('2019-07-03 08:00', null,'2019-07-03 06:30');
insert into #test values ('2019-06-03 08:00', null,'2019-07-22 06:30');
insert into #test values ('2019-07-01 11:00', null,'2019-06-29 10:30');
Select
EstimatedShipDate,
AppointmentShipDate,
ActualShipDate,
DATEDIFF(DAY,ISNULL(CAST(AppointmentShipDate as DateTime),CAST(EstimatedShipDate as DateTime)), CAST(ActualShipDate as DateTime)) as Days,
DATEPART(DAY, ISNULL(CAST(AppointmentShipDate as DateTime),CAST(EstimatedShipDate as DateTime))-CAST(ActualShipDate as DateTime) ) as days2,
DATEDIFF(Hour,ISNULL(CAST(AppointmentShipDate as DateTime),CAST(EstimatedShipDate as DateTime)), CAST(ActualShipDate as DateTime)) as Hours,
convert(varchar, CAST(ActualShipDate as DateTime)-ISNULL(CAST(AppointmentShipDate as DateTime),CAST(EstimatedShipDate as DateTime)),108) as DateSubtract_Convert,
convert(varchar,ISNULL(CAST(AppointmentShipDate as DateTime),CAST(EstimatedShipDate as DateTime)-CAST(ActualShipDate as DateTime) ),108) as DateSubtract_ConvertEarly
from #TEST
In this case, DateDiff calcs 49 days, when really its 48 days 22 hours and 30 minutes late. Or vice versa, I have a date range that shows 2 days 23:20 but it should be 1 day 23:20
One way is to increase granularity of DATEDIFF to MINUTE. That will require calculation for the days, hours, and minutes using the minutes (division and mod).
SELECT ABS(dT.diff_Minutes / 1440) AS [Days] --days, there are 1440 minutes in a day
,ABS((dT.diff_Minutes % 1440) / 60) AS [Hours] --hours remaining in the day
,ABS((dT.diff_Minutes % 1440) % 60) AS [Minutes] --minutes remaining in the day
FROM (
SELECT DATEDIFF(MINUTE, ISNULL(AppointmentShipDate, EstimatedShipDate), ActualShipDate) [diff_Minutes]
FROM #test
) AS dT
Your temp table produces output:
Days Hours Minutes
1 0 30
6 15 45
1 0 0
1 0 0
0 0 0
0 7 0
1 7 0
0 1 30
48 22 30
2 0 30
To put these in DD:HH:MM format is more complicated, but you can cast the numbers to varchar and concatenate as strings. RIGHT is used to add any leading zeroes.
SELECT CASE WHEN dT.diff_Minutes < 0 THEN '- ' ELSE '+ ' END --positive or negative
+ RIGHT('00' + CAST(ABS(dT.diff_Minutes / 1440) as varchar(1000)), 2)
+ ':'
+RIGHT('00' + CAST((ABS(dT.diff_Minutes % 1440) / 60) as varchar(2)), 2)
+ ':'
+RIGHT('00' + CAST((ABS(dT.diff_Minutes % 1440) % 60) as varchar(2)), 2)
AS [DD:HH:MM]
FROM (
SELECT DATEDIFF(MINUTE, ISNULL(AppointmentShipDate, EstimatedShipDate), ActualShipDate) [diff_Minutes]
FROM #test
) AS dT
Produces output:
DD:HH:MM
- 01:00:30
- 06:15:45
+ 01:00:00
+ 01:00:00
+ 00:00:00
+ 00:07:00
+ 01:07:00
- 00:01:30
+ 48:22:30
- 02:00:30
Use seconds instead of days and then do a little math with seconds, minutes and hours.
Here's a simple SSMS example:
DECLARE
#ScheduledDate DATETIME = '6/3/2019 08:00:00',
#ShippedDate DATETIME = '07/22/2019 06:30:00';
SELECT
CONVERT( VARCHAR, DATEDIFF( s, #ScheduledDate, #ShippedDate ) /60/60/24 ) + ' Days and '
+ CONVERT( VARCHAR, ( #ShippedDate - #ScheduledDate ), 108 ) + ' Hours.';
Returns
48 Days and 22:30:00 Hours.

Difference of dates in Where Clause

I am using SQL server. I'm trying to ad a condition that gives me the rows where the difference is date is between 0-3 or if something was opened 0-3 days after the migration date.
When I added the condition to the WHERE clause it acting funcky. I need help figuring out the best way to do this
This give me a result where the date diff is less than 0 even though I say >= 0
select * from table_1
where datediff(day, a.[opened date], d.[UserMigratedDate]) >= 0
This give me a result where the date diff is greater than 4 even though I say < 4
select * from table_1
where datediff(day, a.[opened date], d.[UserMigratedDate]) < 4
When I use a between it does noting. Am I doing this wrong?
select * from table_1
where (datediff(day, a.[opened date], d.[UserMigratedDate]) >= 0 and datediff(day, a.[opened date], d.[UserMigratedDate]) < 4)
I would like to see your test data that's causing this... because the where clause is constructed correctly. Take the below case for example. ID 2-5 will be returned. Also, your first datediff() where you are seeing if the days are >=0 doesn't make sense to be unless someone could migrate something before it was opened...
http://rextester.com/UTLX59878
declare #table table (id int, openedDate datetime, UserMigratedDate datetime)
insert into #table
values
(1,'2017-01-01','2016-12-31'), --this technically shouldn't happen
(2,'2017-01-01','2017-01-01'),
(3,'2017-01-01','2017-01-02'),
(4,'2017-01-01','2017-01-03'),
(5,'2017-01-01','2017-01-04'),
(6,'2017-01-01','2017-01-05')
select
*,
datediff(day, openedDate, UserMigratedDate) as theDateDiff
from #table
where
datediff(day, openedDate, UserMigratedDate) >= 0
and
datediff(day, openedDate, UserMigratedDate) < 4

how to use sql to get number of seconds from a string formatted like "HH:MM:SS"

I have a C# program that recorded a TimeSpan value into a SQL Server table's varchar field. For example, the varchar field might have the value "00:12:05.7989790".
How could I use SQL code to get that total value in SECONDS? Since that varchar represents 12 minutes and 5 seconds, I would like a SQL statement that extracts it as "725".
I've tried some code like this:
select
Case when IsDate(the_value)=1 then datepart(HOUR, CONVERT(datetime, the_value))*360 else 0 end
+ Case when IsDate(the_value)=1 then datepart(MINUTE, CONVERT(datetime, the_value))*60 else 0 end
+ Case when IsDate(the_value)=1 then datepart(SECOND, CONVERT(datetime, the_value))*1 else 0 end
from mytable
but it complains "Conversion failed when converting date and/or time from character string."
Any suggestions would be greatly appreciated.
DATEDIFF function
https://msdn.microsoft.com/en-AU/library/ms189794.aspx
DATEDIFF(ss, '00:00:00.000', [your time column])
I would use datediff() and use explicit conversion to time:
select datediff(second, '00:00:00', convert(time, the_value))
If the units can exceed 23 hours, then you have a challenge, because the conversion will fail. In this case, string manipulation is an option:
select (left(the_value, 2) * 60 * 60 +
substring(the_value, 3, 2) * 60 +
substring(the_value, 5, 2)
) as seconds
This assumes that all strings are in the proper format. You can validate this with a case:
select (case when the_value like '[0-9]0-9]:[0-9[0-9]:[0-9][0-9]%'
then (left(the_value, 2) * 60 * 60 +
substring(the_value, 3, 2) * 60 +
substring(the_value, 5, 2)
)
end) as seconds

SQL Query to return 24 hour, hourly count even when no values exist?

I've written a query that groups the number of rows per hour, based on a given date range.
SELECT CONVERT(VARCHAR(8),TransactionTime,101) + ' ' + CONVERT(VARCHAR(2),TransactionTime,108) as TDate,
COUNT(TransactionID) AS TotalHourlyTransactions
FROM MyTransactions WITH (NOLOCK)
WHERE TransactionTime BETWEEN CAST(#StartDate AS SMALLDATETIME) AND CAST(#EndDate AS SMALLDATETIME)
AND TerminalId = #TerminalID
GROUP BY CONVERT(VARCHAR(8),TransactionTime,101) + ' ' + CONVERT(VARCHAR(2),TransactionTime,108)
ORDER BY TDate ASC
Which displays something like this:
02/11/20 07 4
02/11/20 10 1
02/11/20 12 4
02/11/20 13 1
02/11/20 14 2
02/11/20 16 3
Giving the number of transactions and the given hour of the day.
How can I display all hours of the day - from 0 to 23, and show 0 for those which have no values?
Thanks.
UPDATE
Using the tvf below works for me for one day, however I'm not sure how to make it work for a date range.
Using the temp table of 24 hours:
-- temp table to store hours of the day
DECLARE #tmp_Hours TABLE ( WhichHour SMALLINT )
DECLARE #counter SMALLINT
SET #counter = -1
WHILE #counter < 23
BEGIN
SET #counter = #counter + 1
--print
INSERT INTO #tmp_Hours
( WhichHour )
VALUES ( #counter )
END
SELECT MIN(CONVERT(VARCHAR(10),[dbo].[TerminalTransactions].[TransactionTime],101)) AS TDate, [#tmp_Hours].[WhichHour], CONVERT(VARCHAR(2),[dbo].[TerminalTransactions].[TransactionTime],108) AS TheHour,
COUNT([dbo].[TerminalTransactions].[TransactionId]) AS TotalTransactions,
ISNULL(SUM([dbo].[TerminalTransactions].[TransactionAmount]), 0) AS TransactionSum
FROM [dbo].[TerminalTransactions] RIGHT JOIN #tmp_Hours ON [#tmp_Hours].[WhichHour] = CONVERT(VARCHAR(2),[dbo].[TerminalTransactions].[TransactionTime],108)
GROUP BY [#tmp_Hours].[WhichHour], CONVERT(VARCHAR(2),[dbo].[TerminalTransactions].[TransactionTime],108), COALESCE([dbo].[TerminalTransactions].[TransactionAmount], 0)
Gives me a result of:
TDate WhichHour TheHour TotalTransactions TransactionSum
---------- --------- ------- ----------------- ---------------------
02/16/2010 0 00 4 40.00
NULL 1 NULL 0 0.00
02/14/2010 2 02 1 10.00
NULL 3 NULL 0 0.00
02/14/2010 4 04 28 280.00
02/14/2010 5 05 11 110.00
NULL 6 NULL 0 0.00
02/11/2010 7 07 4 40.00
NULL 8 NULL 0 0.00
02/24/2010 9 09 2 20.00
So how can I get this to group properly?
The other issue is that for some days there will be no transactions, and these days also need to appear.
Thanks.
You do this by building first the 23 hours table, the doing an outer join against the transactions table. I use, for same purposes, a table valued function:
create function tvfGetDay24Hours(#date datetime)
returns table
as return (
select dateadd(hour, number, cast(floor(cast(#date as float)) as datetime)) as StartHour
, dateadd(hour, number+1, cast(floor(cast(#date as float)) as datetime)) as EndHour
from master.dbo.spt_values
where number < 24 and type = 'p');
Then I can use the TVF in queries that need to get 'per-hour' basis data, even for missing intervals in the data:
select h.StartHour, t.TotalHourlyTransactions
from tvfGetDay24Hours(#StartDate) as h
outer apply (
SELECT
COUNT(TransactionID) AS TotalHourlyTransactions
FROM MyTransactions
WHERE TransactionTime BETWEEN h.StartHour and h.EndHour
AND TerminalId = #TerminalID) as t
order by h.StartHour
Updated
Example of a TVF that returns 24hours between any arbitrary dates:
create function tvfGetAnyDayHours(#dateFrom datetime, #dateTo datetime)
returns table
as return (
select dateadd(hour, number, cast(floor(cast(#dateFrom as float)) as datetime)) as StartHour
, dateadd(hour, number+1, cast(floor(cast(#dateFrom as float)) as datetime)) as EndHour
from master.dbo.spt_values
where type = 'p'
and number < datediff(hour,#dateFrom, #dateTo) + 24);
Note that since master.dbo.spt_values contains only 2048 numbers, the function will not work between dates further apart than 2048 hours.
You have just discovered the value of the NUMBERS table. You need to create a table with a single column containing the numbers 0 to 23 in it. Then you join again this table using an OUTER join to ensure you always get 24 rows returned.
So going back to using Remus' original function, I've re-used it in a recursive call and storing the results in a temp table:
DECLARE #count INT
DECLARE #NumDays INT
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
DECLARE #CurrentDay DATE
DECLARE #tmp_Transactions TABLE
(
StartHour DATETIME,
TotalHourlyTransactions INT
)
SET #StartDate = '2000/02/10'
SET #EndDate = '2010/02/13'
SET #count = 0
SET #NumDays = DateDiff(Day, #StartDate, #EndDate)
WHILE #count < #NumDays
BEGIN
SET #CurrentDay = DateAdd(Day, #count, #StartDate)
INSERT INTO #tmp_Transactions (StartHour, TotalHourlyTransactions)
SELECT h.StartHour ,
t.TotalHourlyTransactions
FROM tvfGetDay24Hours(#CurrentDay) AS h
OUTER APPLY ( SELECT COUNT(TransactionID) AS TotalHourlyTransactions
FROM [dbo].[TerminalTransactions]
WHERE TransactionTime BETWEEN h.StartHour AND h.EndHour
AND TerminalId = 4
) AS t
ORDER BY h.StartHour
SET #count = #Count + 1
END
SELECT *
FROM #tmp_Transactions
group by datepart('hour', thetime). to show those hours with no values you'd have to left join a table of times against the grouping (coalesce(transaction.amount, 0))
I've run into a version of this problem before. The suggestion that worked the best was to setup a table (temporary, or not) with the hours of the day, then do an outer join to that table and group by datepart('h', timeOfRecord).
I don't remember why, but probably due to lack of flexibility because of the need for the other table, I ended up using a method where I group by whatever datepart I want and order by the datetime, then loop through and fill any spaces that are skipped with a 0. This approach worked well for me because I'm not reliant on the database to do all my work for me, and it's also MUCH easier to write an automated test for it.
Step 1, Create #table or a CTE to generate a hours days table. Outer loop for days and inner loop hours 0-23. This should be 3 columns Date, Days, Hours.
Step 2, Write your main query to also have days and hours columns and alias it so you can join it. CTE's have to be above this main query and pivots should be inside CTE's for it to work naturally.
Step 3, Do a select from step 1 table and Left join this Main Query table
ON A.[DATE] = B.[DATE]
AND A.[HOUR] = B.[HOUR]
You can also create a order by if your date columns like
ORDER BY substring(CONVERT(VARCHAR(15), A.[DATE], 105),4,2)
Guidlines
This will then give you all data for hours and days and including zeros for hours with no matches to do that use isnull([col1],0) as [col1].
You can now graph facts against days and hours.

Resources