Split hours between dates based on date range - sql-server-2012

Split hours spanning between multiple days
I have data like this.
StartDate EndDate
2015-10-05 20:00:00.000 2015-10-06 12:00:00.000
I need this data to be split by date like
2015-10-05 240 (4 hours)
2015-10-06 720 (12 hours)
I can get the first start date split like this
select (StartDate as date) as StartDate,
DATEDIFF(minute, StartDate, dateadd(day, 1, Cast(StartDate as date))) as diff
from t
which gives me
2015-10-05 240
And get the end date's data like
select
Cast(EndDate as date) as StartDate,
DATEDIFF(minute, Cast(EndDate as date), EndDate) as diff
from t
2015-10-06 720
But I am not sure how to do it all in one query. Besides, the time diff betweens start and end can be more than one day diff like this
StartDate EndDate
2015-10-05 20:00:00.000 2015-10-08 12:00:00.000
for which I need
2015-10-05 240
2015-10-06 1440
2015-10-07 1440
2015-10-06 720
Thanks for looking

This should cover when start and end are on the same date and no limit on days
EDIT: fixed issue with incorrect calculation for the end date
declare #StartDate datetime, #EndDate datetime
set #StartDate = '2015-10-05 20:00'
set #EndDate = '2015-10-05 21:00'
;WITH cte AS
(
SELECT cast(#StartDAte as date) StartDate,
cast(dateadd(day, 1, #StartDAte) as date) EndDate
UNION ALL
SELECT DATEADD(day, 1, StartDate) StartDate,
DATEADD(day, 2, StartDate) EndOfDate
FROM cte
WHERE DATEADD(day, 1, StartDate) <= #EndDate
)
select StartDate,
case
when cast(#StartDate as date) = cast(#EndDate as date) then datediff(minute, #StartDate, #EndDate )
when StartDate = cast(#StartDate as date) then datediff(minute, #StartDate, cast(EndDate as datetime))
when StartDate = cast(#EndDate as date) then datediff(minute, cast(StartDate as datetime), #EndDate)
else 1440 end
from cte

Try it like this:
Remark: If the full intervall can be more than 10 days just add some more values to the tally table. Attention: This solution does not yet cover the situation, when start and end is on the same day...
DECLARE #d1 DATETIME={ts'2015-10-05 08:00:00'};
DECLARE #d2 DATETIME={ts'2015-10-07 12:00:00'};
WITH SomePreCalculations AS
(
SELECT #d1 AS D1
,#d2 AS D2
,CAST(#d1 AS DATE) AS StartDate
,DATEADD(DAY,1,CAST(#d1 AS DATE)) AS FirstMidnight
,CAST(#d2 AS DATE) AS LastMidnight
)
,Differences AS
(
SELECT *
,DATEDIFF(MINUTE,D1,FirstMidnight) AS TilMidnight
,DATEDIFF(MINUTE,LastMidnight,D2) AS FromMidnight
FROM SomePreCalculations
)
,TallyTable AS
(
SELECT RowInx FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS x(RowInx)
)
SELECT CAST(Date AS DATE),Minutes FROM
(
SELECT 0 AS Inx, D1 AS Date, TilMidnight AS Minutes
FROM Differences
UNION SELECT RowInx,(SELECT DATEADD(DAY,RowInx,(SELECT StartDate FROM SomePreCalculations))),1440
FROM TallyTable
WHERE DATEADD(DAY,RowInx,(SELECT StartDate FROM SomePreCalculations))<(SELECT LastMidnight FROM SomePreCalculations)
UNION SELECT 99 AS Inx, D2, FromMidnight
FROM Differences
) AS tbl
ORDER BY tbl.Inx

Related

Overnight Rules Query

I have data:
Current Data
StartDate
StartTime
EndDate
EndTime
12/10/2020
09:00:00
12/13/2020
12:00:00
And I want an output as follows:
Output Data
StartDate
StartTime
EndDate
EndTime
Duration
12/10/2020
09:00:00
12/10/2020
23:59:59
14:59:59
12/11/2020
00:00:00
12/11/2020
23:59:59
23:59:59
12/12/2020
00:00:00
12/12/2020
23:59:59
23:59:59
12/13/2020
00:00:00
12/13/2020
12:00:00
12:00:00
How is it possible in T-SQL? I am running SQL Server 14.x version.
My attempt:
With cte as(
Select [Start Date] StartDate ,
Case
When [Start Date] = [End Date] Then Duration
When [Start Date] != [End Date] Then DateDiff(MINUTE,[Start Time],'23:59:59')
End
as ShutdownTime
from [dbo].[Delay]
where
[Start Date]>=#StartDate and [Start Date]<= #EndDate
Union
Select [End Date] StartDate,
DateDiff(Minute,'00:00:00', [End Time])
as ShutdownTime
from [dbo].[Delay]
where [Start Date] !=[End Date]
and
[Start Date]>=#StartDate and [Start Date]<= #EndDate
)
Select StartDate, Sum(ShutDownTime/cast(60 as float)) ShutDownTime
from cte group by StartDate
If it is less than 24 hrs. it works fine. but in case of more than 24 hrs, it fails.
First, it is much simpler to work with date/times rather than having them in separate columns.
Second, you want a recursive CTE. Here is working code:
with cte as (
select convert(datetime, startdate) + convert(datetime, starttime) as day_startdt,
(case when t.startdate = t.enddate
then v.enddt else dateadd(day, 1, t.startdate)
end) as day_enddt,
v.enddt, 1 as lev
from t cross apply
(values (convert(datetime, enddate) + convert(datetime, endtime))) v(enddt)
union all
select cte.day_enddt,
(case when cte.day_enddt < convert(date, cte.enddt)
then dateadd(day, 1, cte.day_enddt) else cte.enddt
end) as day_enddt,
cte.enddt, lev + 1
from cte
where cte.day_startdt < dateadd(day, -1, cte.enddt)
)
select *
from cte;
Here is a db<>fiddle.
Note that this includes full days, which makes sense to me. It does not calculate the duration, because that calculation does not look correct in the question -- and if it is, you need to explain it.
You can easily split this into separate date/time columns if you want, although I don't recommend doing that.
I recommend to create a View with calculated column:
CREATE VIEW [schema_name.]view_name
AS SELECT
StartDate, StartTime, EndDate, EndTime,
DATEDIFF(hour, StartTime, EndTime) + ':' +
DATEDIFF(minute, StartTime, EndTime) + ':' +
DATEDIFF(second, StartTime, EndTime) + ':' AS 'Duration'
FROM [schema_name.]table_name;
Links:
CREATE VIEW
DATEDIFF
If your date/time isn't of type DATETIME (or similar), have a look at CAST and CONVERT
DECLARE #StartDate DATE = '2020-10-12';
DECLARE #StartTime TIME = '09:00:00';
DECLARE #EndDate DATE = '2020-12-12';
DECLARE #EndTime TIME = '12:00:00';
WITH cte as (
select
#StartDate as StartDate,
#StartTime as StartTime,
CASE WHEN #StartDate<>#EndDate THEN #StartDate ELSE #EndDate END as EndDate,
CASE WHEN #StartDate<>#EndDate THEN CAST('23:59:59' AS TIME) ELSE #EndTime END as EndTime
union all
select
DATEADD(day,1,StartDate),
StartTime,
DATEADD(day,1,EndDate),
CASE WHEN EndDate<>#EndDate THEN CAST('23:59:59' AS TIME) ELSE #EndTime END as EndTime
from cte
where EndDate <= #EndDate
)
select
StartDate,
StartTime,
EndDate,
EndTime,
CONVERT(TIME,DATEADD(second, DATEDIFF(second,StartTime,EndTime),0)) as Duration
from cte;
The duration is calculated as the difference between StartTime and EndTime, because storing values in a database that depend only on other fields (in the same table) is not a smart thing to do.

How to get records in specific time range when the time range is between two days

I'm trying to query a specific range of time:
i.e. 1/1/2021 - 1/31/2021
between 5:55AM - 5:00AM (next day) for all days in above date
You need to extract time portion from your timestamps (datetime type for T-SQL) and filter by them in addition to dates filter.
db<>fiddle here
declare
#dt_from datetime
, #dt_to datetime
, #tm_from time
, #tm_to time
, #dt datetime
;
select
#dt = convert(datetime, '2021-02-10 11:48:36', 120)
, #dt_from = convert(date, '2021-02-01', 23)
, #dt_to = convert(date, '2021-02-28', 23)
, #tm_from = convert(time, '05:55:00', 120)
, #tm_to = convert(time, '05:00:00', 120)
;
with a as (
select #dt as dt union all
select dateadd(hour, 20, #dt) union all
select dateadd(hour, -15, #dt) union all
select dateadd(hour, -6, #dt) union all
select dateadd(day, -30, #dt) union all
select convert(datetime, '2021-02-01 00:01:02', 120) union all
select convert(datetime, '2021-02-28 20:01:02', 120)
)
select *
from dt
/*Restrict dates*/
where dt >= #dt_from
and dt < dateadd(day, 1, #dt_to)
and (
/*Exclude time intervals between 05:00 and 05:55*/
cast(dt as time) <= #tm_to
or cast(dt as time) >= #tm_from
)
Or if you need only the cases when entire time frame falls into your dates (e.g. exclude times from 00:00 till 05:00 for start date and from 05:55 till 23:59:59 of end date):
select *
from dt
where dt between #dt_from and #dt_to
and (
/*Exclude time intervals between 05:00 and 05:55*/
cast(dt as time) <= #tm_to
or cast(dt as time) >= #tm_from
)
/*Exclude time intervals at boundary dates*/
and not(
(cast(dt as date) = #dt_from and cast(dt as time) <= #tm_to)
or (cast(dt as date) = #dt_to and cast(dt as time) >= #tm_from)
)

SQL Server: whole weeks total in a calendar month

I want weekly totals in a month. It will not include any partial week or future weeks. Week starts from Monday to Sunday.
I have a table structure like
Date Value -- Comments
----------------------------------------------------------------------
2016-10-01 7 Ignore this because its not a whole week in a month
2016-10-05 8 Week 1
2016-10-07 5 Week 1
2016-10-11 2 Week 2
2016-10-15 1 Week 2
2016-10-17 9 Ignore this because the week is not finished yet
OUTPUT
WeekNo Total
41 13
42 3
The easier way would be to build a Tally "date" table.
you can generate it from any Tally Table like:
DECLARE #StartDate DATE = '20160101'
, #EndDate DATE = '20161231';
WITH cte AS (
SELECT DATEADD(DAY, n - 1, #StartDate) AS date
FROM tally
WHERE n - 1 <= DATEDIFF(DAY, #StartDate, #EndDate)
)
SELECT
c.date
,YEAR(c.date) AS Year
,MONTH(c.date) AS Month
,DAY(c.date) AS Month
,DATEPART(WEEK,c.date) AS Week
,CASE WHEN 7<>COUNT(c.date) OVER (PARTITION BY YEAR(c.date),MONTH(c.date),DATEPART(WEEK,c.date)) THEN 0 ELSE 1 END AS isFullWeek
FROM cte c
Then you just need to Join it to what ever query you need.
DECLARE #StartDate datetime = '2011-10-01';
DECLARE #EndDate datetime = '2016-10-31';
SELECT
CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 2, tblData.RecordDate) AS date) AS WeekStart,
CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 8, tblData.RecordDate) AS date) AS WeekEnd,
SUM(Value) AS Total
FROM tblData
WHERE (#StartDate IS NULL
OR CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 2, tblData.RecordDate) AS date) >= CAST(#StartDate AS date))
AND (#EndDate IS NULL
OR CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 8, tblData.RecordDate) AS date) <= CAST(#EndDate AS date))
AND CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 8, tblData.RecordDate) AS date) < CAST(GETDATE() AS date)
GROUP BY CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 2, tblData.RecordDate) AS date),
CAST(DATEADD(dd, -DATEPART(dw, tblData.RecordDate) + 8, tblData.RecordDate) AS date)
Create a calendar table that meets your request, like this:
create table calendarTable ([date] date, weekNro int)
go
insert into calendarTable
select dateadd(d,n,'20160101'), DATEPART(WK,dateadd(d,n,'20151231'))
from numbers where n<500
If you don't have a Numbers Table, you must create it first. like this
SET NOCOUNT ON
CREATE TABLE Numbers (n bigint PRIMARY KEY)
GO
DECLARE #numbers table(number int);
WITH numbers(number) as (
SELECT 1 AS number
UNION all
SELECT number+1 FROM numbers WHERE number<10000
)
INSERT INTO #numbers(number)
SELECT number FROM numbers OPTION(maxrecursion 10000)
INSERT INTO Numbers(n) SELECT number FROM #numbers
Then query your table joining calendar table having in mind actual date for completed week, like this:
Similar to #Kilren but translated into postgres and using generate series from https://stackoverflow.com/a/11391987/10087503 to generate the dates
DECLARE #StartDate DATE = '20160101'
, #EndDate DATE = '20161231';
WITH cte AS (
SELECT i::date AS date FROM generate_series(#StartDate,
#EndDate, '1 day'::interval) i
)
SELECT
c.date
,DATE_TRUNC('month' ,c.date) AS month_trunc
,DATE_PART('week',c.date) AS week
,CASE WHEN 7<>COUNT(c.date)
OVER (PARTITION BY DATE_TRUNC('month' ,c.date),DATE_PART('week',c.date))
THEN 0 ELSE 1 END AS is_full_week
FROM cte c
Select DATEPART(ww, date) , SUM(Case When Comments Like '%1' then Value when Comments Like '%2' then Value else Value end)
from schema.tablename
group by DATEPART(ww,date)
I'm sorry if this doesn't work, it's the only way I thought to structure it.

write a query that will run all the days and the name of the day between two set dates [duplicate]

This question already has answers here:
Get a list of dates between two dates
(23 answers)
Closed 8 years ago.
I am trying to write a query that will run all the days and the name of the day between two set dates.
Example:
Date1 = 12/28/2005
Date2 = 12/30/2006
Results:
12/28/2005 Wednesday
12/29/2005 Thursday
12/30/2005 Friday
12/31/2005 Saturday
01/01/2006 Sunday
01/02/2006 Monday
01/03/2006 Tuesday
Any help is appreciated!
You may check this fiddle.
The code:
DECLARE #Date1 DATETIME
DECLARE #Date2 DATETIME
SET #Date1 = '20051228'
SET #Date2 = '20061230'
;WITH cteSequence ( SeqNo) as
(
SELECT 0
UNION ALL
SELECT SeqNo + 1
FROM cteSequence
WHERE SeqNo < DATEDIFF(d,#Date1,#Date2)
)
SELECT CONVERT(VARCHAR,DATEADD(d,SeqNo,#Date1),1) + ' ' + DATENAME(dw,DATEADD(d,SeqNo,#Date1))
FROM cteSequence
OPTION ( MAXRECURSION 0);
GO
You can use that table-valued function:
create function DateTable
(
#FirstDate datetime,
#LastDate datetime,
#handle nvarchar(10)='day',
#handleQuantity int=1
)
returns #datetable table (
[date] datetime
)
AS
begin
with CTE_DatesTable
as
(
select #FirstDate AS [date]
union ALL
select case #handle
when 'month' then dateadd(month, #handleQuantity, [date])
when 'year' then dateadd(year, #handleQuantity, [date])
when 'hour' then dateadd(hour, #handleQuantity, [date])
when 'minute' then dateadd(minute, #handleQuantity, [date])
when 'second' then dateadd(second, #handleQuantity, [date])
else dateadd(day, #handleQuantity, [date])
end
from CTE_DatesTable
where #LastDate >=
case #handle
when 'month' then dateadd(month, #handleQuantity, [date])
when 'year' then dateadd(year, #handleQuantity, [date])
when 'hour' then dateadd(hour, #handleQuantity, [date])
when 'minute' then dateadd(minute, #handleQuantity, [date])
when 'second' then dateadd(second, #handleQuantity, [date])
else DATEADD(day, #handleQuantity, [date])
end
)
insert #datetable ([date])
select [date] from CTE_DatesTable
option (MAXRECURSION 0)
return
end
You can call it like:
select [date],datepart(weekday,[date]) from dbo.DateTable('12/28/2005','12/30/2006',default,default)
You didn't specify your DBMS so I'm assuming Postgres:
select i::date, to_char(i, 'Day')
from generate_series(timestamp '2005-12-28 00:00:00',
timestamp '2006-12-30 00:00:00', interval '1' day) i;
The ANSI SQL solution for this would be:
with recursive date_list (the_date) as (
values ( date '2005-12-28' )
union all
select cast(p.the_date + interval '1' day as date)
from date_list p
where p.the_date <= date '2006-12-30
)
select *
from date_list;

Get start and end dates of month in between two dates in SQL?

I have the following function which is supposed to return the start and end dates of the months in between two months, the problem how ever is that since this month is 28 days the function is calculating all the upcoming months on a 28 day basis thus returning the following wrong values.
StartDate EndDate
-----------------------
2013-02-01 2013-02-28
2013-03-01 2013-03-28
2013-04-01 2013-04-28
declare #sDate datetime,
#eDate datetime;
select #sDate = '2013-02-25',
#eDate = '2013-04-25';
;with months as
(
select DATEADD(mm,DATEDIFF(mm,0,#sDate),0) StartDate,
DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,#sDate)+1,0)) EndDate
union all
select dateadd(mm, 1, StartDate),
dateadd(mm, 1, EndDate)
from months
where dateadd(mm, 1, StartDate)<= #eDate
)
select * from months
how can I modify this to return the right dates?
Try this;
declare #sDate datetime,
#eDate datetime
select #sDate = '2013-02-25',
#eDate = '2013-04-25'
;with cte as (
select convert(date,left(convert(varchar,#sdate,112),6) + '01') startDate,
month(#sdate) n
union all
select dateadd(month,n,convert(date,convert(varchar,year(#sdate)) + '0101')) startDate,
(n+1) n
from cte
where n < month(#sdate) + datediff(month,#sdate,#edate)
)
select startdate, dateadd(day,-1,dateadd(month,1,startdate)) enddate
from cte
FIDDLE DEMO
| STARTDATE | ENDDATE |
---------------------------
| 2013-02-01 | 2013-02-28 |
| 2013-03-01 | 2013-03-31 |
| 2013-04-01 | 2013-04-30 |
If you can get the first day of the month, use dateadd twice to get the last day.
First, add 1 month, then subtract 1 day.
Try This : It's a table function. Return a table of month start and end Date with partial Date Also.
CREATE FUNCTION [dbo].[Fun_GetFirstAndLastDateOfeachMonth] (
#StartDate DATE,
#EndDate DATE
)
RETURNS #Items TABLE (
StartDate DATE ,EndDate DATE,MonthNumber INT,YearNumber INT
)
AS
BEGIN
;WITH cte AS (
SELECT CONVERT(DATE,LEFT(CONVERT(VARCHAR,#StartDate,112),6) + '01') startDate,
MONTH(#StartDate) n
UNION ALL
SELECT DATEADD(MONTH,N,CONVERT(DATE,CONVERT(VARCHAR,YEAR(#StartDate)) + '0101')) startDate,
(n+1) n
FROM cte
WHERE n < MONTH(#StartDate) + DATEDIFF(MONTH,#StartDate,#EndDate)
)
INSERT INTO #Items
SELECT CASE WHEN MONTH(startdate) = MONTH(#StartDate) THEN #StartDate ELSE startdate END AS StartDate,
CASE WHEN MONTH(startdate) = MONTH(#EndDate) THEN #EndDate ELSE DATEADD(DAY,-1,DATEADD(MONTH,1,startdate)) END AS enddate,
MONTH(startDate) AS MonthNumner,YEAR(startDate) AS YearNumber
FROM cte
RETURN
END -- End Function
Execute This After Creating The Function
SELECT * From [EDDSDBO].[Fun_GetFirstAndLastDateOfeachMonth] ('2019-02-25','2019-05-20')
You can use the code below
create FUNCTION [dbo].fn_getfirstandenddate (
#StartDate DATE,
#EndDate DATE
)
RETURNS #Items TABLE (
StartDate DATE ,EndDate DATE
)AS
BEGIN
insert into #ItemsSELECT case when #startdate > DATEADD(DAY,1,EOMONTH((DATEADD(MONTH, x.number, #StartDate)),-1)) then #startdate else DATEADD(DAY,1,EOMONTH((DATEADD(MONTH, x.number, #StartDate)),-1)) end startDay,case when #enddate < eomonth(DATEADD(MONTH, x.number, #StartDate)) then #enddate else eomonth(DATEADD(MONTH, x.number, #StartDate)) end endDate FROM master.dbo.spt_values x WHERE x.type = 'P'AND x.number <= DATEDIFF(MONTH, #StartDate, #EndDate);
RETURN
END
End of Month:
#pDate = EOMONTH(GETDATE())
Starting Date:
#pDate = DATEADD(DAY,-1 * (DAY(GETDATE())-1),GETDATE())