Time Difference between Two rows excluding weekend and Time stamp - sql

Date
ID
Value
2022-10-07 17:30:00.000
1
1
2022-10-10 10:00:00.000
2
2
2022-10-12 08:31:42.000
3
1
I want to find date difference from two rows with few conditions in MS SQL
Difference should be from 9am - 6pm (exclude rest of the time)
Also Exclude weekends.
For example, Date Diff from first row will be 1 hr 30 mins.
I am using this query:
DATEDIFF(MINUTE, LAG(date) OVER (ORDER BY date), date)
How can I add more conditions to this?

First things first, let's compute the values we need to handle, namely the current date (that we already have as a field) and the previous date (using the LAG window function, as you did in your attempt).
WITH cte AS (
SELECT [date] AS curDate,
LAG([date]) OVER(ORDER BY date) AS prevDate
FROM tab
)
CASE 1: prevDate's day is equal to curDate's day
In this case we just need to apply the difference in minutes between the two dates as follows:
CASE WHEN CONVERT(DATE, curDate) = CONVERT(DATE, prevDate)
THEN DATEDIFF(MINUTE, prevDate, curDate)
CASE 2: prevDate's day is not equal to curDate's day
Here we need to compute three things:
the difference in time between endtime (18:00:00) and prevDate's time
DATEDIFF(MINUTE, CAST(prevDate AS TIME), '18:00:00')
the difference in time between starttime (9:00:00) and curDate's time
DATEDIFF(MINUTE, '9:00:00', CAST(curDate AS TIME))
the difference in days between curDate and prevDate. For this last one there's the caveat regarding the weekend days though. We can solve this problem by checking if the day of week of curDate is smaller than the day of week of prevDate (if prevDate's working date is Friday and curDate's working date is Monday, we will have dayofweek = 2 for Monday and dayofweek = 6 for Friday, whereas 2-6<0). If the result is smaller than 0, then we need to subtract 3 days of work (multiplied by 9 days of working activity for each day - 18:00:00-9:00:00).
DATEDIFF(MINUTE, '9:00:00', CAST(curDate AS TIME)) +
DATEDIFF(MINUTE, CAST(prevDate AS TIME), '18:00:00') +
9*(DATEDIFF(DAY, prevDate, curDate) -
CASE WHEN DATEPART(WEEKDAY, curDate) - DATEPART(WEEKDAY, prevDate) > 1
THEN 0
ELSE 3 END
)
Hence the Final Query would be the following one:
WITH cte AS (
SELECT [date] AS curDate,
LAG([date]) OVER(ORDER BY date) AS prevDate
FROM tab
)
SELECT curDate,
CASE WHEN CONVERT(DATE, curDate) = CONVERT(DATE, prevDate)
THEN DATEDIFF(MINUTE, prevDate, curDate)
ELSE DATEDIFF(MINUTE, '9:00:00', CAST(curDate AS TIME)) +
DATEDIFF(MINUTE, CAST(prevDate AS TIME), '18:00:00') +
9*(DATEDIFF(DAY, prevDate, curDate) -
CASE WHEN DATEPART(WEEKDAY, curDate) - DATEPART(WEEKDAY, prevDate) > 1
THEN 0
ELSE 3 END)
END
FROM cte
Check the demo here.

Related

How can I return an event duration divided over multiple days in SQL?

