Weeding out lengthy Durations - sql-server

I only want to keep durations less than 10 minutes long. my current code is as follows:
Duration = DateDiff(ss, TimeBegin, TimeEnd)
TimeBegin and TimeEnd are in TIME format. Obviously Duration right now comes back as:
00:00:07
That's where I'm running into trouble. Can I use a statement that looks like this:
<= 00:10:00 or <= '00:10:00'
Essentially I want:
Duration = (Datediff(ss, TimeBegin, TimeEnd) *ONLY IF LESS THAN 10 MINUTESg)
I already state earlier in the query that if no result is returned to create a NULL, so when a duration is Greater than 10 minutes, I just want it to be ignored as if it didn't exist.

DateDiff(ss, TimeBegin, TimeEnd) gives you the difference in seconds. Just use a Case statement to return the value only if that's under 600 (...ELSE Null is implied):
set #Duration = CASE
WHEN DateDiff(ss, #TimeBegin, #TimeEnd) < 600
THEN DateDiff(ss, #TimeBegin, #TimeEnd)
END;

Agreed with #aucuparia. But (this is for topic starter) be careful with using datediffs is seconds instead of minutes. Their behavior isn't the same, like months/years datediffs. I mean server rounds your operands:
select datediff(mi,'2014-05-15 19:00:00','2014-05-15 19:10:59')
is not the same like
select datediff(ss,'2014-05-15 19:00:00','2014-05-15 19:10:59')
Just execute them both and see the difference. The first one still is 10-minutes difference, but the second will be cut off by 600-seconds 'where' clause.

Related

SQL Server : measuring real-time efficiency by operator

I've been working on some SQL code to measure efficiency in real-time for some production data. Here's a quick background:
Operators will enter in data for specific sub assemblies. This data looks something like this:
ID PO W/S Status Operator TotalTime Date
60129515_2000_6_S025 107294 S025 Completed A 38 05/08/2020
60129515_2000_7_S025 107294 S025 Completed A 46 05/08/2020
60129515_2000_8_S025 107294 S025 Completed A 55 05/08/2020
60129515_2025_6_S020 107295 S020 Completed B 58 05/08/2020
60129515_2025_7_S020 107295 S020 Completed B 47 05/08/2020
60129515_2025_8_S020 107295 S020 Completed B 45 05/08/2020
60129515_2000_1_S090 107294 S090 Completed C 33 05/08/2020
60129515_2000_2_S090 107294 S090 Completed C 34 05/08/2020
60129515_2000_3_S090 107294 S090 Completed C 21 05/08/2020
The relevant columns are the Operator, TotalTime and Date (note that the date is stored as varchar(50) because it plays nicer with Microsoft PowerApps that way).
What I need to do is:
Aggregate the sum of "TotalTime" grouped by Operator
Calculate the time elapsed based on a condition:
If between 7AM and 4PM, calculate the time elapsed since 7AM of the current day
If after 4PM, return the total time between 7AM and 4PM of the current day
Divide the SUM(TotalTime) by the TimeElapsed (AKA the first list item / second list item) in order to get a rough estimate of labor hours worked vs. hours passed in the day.
This calculation would change every time the query was ran. This will allow the Microsoft PowerApp that is pulling this query to refresh the efficiency measure in real time. I've taken a stab at it already - see below:
SELECT
md.Operator,
CASE
WHEN DATEADD(HOUR, -5, GETUTCDATE()) > CONVERT(DATETIME, CONVERT(DATE, DATEADD(HOUR, -5, GETUTCDATE()))) + '7:00' AND GETDATE() < CONVERT(DATETIME, CONVERT(DATE, DATEADD(HOUR, -5, GETUTCDATE()))) + '15:45'
THEN (SUM(isNull(md.TotalTime, 0)) + SUM(isNull(md.DelTime, 0))) * 1.0 / DATEDIFF(MINUTE, CONVERT(DATETIME, CONVERT(DATE, DATEADD(HOUR, -5, GETUTCDATE()))) + '7:00' , DATEADD(HOUR, -5, GETUTCDATE())) * 100.0
ELSE (SUM(isNull(md.TotalTime, 0)) + SUM(isNull(md.DelTime, 0))) / 420 * 100.0
END AS OpEfficiency
FROM
[Master Data] AS md
WHERE
md.[Date] = CONVERT(varchar(50), DATEADD(HOUR, -5, GETUTCDATE()), 101)
GROUP BY
md.Operator
Note: the DelTime is a different column regarding delay times. I am also converting back from UTC time to avoid any time zone issues when transferring to PowerApps.
However, this is horribly inefficient. I am assuming it is because the Date needs to be converted to datetime every single time. Would it work better if I had a calculated column that already had the date converted? Or is there a better way to calculate time elapsed since a certain time?
Thanks in advance.
There are a few things you can do to increase efficiency considerably. First, you want to make sure SQL can do a simple comparison when selecting rows, so you'll start by calculating a string to match your date on since your [Date] field is a string not a date.
Second, calculate the minutes in your shift (either 540 for a full shift or scaled down to 0 at 7 AM exactly) ahead of time so you aren't calculating minutes in each row.
Third, when summing for operators, use a simple sum on the minutes and calculate efficiency from that sum and your pre-calculated shift so far minutes.
One note - I'm casting the minutes-so-far as FLOAT in my example, maybe not the best type but it's clearer than other decimal types like DECIMAL(18,6) or whatever. Pick something that will show the scale you want.
My example uses a Common Table Expression to generate that date string and minutes-so-far FLOAT, that's nice because it fits in a direct query, view, function, or stored procedure, but you could DECLARE variables instead if you wanted to.
By filtering with an INNER JOIN on the [Date] string against the pre-calculated TargetDate string, I make sure the data set is pared down to the fewest records before doing any math on anything. You'll definitely want to INDEX [Date] to keep this fast as your table fills up.
All these together should give a pretty fast query, good luck
with cteNow as ( --Calculate once, up front - date as string, minutes elapsed as FLOAT (or any non-integer)
SELECT CASE WHEN 60*DATEPART(HOUR, GETUTCDATE())+DATEPART(MINUTE, GETUTCDATE()) > 60*21
--4PM in UTC-5, expressed in minutes
THEN CONVERT(float,(16-7)*60) --minutes in (4 PM-7 AM) * 60 minutes/hour
ELSE --Assume nobody is running this at 6 AM, so ELSE = between 7 and 4
CONVERT(float,60*DATEPART(HOUR, GETUTCDATE()) + DATEPART(MINUTE, GETUTCDATE()) - ((7+5)*60))
--Minutes since midnight minus minutes from midnight to 7 AM, shifted by
--UTS offset of 5 hours
END as MinutesToday --Minutes in today's shift so far
, FORMAT(DATEADD(HOUR,-5,GETUTCDATE()),'MM/dd/yyyy') as TargetDate --Date to search for
--as a string so no conversion in every row comparison. Also, index [Date] column
)
SELECT md.Operator, SUM(md.TotalTime) as TotalTime, SUM(md.TotalTime) / MinutesToday as Efficiency
FROM [Master Data] AS md INNER JOIN cteNow as N on N.TargetDate = md.[Date]
GROUP BY md.Operator, MinutesToday
BTW, you didn't make allowances for lunch or running before 7 AM, so I also ignored those. I think both could be addressed in cteNOW without adding much complexity.

Counting Columns with conditions, assigning values based on count

I have a table with call logs. I need to assign time slots for next call based on which time slot the phone number was reachable in.
The relevant columns of the table are:
Phone Number | CallTimeStamp
CallTimeStamp is a datetime object.
I need to calculate the following:
Time Slot: From the TimeStamp, I need to calculate the count for each time slot (eg. 0800-1000, 1001-1200, etc.) for each phone number. Now, if the count is greater than 'n' for a particular time slot, then I need to assign that time slot to that number. Otherwise, I select a default time slot.
Weekday Slot: Same as above, but with weekdays.
Priority: Basically a count of how many times a number was reached
Here's I have gone about solving these issues:
Priority
To calculate the number of times a phone number is called is straight forward. If a number exists in the call log, I know that it was called. In that case, the following query will give me the call count for each number.
SELECT DISTINCT(PhoneNumber), COUNT(PhoneNumber) FROM tblCallLog
GROUP BY PhoneNumber
However, my problem is that I need to change the values in the field Count(PhoneNumber) based on the value in that column itself. How do I go about achieving this? (eg. If Count(PhoneNumber) gives me a value > 20, I need to change it to 5).
Time Slot / Weekday
This is where I'm completely stumped and am looking for the "database" way of doing things.
Unfortunately, I can't get out of my iterative process of thinking. For example, if I was aggregating for a certain phone number (say '123456') and in a certain time slot (say between 0800-1000 hrs), I can write a query like this:
DECLARE #T1Start time = '08:00:00.0000'
DECLARE #T2End time = '10:00:00.0000'
SELECT COUNT(CallTimeStamp) FROM tblCallLog
WHERE PhoneNumber = '123456' AND FORMAT(CallTimeStamp, 'hh:mm:ss') >= #T1Start AND FORMAT(CallTimeStamp, 'hh:mm:ss') < #T2End
Now, I could go through each and every Distinct Phone Number in the table, count the values for each time slot and then assign a slot value for the phone number. However, there has to be a way that does not involve me iterating through a database.
So, I am looking for suggestions on how to solve this.
Thanks
You can use DATEPART Function to get week day slot.
To calculate time slot you can try dividing number of minutes from beginning of day and dividing it by size of the time slot. It would return you slot number. You can use either CASE statement to translate it to proper string or look table where you can store slot descriptions.
SELECT
PhoneNumber
, DATEPART(WEEKDAY, l.CallTimeStamp) AS DayOfWeekSlot
, DATEDIFF(MINUTE, CONVERT(DATE, l.CallTimeStamp), l.CallTimeStamp) / 120 AS TwoHourSlot /*You can change number of minutes to get different slot size*/
, COUNT(*) AS Count
FROM tblCallLog l
GROUP BY PhoneNumber
, DATEPART(WEEKDAY, l.CallTimeStamp)
, DATEDIFF(MINUTE, CONVERT(DATE, l.CallTimeStamp), l.CallTimeStamp) / 120
You could try this to return the phone number, the day of the week and a 2 hour slot. If the volume of calls is greater than 20 the value is set to 5 (not sure why to 5?). The code for the 2 hour section is adapted from this question How to Round a Time in T-SQL where the value 2 in (24/2) is the number of hours in your time period.
SELECT
PhoneNumber
, DATENAME(weekday,CallTimeStamp) as [day]
, CONVERT(smalldatetime,ROUND(CAST(CallTimeStamp as float) * (24/2),0)/(24/2)) AS RoundedTime
, CASE WHEN COUNT(*) > 20 THEN 5 ELSE COUNT(*) END
FROM
tblCallLog
GROUP BY
PhoneNumber
, DATENAME(weekday,dateadd(s,start_ts,'01/01/1970'))

Setting hh:mm to 24 hours format in one single t-sql Query - SQL Server 2012

I got a query which returns some hh:ss time values. The problem however is that it returns it in a PM/AM format while it needs to be a 24 hours format. I can't change the global language setting because this 24 hours time setting is query specific.
I was wondering how to solve this issue?
The query I got now is as follows:
SELECT
dbo.qryMPDisplayPre.Datum, dbo.qryMPDisplayPre.Relatie,
dbo.qryMPDisplayPre.[Order], dbo.qryMPDisplayPre.Status,
dbo.WorkOrder.DeviceID, dbo.Relaties.RelatieNaam AS Monteur,
dbo.Orders.Omschrijving AS OrderOmschrijving,
Format(dbo.WorkOrder.WBTravelDeparture, 'hh:mm') AS TravelDeparture,
Format(dbo.WorkOrder.WBTravelArrival, 'hh:mm') AS TravelArrival,
Format(dbo.WorkOrder.WBWorkArrival, 'hh:mm') AS WorkArrival,
Format(dbo.WorkOrder.WBWorkDeparture, 'hh:mm') AS WorkDeparture,
(CASE WHEN WorkOrder.[WBtravelhours] IS NULL
THEN 0 ELSE (CAST(WorkOrder.[WBTravelHours] * 100.0 / 100.0 AS DECIMAL(30, 2))) END) AS TravelHours,
(CASE WHEN WorkOrder.[wbworkhours] IS NULL
THEN 0 ELSE (CAST(WorkOrder.[WBWorkHours] * 100.0 / 100.0 AS DECIMAL(30, 2))) END) AS WorkHours,
dbo.qryWBMontageGeboekt.Geboekt, dbo.Orders.OpdAdres,
dbo.Orders.OpdPC, dbo.Orders.OpdPlaats,
LEFT(dbo.Orders.Omschrijving, 9) AS Expr1
FROM
dbo.qryWBMontageGeboekt
RIGHT OUTER JOIN
dbo.Orders
RIGHT OUTER JOIN
dbo.Relaties
RIGHT OUTER JOIN
dbo.WorkOrder
RIGHT OUTER JOIN
dbo.qryMPDisplayPre ON dbo.WorkOrder.WONummer = dbo.qryMPDisplayPre.[Order]
AND dbo.WorkOrder.WOStatus = dbo.qryMPDisplayPre.Status
AND dbo.WorkOrder.WOAssignmentDate = dbo.qryMPDisplayPre.Datum
ON dbo.Relaties.RelatieNummer = dbo.qryMPDisplayPre.Relatie
ON dbo.Orders.Nummer = dbo.qryMPDisplayPre.[Order]
ON dbo.qryWBMontageGeboekt.Datum = dbo.qryMPDisplayPre.Datum
AND dbo.qryWBMontageGeboekt.Relatie = dbo.qryMPDisplayPre.Relatie
AND dbo.qryWBMontageGeboekt.[Order] = dbo.qryMPDisplayPre.[Order]
WHERE
(dbo.qryMPDisplayPre.Datum > '11/1/2012')
AND (dbo.qryMPDisplayPre.Status <> 0)
It is kinda weird since the values in WorkArrival are getting displayed correctly in the 24-hours format. Though the values in TravelDeparture, TravelArrival and WorkDeparture aren't while they are formatted the same way as the WorkArrival one.
So this made me believe that there was something wrong with the values from where they are fetched, the WorkOrder table. Though this table contains date times in a 24-hours way and they are all the same (so this couldn't be the problem).
See here the workorder table from where the values are fetched:
As you can see this are all dates with 24 hour HH:MM values.
Now below you can see the Query results with its PM/AM formatted time values:
As you can see the Query results are very weird. It seems that the WorkArrival fields returns its value correct, but the others don't. What is also strange is the fact that the field TravelDeparture returns some off its values correctly (2 top ones) but others incorrect..
Any clue how this can happen, and how I can let the values return in a 24 hours manor (in the query results).
In your example they should all be in 12 hour format, and I see no reason for it not being the case. The format for 12 hours is 'hh' and you are using it in all places.
Is this your original query? If not then check your format strings for upper / lower case. The format for 24 hours happens to be 'HH' (upper case instead of lower case being the only difference).

MSSQL - Extract second from time

I'm new to Microsoft SQL Server. And I wanted to extract second from the time datatype.
I've got experience in PostgreSQL and I extract the second the using this function.
EXTRACT(EPOCH FROM timestamp)
But in Microsoft SQL Server, I found this function -
DATEDIFF(second, 0, timestamp)
Which gives me the second if the time is less the 24 hours.
But when I try a query like this.
SELECT SUM(DATEDIFF(second, '0:00:00', '86:01:12'))
It gives me a error that.
Conversion failed when converting date and/or time from character string
Searching for a solution from Saturday and couldn't find it.
Some one help me out, how can I convert the time datatype which is greater then 24 hours to seconds.
Thanks in advance.
Maybe not the best way, but you can basically explode it to hours, minutes and seconds, then add them all up. If I understand right what you wish to do is to convert a time format to seconds.
This works on 2008 R2
DECLARE #inputTime AS VARCHAR(10) = '86:01:12'
DECLARE #timeSec AS INT = SUBSTRING(#inputTime,1,2) * 3600 + SUBSTRING(#inputTime,4,2) * 60 + SUBSTRING(#inputTime,7,2)
PRINT #timeSec
You can do this as a one-liner, to nest it in any of your queries:
SELECT SUBSTRING('86:01:12',1,2) * 3600 + SUBSTRING('86:01:12',4,2) * 60 + SUBSTRING('86:01:12',7,2)
As you can see it will treat it as a String, get the first two chars as hours, multiply by 3600 (60 seconds in a minute * 60 minutes in an hour). Next take the minutes and multiply them by 60 (60 seconds in a minute), and last add the seconds up, which is * 1 since that is the resolution you are looking for.
For easier use, you can create a function, and use that onwards to make your code less messy:
CREATE FUNCTION dbo.stampSeconds (#inputTime VARCHAR(10))
RETURNS INT
AS BEGIN
DECLARE #timeSec AS INT = SUBSTRING(#inputTime,1,2) * 3600 + SUBSTRING(#inputTime,4,2) * 60 + SUBSTRING(#inputTime,7,2)
RETURN #timeSec
END
Which you can then use like:
SELECT dbo.stampSeconds ('29:07:01')

SQL: Is it possible to SUM() fields of INTERVAL type?

I am trying to sum INTERVAL. E.g.
SELECT SUM(TIMESTAMP1 - TIMESTAMP2) FROM DUAL
Is it possible to write a query that would work both on Oracle and SQL Server? If so, how?
Edit: changed DATE to INTERVAL
I'm afraid you're going to be out of luck with a solution which works in both Oracle and MSSQL. Date arithmetic is something which is very different on the various flavours of DBMS.
Anyway, in Oracle we can use dates in straightforward arithmetic. And we have a function NUMTODSINTERVAL which turns a number into a DAY TO SECOND INTERVAL. So let's put them together.
Simple test data, two rows with pairs of dates rough twelve hours apart:
SQL> alter session set nls_date_format = 'dd-mon-yyyy hh24:mi:ss'
2 /
Session altered.
SQL> select * from t42
2 /
D1 D2
-------------------- --------------------
27-jul-2010 12:10:26 27-jul-2010 00:00:00
28-jul-2010 12:10:39 28-jul-2010 00:00:00
SQL>
Simple SQL query to find the sum of elapsed time:
SQL> select numtodsinterval(sum(d1-d2), 'DAY')
2 from t42
3 /
NUMTODSINTERVAL(SUM(D1-D2),'DAY')
-----------------------------------------------------
+000000001 00:21:04.999999999
SQL>
Just over a day, which is what we would expect.
"Edit: changed DATE to INTERVAL"
Working with TIMESTAMP columns is a little more labourious, but we can still work the same trick.
In the following sample. T42T is the same as T42 only the columns have TIMESTAMP rather than DATE for their datatype. The query extracts the various components of the DS INTERVAL and converts them into seconds, which are then summed and converted back into an INTERVAL:
SQL> select numtodsinterval(
2 sum(
3 extract (day from (t1-t2)) * 86400
4 + extract (hour from (t1-t2)) * 3600
5 + extract (minute from (t1-t2)) * 600
6 + extract (second from (t1-t2))
7 ), 'SECOND')
8 from t42t
9 /
NUMTODSINTERVAL(SUM(EXTRACT(DAYFROM(T1-T2))*86400+EXTRACT(HOURFROM(T1-T2))*
---------------------------------------------------------------------------
+000000001 03:21:05.000000000
SQL>
At least this result is in round seconds!
Ok, after a bit of hell, with the help of the stackoverflowers' answers I've found the solution that fits my needs.
SELECT
SUM(CAST((DATE1 + 0) - (DATE2 + 0) AS FLOAT) AS SUM_TURNAROUND
FROM MY_BEAUTIFUL_TABLE
GROUP BY YOUR_CHOSEN_COLUMN
This returns a float (which is totally fine for me) that represents days both on Oracle ant SQL Server.
The reason I added zero to both DATEs is because in my case date columns on Oracle DB are of TIMESTAMP type and on SQL Server are of DATETIME type (which is obviously weird). So adding zero to TIMESTAMP on Oracle works just like casting to date and it does not have any effect on SQL Server DATETIME type.
Thank you guys! You were really helpful.
You can't sum two datetimes. It wouldn't make sense - i.e. what does 15:00:00 plus 23:59:00 equal? Some time the next day? etc
But you can add a time increment by using a function like Dateadd() in SQL Server.
In SQL Server as long as your individual timespans are all less than 24 hours you can do something like
WITH TIMES AS
(
SELECT CAST('01:01:00' AS DATETIME) AS TimeSpan
UNION ALL
SELECT '00:02:00'
UNION ALL
SELECT '23:02:00'
UNION ALL
SELECT '17:02:00'
--UNION ALL SELECT '24:02:00' /*This line would fail!*/
),
SummedTimes As
(
SELECT cast(SUM(CAST(TimeSpan AS FLOAT)) as datetime) AS [Summed] FROM TIMES
)
SELECT
FLOOR(CAST(Summed AS FLOAT)) AS D,
DATEPART(HOUR,[Summed]) AS H,
DATEPART(MINUTE,[Summed]) AS M,
DATEPART(SECOND,[Summed]) AS S
FROM SummedTimes
Gives
D H M S
----------- ----------- ----------- -----------
1 17 7 0
If you wanted to handle timespans greater than 24 hours I think you'd need to look at CLR integration and the TimeSpan structure. Definitely not portable!
Edit: SQL Server 2008 has a DateTimeOffset datatype that might help but that doesn't allow either SUMming or being cast to float
I also do not think this is possible. Go with custom solutions that calculates the date value according to your preferences.
You can also use this:
select
EXTRACT (DAY FROM call_end_Date - call_start_Date)*86400 +
EXTRACT (HOUR FROM call_end_Date - call_start_Date)*3600 +
EXTRACT (MINUTE FROM call_end_Date - call_start_Date)*60 +
extract (second FROM call_end_Date - call_start_Date) as interval
from table;
You Can write you own aggregate function :-). Please read carefully http://docs.oracle.com/cd/B19306_01/appdev.102/b14289/dciaggfns.htm
You must create object type and its body by template, and next aggregate function what using this object:
create or replace type Sum_Interval_Obj as object
(
-- Object for creating and support custom aggregate function
duration interval day to second, -- In this property You sum all interval
-- Object Init
static function ODCIAggregateInitialize(
actx IN OUT Sum_Interval_Obj
) return number,
-- Iterate getting values from dataset
member function ODCIAggregateIterate(
self IN OUT Sum_Interval_Obj,
ad_interval IN interval day to second
) return number,
-- Merge parallel summed data
member function ODCIAggregateMerge(
self IN OUT Sum_Interval_Obj,
ctx2 IN Sum_Interval_Obj
) return number,
-- End of query, returning summary result
member function ODCIAggregateTerminate
(
self IN Sum_Interval_Obj,
returnValue OUT interval day to second,
flags IN number
) return number
)
/
create or replace type body Sum_Interval_Obj is
-- Object Init
static function ODCIAggregateInitialize(
actx IN OUT Sum_Interval_Obj
) return number
is
begin
actx := Sum_Interval_Obj(numtodsinterval(0,'SECOND'));
return ODCIConst.Success;
end ODCIAggregateInitialize;
-- Iterate getting values from dataset
member function ODCIAggregateIterate(
self IN OUT Sum_Interval_Obj,
ad_interval IN interval day to second
) return number
is
begin
self.duration := self.duration + ad_interval;
return ODCIConst.Success;
exception
when others then
return ODCIConst.Error;
end ODCIAggregateIterate;
-- Merge parallel calculated intervals
member function ODCIAggregateMerge(
self IN OUT Sum_Interval_Obj,
ctx2 IN Sum_Interval_Obj
) return number
is
begin
self.duration := self.duration + ctx2.duration; -- Add two intervals
-- return = All Ok!
return ODCIConst.Success;
exception
when others then
return ODCIConst.Error;
end ODCIAggregateMerge;
-- End of query, returning summary result
member function ODCIAggregateTerminate(
self IN Sum_Interval_Obj,
returnValue OUT interval day to second,
flags IN number
) return number
is
begin
-- return = All Ok, too!
returnValue := self.duration;
return ODCIConst.Success;
end ODCIAggregateTerminate;
end;
/
-- You own new aggregate function:
CREATE OR REPLACE FUNCTION Sum_Interval(
a_Interval interval day to second
) RETURN interval day to second
PARALLEL_ENABLE AGGREGATE USING Sum_Interval_Obj;
/
Last, check your function:
select sum_interval(duration)
from (select numtodsinterval(1,'SECOND') as duration from dual union all
select numtodsinterval(1,'MINUTE') as duration from dual union all
select numtodsinterval(1,'HOUR') as duration from dual union all
select numtodsinterval(1,'DAY') as duration from dual);
Finally You can create SUM function, if you want.

Resources