Count hours within intervals grouped by day - sql

I have a table tbl with the following sample code:
declare #tbl table
(
Date1 datetime,
Date2 datetime
)
insert into #tbl
values
('2020-06-03 11:00','2020-06-03 14:00'),
('2020-06-03 19:00','2020-06-04 01:00'),
('2020-06-04 11:00','2020-06-04 14:00')
select * from #tbl
I want to output the number of hours that where spent between the two columns.
Sample desired output:
Day | Hours
2020-06-03 | 8
2020-06-04 | 4
I tried doing:
select sum(datediff(hour,Date1,Date2)) from #tbl
group by cast(Date1 as date)
But this doesn't have in mind the interval that crossed the two days, outputting 9 and 3.
Any ideas on this?
Thanks!

For the generic solution, you can use a recursive CTE to split the time spans into separate days and then aggregate:
with cte as (
select date1, date1 as date2,
date2 as enddate, 1 as lev
from t
union all
select date2,
(case when datediff(day, date1, enddate) = 0 then enddate
else dateadd(day, 1, datefromparts(year(date1), month(date1), day(date1)))
end) as date2,
enddate, 1 + lev
from cte
where date1 < enddate
)
select convert(date, date1), sum(datediff(hour, date1, date2))
from cte
group by convert(date, date1);
Here is a db<>fiddle.

I'm ripping this answer from another, with a slight difference to aggregate per day instead.
You need to split your values into each day, and then you can SUM per day:
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP(SELECT MAX(DATEDIFF(DAY, Date1, Date2)+1) FROM #tbl)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I
FROM N N1, N N2, N N3), --1000 days enough?
Dates AS(
SELECT DATEADD(DAY, T.I,CONVERT(date,YT.Date1)) AS [Date],
CASE WHEN T.I = 0 THEN YT.Date1 ELSE DATEADD(DAY, T.I,CONVERT(date,YT.Date1)) END AS StartingDateTime,
CASE WHEN LEAD(T.I) OVER (PARTITION BY YT.Date1 ORDER BY T.I) IS NULL THEN YT.Date2 ELSE DATEADD(DAY, T.I+1,CONVERT(date,YT.Date1)) END AS EndingDateTime
FROM Tally T
JOIN #tbl YT ON T.I <= DATEDIFF(DAY, YT.Date1, YT.Date2))
SELECT D.[Date],
SUM(DATEDIFF(HOUR,D.StartingDateTime,D.EndingDateTime)) AS [Hours]
FROM Dates D
GROUP BY D.[Date];

Related

Split record with start- and enddate into separate records in SQL?

How to split record with start- and enddate into separate records in SQL?
Example:
Start Date End Date
05/09/2000 12/31/2002
The result should be:
Start Date End Date
05/09/2000 12/31/2000
01/01/2001 12/31/2001
01/01/2002 12/31/2002
You can use a recursive CTE. In SQL Server, this would look like:
with cte as (
select start_date, end_date
from t
union all
select datefromparts(year(start_date) + 1, 1, 1), end_date
from cte
where datediff(year, start_date, end_date) > 0
)
select start_date,
(case when datediff(year, start_date, end_date) > 0
then datefromparts(year(start_date), 12, 31)
else end_date
end)
from cte;
Here is a db<>fiddle.
Just another option using an ad-hoc tally table
Example
Declare #YourTable Table ([Start Date] date,[End Date] date) Insert Into #YourTable Values
('05/09/2000','12/31/2002')
Select B.*
From #YourTable A
Cross Apply (
Select [Start Date] = min(D)
,[End Date] = max(D)
From ( Select Top (DateDiff(DAY,[Start Date],[End Date])+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),[Start Date])
From master..spt_values n1,master..spt_values n2
) B1
Group By Year(D)
) B
Returns
Start Date End Date
2000-05-09 2000-12-31
2001-01-01 2001-12-31
2002-01-01 2002-12-31

SQL Server for last 12 months with begin_date and end_date