I have a database that contains Event data. These Events represent a warning state in a system. They have a StartTime, EndTime, Duration (in seconds) and VarName (and some other attributes that are less relevant for the question).
I am trying to write a query that will allow me to represent the amount of time that a certain warning was active per day. This way, service engineers can easily see if certain changes/fixes caused a warning to decrease or disappear over time.
A quick-and-dirty first attempt is shown below.
SELECT
[VarName] AS metric,
SUM([Duration]) AS value,
Convert(date, [StartTime]) AS date
FROM [dbo].[Events]
WHERE VarName LIKE 'WARN%'
GROUP BY Convert(date, [StartTime]), VarName
ORDER BY date
This works well enough when Events last only a short time and handles multiple Events of the same type in a day. But it breaks down when Events span multiple days (or even weeks).
Example:
VarName
StartTime
EndTime
Duration
WARN_1
2021-06-28 23:00:00.000
2021-06-29 02:00:00.00
10800
What I get:
metric
date
value
WARN_1
2021-06-28
10800
What I want:
metric
date
value
WARN_1
2021-06-28
3600
WARN_1
2021-06-29
7200
Taking into account that:
An event can occur multiple times in the same day
An event can span multiple days
I'll be fiddling with this today and if I come up with a working solution I'll append it to this post. But I don't work with SQL all that often, and it feels like this may require some more advanced trickery. Any help is appreciated!
You can use a recursive CTE to break the periods into days:
with cte as (
select varname, starttime,
(case when datediff(day, starttime, endtime) = 0
then endtime
else dateadd(day, 1, convert(date, starttime))
end) as day_endtime,
endtime
from t
union all
select varname, day_endtime,
(case when datediff(day, starttime, endtime) = 1
then endtime
else dateadd(day, 1, convert(date, day_endtime))
end) as day_endtime,
endtime
from cte
where datediff(day, starttime, endtime) > 0
)
select *
from cte;
To aggregate, change the last part to:
select varname, convert(date, starttime),
sum(datediff(second, starttime, day_endtime))
from cte
group by varname, convert(date, starttime);
Here is a db<>fiddle.
If you need timespans longer than 100 days, add option (maxrecursion 0) or any number larger than about 732.
If you have a numbers table, you can join to that based on the difference in days which will give you n number of rows for each event based on the days. You then just need to calculate how much of each event falls in that day, which I've done with a case expression:
SELECT t.VarName,
StartDate = CONVERT(DATE, DATEADD(DAY, n.Number, t.StartTime)),
Duration = CASE -- starts and ends on same date
WHEN CONVERT(DATE, t.StartTime) = CONVERT(DATE, t.EndTime) THEN t.Duration
-- First Day
WHEN n.Number = 0 THEN DATEDIFF(SECOND, t.StartTime, CONVERT(DATE, DATEADD(DAY, 1, t.StartTime)))
--Last Day
WHEN CONVERT(DATE, DATEADD(DAY, n.Number, t.StartTime)) = CONVERT(DATE, t.EndTime)
THEN DATEDIFF(SECOND, CONVERT(DATE, DATEADD(DAY, n.Number, t.StartTime)), t.EndTime)
-- Middle Day
ELSE 86400
END
FROM #t AS t
INNER JOIN Numbers AS n
ON n.Number <= DATEDIFF(DAY, t.StartTime, t.EndTime);
If you don't have a numbers table, you can very easily create this on the fly:
DECLARE #T TABLE (VarName VARCHAR(10), StartTime DATETIME, EndTime DATETIME, Duration AS DATEDIFF(SECOND, StartTime, EndTime));
INSERT #T (VarName, StartTime, EndTime)
VALUES
('WARN_1', '20210628 23:00:00.000', '20210629 02:00:00.00'),
('WARN_2', '20210629 11:00:00.000', '20210629 14:00:00.00'),
('WARN_3', '20210630 23:00:00.000', '20210704 02:00:00.00');
-- This will do numbers 0-99, add more cross joins if necessary
WITH Numbers (Number) AS
( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) - 1
FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n1 (N)
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n2 (N)
)
SELECT t.VarName,
StartDate = CONVERT(DATE, DATEADD(DAY, n.Number, t.StartTime)),
Duration = CASE -- starts and ends on same date
WHEN CONVERT(DATE, t.StartTime) = CONVERT(DATE, t.EndTime) THEN t.Duration
-- First Day
WHEN n.Number = 0 THEN DATEDIFF(SECOND, t.StartTime, CONVERT(DATE, DATEADD(DAY, 1, t.StartTime)))
--Last Day
WHEN CONVERT(DATE, DATEADD(DAY, n.Number, t.StartTime)) = CONVERT(DATE, t.EndTime)
THEN DATEDIFF(SECOND, CONVERT(DATE, DATEADD(DAY, n.Number, t.StartTime)), t.EndTime)
-- Middle Day
ELSE 86400
END
FROM #t AS t
INNER JOIN Numbers AS n
ON n.Number <= DATEDIFF(DAY, t.StartTime, t.EndTime);

