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

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;

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

Count hours within intervals grouped by day

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];

SQL: Daily report for with user counts

I am trying to get the 30 days report of users, that will return date and total count of users as count created on that date and i did it with this query
Select count(*) As [Count] ,
(SELECT CONVERT(date, AddDate)) As [Date]
from Users
WHERE AddDate >= (SELECT DateAdd(month, -1, Convert(date, GetDate())))
Group By CONVERT(date, AddDate)
it give me only those dates on which any user is created, but i want to show all 30 days either if it has count 0.
Same Case i want to do with monthly report.
i am getting months in which users are created , now i want to change it to get last 12 months from this month and their total users count. For this i am using this query
Select count(*) As [Count] ,
(Select DATEPART( month , DateAdd( month , DATEPART(mm,AddDate) , -1 ) )) as Month
from Users
WHERE AddDate >= (SELECT DateAdd(YEAR, -1, Convert(date, GetDate())))
Group By DATEPART(mm,AddDate)
Using a Calendar CTE:
With NumberSequence ( Number ) as
(
Select 1 as Number
union all
Select Number + 1
from NumberSequence
where Number <= 30
)
, CalendarCTE as
(
select cast(dateadd(dd, -30 + Number,getdate()) as Date) as CalDate
from Numbersequence
)
select CalDate, count(U1.*) as CountUsers
from CalendarCTE
left join Users U1
on CalDate = convert(date, U1.AddDate)
group by CalDate
As I mentioned in comment, You need a Calendar table and Left Join
SELECT Count(u.adddate) AS [Count],
c.dates AS [Date]
FROM calendar_table C
LEFT JOIN users U
ON c.dates = CONVERT(DATE, adddate)
WHERE c.dates >= Dateadd(month, -1, CONVERT(DATE, Getdate()))
GROUP BY c.dates
To generate/create a calendar table or dates check out the below questions
How to generate a range of dates in SQL Server
Generate Dates between date ranges
How to create a Calender table for 100 years in Sql
Try this script :
WITH CTEDates
AS
(
SELECT CAST(GetDate() as date) AS [date]
UNION ALL
SELECT DATEADD(dd, 1, [date])
FROM CTEDates
WHERE DAY([date]) <= 30
)
Select count(*) As [Count] ,CONVERT(date, AddDate) As [Date]
from CTEDates
LEFT JOIN Users ON CTEDates.date=CONVERT(date, AddDate)
WHERE AddDate >= DateAdd(month, -1, GetDate())
Group By CONVERT(date, AddDate)
DECLARE #StartDate Datetime
DECLARE #EndDate Datetime
CREATE TABLE #tMyCalanderDate (dtDate Datetime Primary key)
SELECT #StartDate = '01-Sep-2016'
SELECT #EndDate = '30-Sep-2016'
WHILE #StartDate <= #EndDate
BEGIN
INSERT INTO #tMyCalanderDate (dtDate)
SELECT #StartDate
SELECT #StartDate = DATEADD(day,1,#StartDate)
END
SELECT count(A.UserID) As [Count] , B.dtDate As [Date]
FROM Users AS A
RIGHT JOIN #tMyCalanderDate AS B ON CONVERT(date, A.AddDate) = CONVERT(date, B.dtDate)
WHERE CONVERT(date, A.AddDate) BETWEEN #StartDate AND #EndDate
Group By CONVERT(date, B.dtDate)
You can use a CTE to get a thirty day calendar. Then left join your Users table to it.
DECLARE #CurrentTime DATETIME = GETDATE()
;WITH CTE AS
(
SELECT CONVERT(DATE, #CurrentTime) AS [Date]
UNION ALL
SELECT DATEADD(dd, -1, Date)
FROM CTE
WHERE DATEADD(dd, 29, Date) > #CurrentTime
)
SELECT COUNT(U.AddDate) AS [Count]
, CTE.[Date] AS [Date]
FROM CTE
LEFT JOIN users U
ON CTE.Date = CONVERT(Date, AddDate)
GROUP BY CTE.Date
You can use a similar CTE to get the twelve month calendar and use the same joins to get the count.
HTH.

SQL: Where Clause

SELECT DISTINCT Campaign_id
FROM Impressions
WHERE Date BETWEEN '2015-03-01' AND '2015-03-31' ;
The above query gives me the result for the Campaign_id's that have been active on any date between 2015-03-01 and 2015-03-31.
I want the result set to contain the campaign_id's if the have been active on all the dates in between 2015-03-01 and 2015-03-31.
How would I go about this?
Assuming DATE is a DATE datatype and has no time component.
DECLARE #Start DATE = '2015-03-01',
#End DATE = '2015-03-31'
SELECT Campaign_id
FROM Impressions
WHERE Date BETWEEN #Start AND #End
GROUP BY Campaign_id
HAVING COUNT(DISTINCT Date) = 1 + DATEDIFF(DAY, #Start, #End);
Or a version without the variables
SELECT Campaign_id
FROM Impressions
CROSS APPLY (VALUES ({ d '2015-03-01' },
{ d '2015-03-31' })) V([Start], [End])
WHERE [Date] BETWEEN [Start] AND [End]
GROUP BY Campaign_id, [Start], [End]
HAVING COUNT(DISTINCT Date) = 1 + DATEDIFF(DAY, [Start], [End]);
Using HAVING clause with COUNT(DISTINCT):
SELECT Campaign_id
FROM Impressions
WHERE Date between '2015-03-01' and '2015-03-31'
GROUP BY Campaign_id
HAVING COUNT(DISTINCT Date) = 31;
You should also review this blog post by Aaron Betrand to understand why using BETWEEN for dates is a bad idea.
You can make arrange the query to only mention the dates once by doing something like:
WITH params as (
SELECT CAST('2015-03-01' as DATE) as date1, CAST('2015-03-31' as DATE) date2
)
SELECT i.Campaign_id
FROM params CROSS JOIN
Impressions i
WHERE i.Date >= params.Date1 and i.Date < DATEADD(day, 1, params.Date2)
GROUP BY i.Campaign_id, params.date1, params.date2
HAVING COUNT(DISTINCT i.Date) = 1 + DATEDIFF(day, params.date1, params.date2);
Note: Some would prefer a JOIN to a CROSS JOIN in this case. By habit, I always put a parameters CTE in a query using CROSS JOIN.

Select Dates Between Two Column Values

If I have a table with a StartDate column and an EndDate column can I produce a query that returns a set including every day in the range. I could use a table variable and do some procedural code but I'd like to know if there's a way to do it in a query.
E.g. StartDate = 1/1/2010, EndDate = 1/5/2010, result would be:
1/1/2010
1/2/2010
1/3/2010
1/4/2010
1/5/2010
...for every row in the table that has the StartDate and EndDate columns.
*I'm on SQL 2005
SQL Server 2005+:
WITH dates AS (
SELECT t.startdate 'date'
FROM TABLE t
WHERE t.startdate = '1/1/2010'
UNION ALL
SELECT DATEADD(dd, 1, t.date)
FROM dates t
WHERE DATEADD(dd, 1, t.date) <= (SELECT t.enddate FROM TABLE t WHERE t.enddate = '1/5/2010'))
SELECT ...
FROM TABLE t
JOIN dates d ON d.date = t.date
If your dates are no more than 2047 days apart:
SELECT DATEADD(day,
n.number,
(SELECT t.startdate FROM TABLE t WHERE t.startdate = '1/1/2010')
)
FROM (SELECT DISTINCT number
FROM MASTER.dbo.SPT_VALUES
WHERE name IS NULL) n
WHERE DATEADD(day, n.number, (SELECT t.startdate FROM TABLE t WHERE t.startdate = '1/1/2010')) <= (SELECT t.endate FROM TABLE t WHERE t.endate = '1/5/2010')
with DateList as
(
select cast('1/1/2010' as datetime) DateValue
union all
select DateValue + 1
from DateList
where DateValue + 1 >= '1/1/2010' AND DateValue +1 <= '1/5/2010'
)
select CONVERT(varchar, DateValue, 101)
from DateList
OPTION (MAXRECURSION 0)