I'm having trouble to display the last 12 months for each record, can anyone help?
Right now I can only display one month for each record.
DECLARE #DateEnd as DATETIME = DATEADD(month,((YEAR(getdate())-1900)*12) + MONTH(getdate())-1,-1)
-- SET #DateEnd = '20191130'
DECLARE #Frequency_List table (FREQUENCY_ID char(3)); INSERT into #Frequency_List values ('118') -- ('111'),('118'),('110') -- 'MTD','QTD','YTD'
DECLARE #Entity_List table (ENTITY_NAME char(50)); INSERT into #Entity_List values
('F1000'),('R2202'),('R528'),('R810'),('R567'),('R402I'),('R508'),('F1000'),('A950A'),('R557'),('R559'),('R560'),('TBNOBL'),('ALTACORP'),('R590RVME'),('Z490'),('R5070'),('R591'),('R710')
select P.PORTF_CODE, F.EXT_NAME, REPLACE(CONVERT(VARCHAR(10), AKRE.BEGIN_DATE, 102), '.', '-') as 'BEGIN_DATE', REPLACE(CONVERT(VARCHAR(10), AKRE.END_DATE, 102), '.', '-') as 'END_DATE',
ISNULL(AKRE.PTF_END_OAD,0) 'PTF End Duration', ISNULL(AKRE.BMK_END_OAD,0) 'BMK End Duration', (ISNULL(AKRE.PTF_END_OAD,0)-ISNULL(AKRE.BMK_END_OAD,0)) 'Diff End Duration',
ISNULL(AKRE.PTF_END_OAS,0)*10000 'PTF End Spread', ISNULL(AKRE.BMK_END_OAS,0)*10000 'BMK End Spread', (ISNULL(AKRE.PTF_END_OAS,0)-ISNULL(AKRE.BMK_END_OAS,0))*10000 'Diff End Spread',
((ISNULL(AKRE.PTF_END_OAD,0)*(ISNULL(AKRE.PTF_END_OAS,0)*10000))-(ISNULL(AKRE.BMK_END_OAD,0)*(ISNULL(AKRE.BMK_END_OAS,0)*10000))) 'DIF_END_DTS'
from BISAMDW..ATTX_KEY_RATES_EFFECTS AKRE
left join BISAMDW..PORTFOLIO P on P.PORTF_ID = AKRE.PORTF_ID
left join BISAMDW..ATTR_INSTRUMENT AI on AI.ATINS_ID = AKRE.ATINS_ID
left join [BISAMDW].[dbo].[UD_GROUP] GRP on AKRE.USER_DEFINED_GROUP_ID=GRP.USER_DEFINED_GROUP_ID
left join BISAMDW..T_FREQUENCY F on F.FREQUENCY_ID = AKRE.FREQUENCY_ID
where AKRE.END_DATE = #DateEnd and P.PORTF_NAME in ( select ENTITY_NAME from #Entity_List)
and AKRE.PORTF_CONFIG_ID in ( 1 )
and AKRE.FREQUENCY_ID in (select FREQUENCY_ID from #Frequency_List)
and AKRE.PTF_RETURN is NOT null
and GRP.EXT_CODE in ('BARCLAYS','MASTER_2016')
order by 1,2
The result for the columns of start month and end month in sample resultshould be something like this:
MonthStartDate MonthEndDate
2011-04-01 2011-04-30
2011-05-01 2011-05-31
2011-06-01 2011-06-30
2011-07-01 2011-07-31
2011-08-01 2011-08-31
2011-09-01 2011-09-30
2011-10-01 2011-10-31
2011-11-01 2011-11-30
2011-12-01 2011-12-31
....
Thank you!
If what you want is generate a list of month starts and ends for the last 12 months (plus the current month), then one option is to use a recursive query:
with cte as (
select
dateadd(
year,
-1,
datefromparts(year(getdate()), month(getdate()), 1)
) monthStartDate
union all
select dateadd(month, 1, monthStartDate)
where monthStartDate < dateadd(month, -1, getdate())
)
select monthStartDate, eomonth(monthStartDate) monthEndDate from cte
If you are going to use this data several times, then you should store the results of this query in a separate table, which you can then use directly in your queries. This is called a calendar table.
Personally, I would suggest creating a Calendar Table (there are 100's of examples on how to create these), if you simply need the start and end dates of each month. Then getting the start and end dates from that table is trivial:
SELECT MIN(C.CalendarDate) AS MonthStart,
MAX(C.CalendarDate) AS MonthEnd
FROM dbo.Calendar C
WHERE C.CalendarDate >= '20110101'
AND C.CalendarDate < '20200601'
GROUP BY C.CalendarYear,
C.CalendarMonth;
Alternatively, you can use a Tally to generate these on the fly:
DECLARE #StartDate date = '20110101',
#EndDate date = '20200601';
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (SELECT DATEDIFF(MONTH, #StartDate, #EndDate)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I
FROM N N1, N N2, N N3, N N4, N N5)
SELECT DATEADD(MONTH, T.I, #StartDate) AS MonthStart,
EOMONTH(DATEADD(MONTH, T.I, #StartDate)) AS MonthEnd
FROM Tally T;

SQL Query to calculate the hours between two dates grouped by days

I need to write an SQL query for the following scenario.
I am having start date as 2020-01-10 13:00:00.347 and end date as 2020-01-12 02:00:00.347, so I need data grouped as
Day Hours
---- -----
10-01-2020 11
11-01-2020 24
12-01-2020 2.30
which means 11 hours was for the first date and 24 hours in second day and 2.3 hours on 3rd day.
What will the most Efficient SQL query to fetch the data in the above-mentioned format? Thanks in advance.
You can use a recursive CTE to break the dates into ranges:
with recursive cte as (
select start_date as day_start,
(case when date(start_date) = date(end_date) then end_date else date(start_date) + interval 1 day end) as day_end,
end_date
from (select cast('2020-01-10 13:00:00.347' as datetime) as start_date,
cast('2020-01-12 02:00:00.347' as datetime) as end_date
) t
union all
select day_end,
(case when date(day_end) = date(end_date) then end_date else date(day_end) + interval 1 day end) as day_end,
end_date
from cte
where day_end <> end_date
)
select day_start, day_end,
timestampdiff(second, day_start, day_end) / (60 * 60)
from cte;
Here is a db<>fiddle.
EDIT:
In SQL Server, this looks like:
with cte as (
select start_date as day_start,
(case when cast(start_date as date) = cast(end_date as date) then end_date else dateadd(day, 1, cast(start_date as date)) end) as day_end,
end_date
from (select cast('2020-01-10 13:00:00.347' as datetime) as start_date,
cast('2020-01-12 02:00:00.347' as datetime) as end_date
) t
union all
select day_end,
(case when cast(day_end as date) = cast(end_date as date) then end_date else dateadd(day, 1, day_end) end) as day_end,
end_date
from cte
where day_end <> end_date
)
select day_start, day_end,
datediff(second, day_start, day_end) / (60.0 * 60)
from cte;
Here is this db<>fiddle.
As the OP has asked for the most effecient method, and rCTE's are known to perform poorly, a more efficient approach would be using a Tally.
This isn't anywhere near as easy to read for a beginner, however, does get the results you are after (with the exception of that 2020-01-12 has a value of 2.0 not 2.3, as your math is clearly wrong there):
CREATE TABLE dbo.YourTable (StartDate datetime,
EndDate datetime);
INSERT INTO dbo.YourTable (StartDate,
EndDate)
VALUES('2020-01-10T13:00:00.347','2020-01-12T02:00:00.347'),
('2020-01-14T17:24:41.243','2020-01-19T09:17:12.997');
GO
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP(SELECT MAX(DATEDIFF(DAY, StartDate, EndDate)+1) FROM dbo.YourTable)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I
FROM N N1, N N2, N N3), --1000 days enough?
Dates AS(
SELECT DATEADD(DAY, T.I,CONVERT(date,YT.StartDate)) AS [Date],
CASE WHEN T.I = 0 THEN YT.StartDate ELSE DATEADD(DAY, T.I,CONVERT(date,YT.StartDate)) END AS StartingDateTime,
CASE WHEN LEAD(T.I) OVER (PARTITION BY YT.StartDate ORDER BY T.I) IS NULL THEN YT.EndDate ELSE DATEADD(DAY, T.I+1,CONVERT(date,YT.StartDate)) END AS EndingDateTime
FROM Tally T
JOIN dbo.YourTable YT ON T.I <= DATEDIFF(DAY, YT.StartDate, YT.EndDate))
SELECT D.[Date],
(DATEDIFF(SECOND,D.StartingDateTime,D.EndingDateTime) * 1.0) / 60 / 60 AS [Hours]
FROM Dates D;
GO
DROP TABLE dbo.YourTable;
DB<>Fiddle

Grouping of actvie events given Start and End date (SQL Server)

I have the challenge that I have a table with start and end date of an event:
Event Start End
A 01Jan2018 01Mar2018
B 01Feb2018 01Apr2018
I would like to have a table as output with a group by of active events in a month:
Year Month count_active_events
2018 1 1
2018 2 2
2018 3 2
2018 4 1
Can anyone think of a SQL statement that makes this request feasible?
THX
Lazloo
One method generates the dates and then does the calculation using a correlated subquery or apply:
with dates as (
select cast('2018-01-01' as date) as dte
union all
select dateadd(month, 1, dte)
from dates
where dte < '2018-04-01'
)
select d.dte,
(select count(*)
from t
where t.start <= d.dte and t.end >= d.dte
) as num_active
from dates d;
Try this:
declare #tbl table([Event] char(1), [Start] date , [End] date);
insert into #tbl Values
('A' , '01Jan2018', '01Mar2018'),
('B' , '01Feb2018', '01Apr2018');
declare #start date,
#end date;
select #start = min([Start]), #end = max([End]) from #tbl;
with cte as(
select #start dt from #tbl
union all
select dateadd(month, 1, dt) from cte
where dateadd(month, 1, dt) <= #end
)
select datepart(month, c.dt), datepart(year, c.dt), count(distinct [Event]) from cte c
left join #tbl t on c.dt between t.Start and t.[End]
group by datepart(month, c.dt), datepart(year, c.dt)
You need apply with recursive cte to generate start event dates :
with tt1 as (
select event, cast(start as date) start, cast([end] as date) [end]
from table
union all
select event, dateadd(month, 1, start), [end]
from tt1
where start < [end]
)
select year(tt.dt), month(tt.dt), tt1.cnt
from table t cross apply
( values (start), ([end])
) tt (dt) outer apply
( select count(*) cnt
from tt1
where year(tt1.start) = year(tt.dt) and
month(tt1.start) = month(tt.dt)
) tt1;

How to make temporary table with row for each of last 24 hours?

Basically, I want query that would generate result with a row for each hour in last 24 hours:
01/01/2011 00:00:00
01/01/2011 01:00:00
01/01/2011 02:00:00
...
Any way I can do that without cursors and temp tables?
One row for each hour for a given date (SQL Server solution).
select dateadd(hour, Number, '20110101')
from master..spt_values
where type = 'P' and
number between 0 and 23
result with a row for each hour in last 24 hours
select dateadd(hour, datediff(hour, 0, getdate()) - number, 0)
from master..spt_values
where type = 'P' and
number between 0 and 23
Well... on SQL Server you could do this...
WITH cte
AS
(
SELECT CAST('1-jan-2011' AS DATETIME) AS 'date'
UNION ALL
SELECT DATEADD(hh, 1, [date]) FROM cte WHERE [date] < '1-jan-2011 23:00'
)
SELECT [date] FROM cte
...but in reality, a table with just the hours (0 to 23) would be more useful, because you could then add the hour to any date.
WITH cte
AS
(
SELECT 0 as 'Hour'
UNION ALL
SELECT hour + 1 FROM cte WHERE hour < 23
)
SELECT DateAdd(hh, hour, '1-jan-2010') FROM cte
Another, slightly more isoteric way would be to use the row_number ranking function against the first 24 rows of some abitrary object (like spt_values)...
WITH cte AS
(
SELECT n
FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY type ) FROM master..spt_values ) D ( n )
WHERE n < 24
)
SELECT dateadd(hh,n,'01-jan-2011') FROM cte
Here is the simple way...
SELECT '01/01/2011 00:00:00' as [hour], blah, blah2
UNION ALL
SELECT '01/01/2011 01:00:00' as [hour], blah, blah2
UNION ALL
SELECT '01/01/2011 02:00:00' as [hour], blah, blah2
UNION ALL
...etc 24 times.
On a particular platform or solving a particular problem there might be a better way, but you will have to give more detail to get that answer.