Calculate due date and exclude working/business days SQL

I would like to calculate a due date.
If the start date is 30/12/2020, I need to count 20 working days from start date and display the date for the 20th working date from start date.
For example if the start date is 30/12/2020 must give me 28/01/2021 (excludes saturdays and sundays and finds the 20th working day from 30/12/2020).
But I am unable to exclude the weekends.
SELECT
DATEADD(DAY,20,CAST(CAST('2020-12-30' AS DATE) AS DATETIME))
-(CASE WHEN DATENAME(dw,'2020-12-30') = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw,'2020-12-30') = 'Saturday' THEN 1 ELSE 0 END) AS DueDate
thanks
Your best pick would be to build a calendar table, with a boolean flag that indicates whether each day is a working day or not. Then you would just do something like:
select dt
from calendar
where dt >= '20201231' and is_working_day = 1
order by dt
offset 19 rows fetch next 1 row only
The way your question is asked, however, one option enumerates the days in a subquery until the count of working days is reached:
declare #dt date = '20201231';
declare #no_days int = 20;
with cte as (
select #dt as dt, #no_days as no_days
union all
select dateadd(day, 1, dt),
case when datename(weekday, dt) in ('Saturday', 'Sunday')
then no_days
else no_days - 1
end
from cte
where no_days > 1
)
select max(dt) as res from cte
If you don't care about holidays, then the 20th working day is exactly 28 days later. So you can use:
dateadd(day, 28, '2020-12-30')
Note: This assumes that the starting date is a working day.

How can I select the past seven days and its corresponding week in the past year

How can I select
The past week
Its corresponding days in the year before
This is needed for a dashboard, I would like to show a chart with results from the past seven days. It displays green if our call-center handles 98% of their phone calls within a certain time-span, red if we go over 98%. As a reference I would like to create a chart below with the corresponding seven days in the year before. This is challenging, because weekdays really influence the workload. That means I can't compare a Tuesday with a Sunday or Monday.
For instance, today is Saturday 21st Dec 2019, I would like to report the following timespans:
2019-12-13 00:00:00 -> 2019-12-20 23:59:59
and
2018-12-14 00:00:00 -> 2018-12-21 23:59:59
I made the following code (used within a select statement):
case when cs.ReachedAt between (getdate() - 7) and getdate() then 1 else 0 end as Is_PastWeek
case when cs.ReachedAt between (convert(datetime, convert(varchar(50), convert(date, dateadd(d, -1, dateadd(wk, -52, getdate())))) + ' 23:59:59')) and (convert(datetime, convert(varchar(50), convert(date, dateadd(d, -8, dateadd(wk, -52, getdate())))) + ' 00:00:00')) then 1 else 0 end as Is_SameWeekLastYear
It works, but isn't perfect. I just select the corresponding weekday in the same week as 52 weeks ago. Which means I sometimes end up selecting a matching weekday, but not the nearest. How can I do this better?
EDIT
To clarify what I mean by "picking the nearest corresponding weekday in the year before", i made the following example:
with cte1 as (
select row_number() over (order by (select 1)) - 1 as incrementor
from master.sys.columns sc1
cross join master.sys.columns sc2
), cte2 as (
select dateadd(day, cte1.incrementor, '2000-01-01') as generated_date
from cte1
where dateadd(day, cte1.incrementor, '2000-01-01') < getdate()
), cte3 as (
select convert(date, generated_date) as generated_date
, convert(date, getdate()) as now_date
from cte2
), cte4 as (
select *
, convert(date, dateadd(YEAR, -1, now_date)) as year_back
from cte3
)
select now_date
, generated_date
from cte4
where 1=1
and datepart(week, year_back) = datepart(week, generated_date)
and datepart(DW, year_back) = datepart(DW, generated_date)
This will result in:
For the grey values, I would rather take the weekday of one week later. That way I pick "the nearest corresponding weekday in the year before".
Please note that the above is an example to show what I mean, my ultimate goal is to start with this date, select the whole week before... And all (if possible) neatly within a where clause.
The expression datepart(week, getdate()) will deliver you the calendar week. With this, you can go further.
This is too long for a comment.
What difference does it make? If you are looking for the past week, just look at the same 7 days from the previous year. In one case the week might start on a Tuesday and in the other on a Wednesday. But in both cases, each weekday occurs once.
The logic would be:
where cs.ReachedAt >= datefromparts(year(getdate() - 7) - 1, month(getdate() - 7), day(getdate() - 7) and
cs.ReachedAt < datefromparts(year(getdate()), month(getdate()), day(getdate()))
The logic for the current year:
where cs.ReachedAt >= convert(date, getdate() - 7) and
cs.ReachedAt < convert(date, getdate())

How to skip overlap intervals in the following query to get the accurate per day track time

I have a query (see SQL Fiddle) which calculates the total track time per day. It worked fine until I found that my data is not clean and it has some intervals overlapping (i.e. starttime is repeated in some cases).
There are 1440 minutes in a day and therefore the maximum track time should be 1440, but due to the overlapping intervals the track time exceeds 1440 minutes per day in some cases.
At the moment the query makes it 1440 if the sum exceeds 1440. But if a value is less than 1440 it still can be wrong.
For example
One interval is from 10:00 to 14:00.
Second interval is from 13:00 to 15:00.
End result is 4 + 2 = 6 hours, where hour between 13:00 and 14:00 is counted twice.
End result is 360 minutes, which is less than 1440, but it is not a
correct answer, because data is not correct.
I want some help to fix the query so that it skips overlaps and calculates the correct track time. Thanks
;WITH
CTE_Dates
AS
(
SELECT
Email
,CAST(MIN(StartTime) AS date) AS StartDate
,CAST(MAX(EndTime) AS date) AS EndDate
FROM track
GROUP BY Email
)
SELECT
CTE_Dates.Email
,DayStart AS xDate
-- if some intervals overlap, it is possible
-- to get SUM more than 1440 per day
-- truncate such values for now
,CASE
WHEN ISNULL(SUM(DATEDIFF(second, RangeStart, RangeEnd)) / 60, 0) > 1440
THEN 1440
ELSE ISNULL(SUM(DATEDIFF(second, RangeStart, RangeEnd)) / 60, 0)
END AS TrackMinutes
FROM
Numbers
CROSS JOIN CTE_Dates
CROSS APPLY
(
SELECT
DATEADD(day, Numbers.Number-1, CTE_Dates.StartDate) AS DayStart
,DATEADD(day, Numbers.Number, CTE_Dates.StartDate) AS DayEnd
) AS A_Date
OUTER APPLY
(
SELECT
-- MAX(DayStart, StartTime)
CASE WHEN DayStart > StartTime THEN DayStart ELSE StartTime END AS RangeStart
-- MIN(DayEnd, EndTime)
,CASE WHEN DayEnd < EndTime THEN DayEnd ELSE EndTime END AS RangeEnd
FROM track AS T
WHERE
T.Email = CTE_Dates.Email
AND T.StartTime < DayEnd
AND T.EndTime > DayStart
) AS A_Track
WHERE
Numbers.Number <= DATEDIFF(day, CTE_Dates.StartDate, CTE_Dates.EndDate)+1
GROUP BY DayStart, CTE_Dates.Email
ORDER BY DayStart;
This is a "gaps and islands" problem. I faked my own test data (since you didn't provide any), but I think it works. The key intuition is that all values within the same "island" (that is, contiguous time interval) will have the same difference from a row_number() column. If you want a little insight into it, do a raw select from the IntervalsByDay cte (as opposed to the subquery I have now); this will show you the islands calculated (with start and end points).
edit: I didn't see that you had a fiddle on the first go around. My answer has been changed to reflect your data and desired output
with i as (
select datediff(minute, '2013-01-01', StartTime) as s,
datediff(minute, '2013-01-01', EndTime) as e
from #track
), brokenDown as (
select distinct n.Number
from i
join dbadmin.dbo.Numbers as n
on n.Number >= i.s
and n.Number <= i.e
), brokenDownWithID as (
select Number, Number - row_number() over(order by Number) as IslandID,
cast(dateadd(minute, number, '2013-01-01') as date) as d
from brokenDown
), IntervalsByDay as (
select
dateadd(minute, min(number), '2013-01-01') as [IntervalStart],
dateadd(minute, max(number), '2013-01-01') as [IntervalEnd],
d,
max(Number) - min(Number) + 1 as [NumMinutes]
from brokenDownWithID
group by IslandID, d
)
select d, sum(NumMinutes) as NumMinutes
from IntervalsByDay
group by d
order by d

Function to go back 2 years, first day of last month

I'm hoping to find a solution for this to automate a report I have. Basically what I'm trying to accomplish here is grabbing a date (first day of previous month, two years ago through last day of previous month current year).
So the date span if running this month would look like this: between 4/1/2013 and 3/31/2015
I have found code to get the date two years ago but I'm not able to also incorporate the month functions... Any help is very much appreciated!
For year I'm using this:
SELECT CONVERT(VARCHAR(25),DATEADD(year,-2,GETDATE()),101)
First day of previous month 2 years ago:
SELECT CONVERT(DATE,dateadd(day, -1, dateadd(day, 1 - day(GETDATE()), GETDATE())))
Last day of last month:
SELECT CONVERT(DATE,DATEADD(month, DATEDIFF(month, 0, DATEADD(year,-2,GETDATE())), 0))
Then just do whatever logic you need with them
Your where clause can look something like this:
where date >= cast(dateadd(year, -2,
dateadd(month, -1, getdate() - day(getdate()) + 1)
) as date) and
date < cast(getdate() - day(getdate()) + 1 as date)
This makes use of the handy convenience that subtracting/adding a number to a datetime is the same as adding a date. The start date says: get the first day of the month, then subtract one month, then subtract two years. This could have been done as dateadd(month, -25, . . .), but I think separating the logic is clearer.
This gives you two dates you are looking for:
SELECT
CAST((DATEADD(yy, -2, DATEADD(d, -1 * DATEPART(dd, getdate()) + 1 , GETDATE() ))) as date) as yourTwoYearsAgoDate,
CAST((DATEADD(d, -1 * DATEPART(dd, GETDATE()), GETDATE())) as date) as yourEndOfLastMonthDate
Given a reference date (e.g. "today"),
declare #today date = '23 April 2015'
The 1st of the month is computed by subtracting 1 less than the day number of the current month:
select first_of_current_month = dateadd(day,1-day(#today),#today)
The last day of the previous month is day 0 of the current month, so to get the last day of the previous month, just subtract the current day number:
select last_of_previous_month = dateadd(day,-day(#today),#today)
Moving two years back is easy:
select two_years_back = dateadd(year,-2, #today )
Putting it all together, this should do you:
declare #today date = '23 April 2015'
select *
first_day_of_current_month = dateadd(day,1-day(#today),#today),
last_day_of_previous_month = dateadd(day, -day(#today),#today) ,
date_from = dateadd(year,-2, dateadd(day,1-day(#today),#today) ) ,
date_thru = dateadd(day, -day(#today),#today)
yielding the expected results:
first_day_of_current_month: 2015-04-01
last_day_of_previous_month: 2015-03-31
date_from : 2013-04-01
date_thru : 2015-03-31
So you should be able to say something like this:
select *
from foo t
where t.transaction_date between dateadd(year,-2, dateadd(day,1-day(#today),#today) )
and dateadd(day, -day(#today),#today)
If you have to deal with datetime values rather than date, its easier to not use between and say something like this:
declare #today date = current_timestamp -- get the current date without a time component
select *
from foo t
where t.transaction_date >= dateadd(year,-2, dateadd(day,1-day(#today),#today) )
and t.transaction_date < dateadd(year, 0, dateadd(day, -day(#today),#today)
[superfluous addition of 0 years added for clarity]