TSQL: Continuous period for whole year / each month - sql-server

I try to find all Cust who have membership for at least for one day in each month during 2018.
I came up with solution checking their membership at the beginning / middle / end end of each month like in snippet below, but trying to find more intelligent solution.
I know that I can use tally table for each of 365 days to check this but probably there is more elegant solution ? I'm bit new to SQL, I think I'm missing something in GROUPing area.
In the code snippet shown below, both Cust have at least one day membership.
Desired output:
CustID
------
1
22
Code:
with data as
(
select *
from (values (1, 1, '2017-12-11', '2018-01-16'), (1, 22, '2018-01-28', '2018-03-9' ), (1, 333, '2018-03-1', '2018-12-31') , -- island
(22, 1, '2017-12-31', '2018-01-11'), (22, 2, '2017-2-11', '2019-12-31')) as t (CustID, ContractID, StartDD, EndDD) ---
)
select
isdate(startDD), isdate(EndDD)
from
data
), gaps as
(
select
*,
datediff(day, lag(EndDD, 1, StartDD) over (partition by CustID order by StartDD), StartDD) as BreakDD -- negative is island
from
data
)
select
*,
datepart(month,StartDD) mmS , datepart(month,EndDD) mmE
from
gaps
-- and was active any 1+ day during each of the 12 months in 2018 ????
where
1 = 1
/* and (cast('1/1/2018' as date) between StartDD and EndDD
or cast('1/15/2018' as date) between StartDD and EndDD
or cast('1/31/2018' as date) between StartDD and EndDD)
---- etc.. for each month
and ( cast('12/1/2018' as date) between StartDD and EndDD
or cast('12/15/2018' as date) between StartDD and EndDD
or cast('12/31/2018' as date) between StartDD and EndDD
)
*/
--select CustID, max(BreakDD) Max_Days
--from gaps
--group by CustID

Try this answer.
First create a function to return all the month and year between the given dates.
Function:
--SELECT * FROM dbo.Fn_GetMonthYear('2017-12-11','2018-01-16')
ALTER FUNCTION dbo.Fn_GetMonthYear(#StartDate DATETIME,#EndDate DATETIME)
RETURNS TABLE
AS
RETURN(
SELECT DATEPART(MONTH, DATEADD(MONTH, x.number, #StartDate)) AS [Month]
,DATEPART(YEAR, DATEADD(MONTH, x.number, #StartDate)) AS [Year]
FROM master.dbo.spt_values x
WHERE x.type = 'P'
AND x.number <= DATEDIFF(MONTH, #StartDate, #EndDate)
)
Table Schema:
CREATE TABLE #t(CustID INT, ContractID INT, StartDD date, EndDD date)
INSERT INTO #t values (1, 1, '2017-12-11', '2018-01-16'), (1, 22, '2018-01-28', '2018-03-9' ), (1, 333, '2018-03-1', '2018-12-31') , -- island
(22, 1, '2017-12-31', '2018-01-11'), (22, 2, '2017-2-11', '2019-12-31')
Here is the T-SQL Query for your requirement.
SELECT CustID
,COUNT(DISTINCT [Month]) NoOfMonths
FROM(
SELECT *
FROM #t t
CROSS APPLY dbo.Fn_GetMonthYear(StartDD,EndDD)
)D
WHERE [Year] = 2018
GROUP BY CustID
HAVING COUNT(DISTINCT [Month])=12
Result:
CustID NoOfMonths
1 12
22 12

find all Cust who have membership for at least for one day in each
month during 2018
I think this mean that data must be present between '2018-01-01' and '2018-12-31' for each custid.
CREATE TABLE #t(CustID INT, ContractID INT, StartDD date, EndDD date)
INSERT INTO #t values (1, 1, '2017-12-11', '2018-01-16'), (1, 22, '2018-01-28', '2018-03-9' ), (1, 333, '2018-03-1', '2018-12-31') , -- island
(22, 1, '2017-12-31', '2018-01-11'), (22, 2, '2017-2-11', '2019-12-31')
declare #From Datetime='2018-01-01'
declare #To datetime='2018-12-31'
;with CTE as
(
select CustID,min(StartDD)StartDD
,max(EndDD)EndDD
from #t
group by CustID
)
select CustID,StartDD
,EndDD
from CTE
where StartDD<=#From and EndDD>=#To
This script is not tested across all sample data.
But logic is clear.So it can be corrected accordingly.
So tell for what sample data it is not working.

Related

How to count number of report runs last 7 days? Year to date? All time?

I'm trying to create an SSRS report that looks similar to the table below:
Report
Earliest Run
Recent Run
Runs Last 7 days
Runs YTD
Runs All Time
Report 1
3/3/19 1:30
7/8/22 2:45
8
86
233
I know how to query the last 3 columns individually, but is it possible to get all 3 columns using 1 query? I have tried the query below to show my line of thinking but its not working as desired.
SELECT Report
,Min(TimeStart) AS EarliestRun
,Max(TimeStart) AS RecentRun
,CASE WHEN TimeStart BETWEEN GETDATE()-7 AND GETDATE() THEN COUNT(Report) END AS RunsLast7Days
FROM ReportHistory
WHERE TimeStart BETWEEN '1/1/2019 00:00' AND GETDATE()
GROUP BY Report
Yes - use conditional aggregation. Don't filter the query at all since you need an "all time" value. Instead, use sum with a conditional expression for the periods of interest.
select ...
sum(case when TimeStart >= dateadd(day, -7, getdate()) then 1 else 0 end) as [Runs Last 7 days],
sum(case when TimeStart >= datefromparts(year(getdate()), 1, 1) then 1 else 0 end) as [Runs YTD],
...
from dbo.ReportHistory
order by ...;
I was going to propose using CROSS APPLY but SMor has done it with less code
CREATE TABLE #Reports (
ReportId INT NOT NULL,
ReportName VARCHAR(20) NOT NULL
);
INSERT INTO #Reports(ReportId, ReportName)
VALUES(1, 'Report 1');
CREATE TABLE #ReportRun (
ReportId INT,
RunDateTime DATETIME2(2)
);
INSERT INTO #ReportRun(ReportId, RunDateTime)
VALUES
(1, '20220508 10:00:00'),
(1, '20220502 10:00:00'),
(1, '20220101 10:00:00'),
(1, '20210501 10:00:00'),
(1, '20210209 10:00:00'),
(1, '20200509 10:00:00'),
(1, '20190509 10:00:00');
GO
-- SELECT * FROM #Reports
-- SELECT * FROM #ReportRun
SELECT R.ReportName, B.RunLast7Days, C.RunYearToDate, D.RunAllTime
FROM #Reports AS R
CROSS APPLY (
SELECT TOP 1 RunDateTime
FROM #ReportRun
WHERE ReportId = R.ReportId
ORDER BY RunDateTime DESC
) AS ER
CROSS APPLY (
SELECT COUNT(*) AS RunLast7Days
FROM #ReportRun
WHERE ReportId = R.ReportId
AND RunDateTime >= DATEADD(day, -7, CONVERT(date, GETDATE())) -- best to set it to the start of the day
GROUP BY ReportId
) AS B
CROSS APPLY (
SELECT COUNT(*) AS RunYearToDate
FROM #ReportRun
WHERE ReportId = R.ReportId
AND RunDateTime >= DATEADD(yy, DATEDIFF(yy, 0, GETDATE()), 0)
GROUP BY ReportId
) AS C
CROSS APPLY (
SELECT COUNT(*) AS RunAllTime
FROM #ReportRun
WHERE ReportId = R.ReportId
GROUP BY ReportId
) AS D

How to get next row value based on previous row excluding week ends in SQL Server?

I have a table as follows: (Expected result without weekend exclude logic)
Start Date
End Date(Expected Date)
No of Days(input)
01-01-2021
02-01-2021
2
03-01-2021
08-01-2021
5
09-01-2021
10-01-2021
2
11-01-2021
20-01-2021
10
21-01-2021
09-02-2021
20
10-02-2021
10-02-2021
1
I want to re-generate the StartDate and EndDate data based on the NumberOfDays values, and the StartDate for subsequent rows based on previous row's EndDate + 1 day and in this sequence, I need to exclude the weekend dates as well, and I have another scenario to include weekend dates based on condition.
I want to apply this logic and select the data in same select query using SQL Server.
This is what I have tried
declare #t table ( StartDate date, EndDate date, DaysToAdd int );
insert into #t(StartDate, EndDate, DaysToAdd)
values('20210217', '20210227', 10), ('20210312', '20210310', 10), ('20210326', '20210401', 10), ('20210409', '20210401', 10), ('20210507', '20210401', 10), ('20210606', '20210529', 10), ('20210618', '20210417', 3), ('20210620', '20210309', 2), ('20300913', '20210227', 2), (null, '20300914', 4);
select * from #t
select dateadd(day, -DaysToAdd-1+count(*) over(order by isnull(StartDate, EndDate), EndDate) + sum(DaysToAdd) over(order by isnull(StartDate, EndDate), EndDate), min(StartDate) over()) as NewStartDate, dateadd(day, -1+count(*) over(order by isnull(StartDate, EndDate), EndDate) + sum(DaysToAdd) over(order by isnull(StartDate, EndDate), EndDate), min(StartDate) over()) as NewEndDate, * from #t;
My Expected result:
Start Date
End Date(Expected Date)
No of Days(input)
01-01-2021
04-01-2021
2
05-01-2021
11-01-2021
5
12-01-2021
13-01-2021
2
14-01-2021
27-01-2021
10
28-01-2021
24-02-2021
20
25-02-2021
25-02-2021
1
it is best if you have a calendar table
for the solution, i create a simple calendar table
create table calendar
(
CalDate date,
isWeekEnd bit
);
then populate it with dates
with rcte as
(
select CalDate = convert(date, '2021-01-01')
union all
select CalDate = dateadd(day, 1, CalDate)
from rcte
where CalDate <= '2021-12-30'
)
insert into calendar (CalDate, isWeekEnd)
select CalDate,
case when left(datename(weekday, CalDate), 3) in ('Sat', 'Sun') then 1 else 0 end
from rcte
option (maxrecursion 0)
your sample table & data
declare #t table (id int identity, StartDate date, EndDate date, DaysToAdd int );
insert into #t(StartDate, EndDate, DaysToAdd)
values('2021-01-01', '2021-01-02', 2),
('2021-01-03', '2021-01-08', 5),
('2021-01-09', '2021-01-10', 2),
('2021-01-11', '2021-01-20', 10),
('2021-01-21', '2021-02-09', 20),
('2021-02-10', '2021-02-10', 1);
Since you only interested in the StartDate of first row, I select it into a variable
The actual query
declare #StartDate date;
select #StartDate = StartDate
from #t
where id = 1;
with
cal as
(
select CalDate, rn = row_number() over (order by CalDate)
from Calendar
where CalDate >= #StartDate
and isWeekEnd = 0
),
t as
(
select t.id, t.DaysToAdd,
s = sum(t.DaysToAdd) over (order by t.id) - t.DaysToAdd + 1,
e = sum(t.DaysToAdd) over (order by t.id)
from #t t
)
select t.id,
t.DaysToAdd,
StartDate = s.CalDate,
EndDate = e.CalDate
from t
inner join cal s on t.s = s.rn
inner join cal e on t.e = e.rn
order by t.id
db<>fiddle demo

How to count number per month and then take average of it in same select statement

How to calculate total average per month in case like below?:
We have 9 claimID's. so Aveage would be 9/ 6 distinct months = 1.5
DECLARE #TestTable TABLE (claimid int, DateClosed datetime)
INSERT INTO #TestTable
VALUES (111, '01-01-2018'), (222, '01-03-2018'), (333, '01-12-2018'),
(444, '07-03-2018'), (555, '08-15-2018'), (666, '09-13-2018'),
(777, '04-03-2019'), (888, '05-01-2019'), (999, '07-01-2018'),
(1000, NULL), (1100, NULL), (1200, NULL)
SELECT
ClaimID,
CAST(DateClosed AS DATE) AS DateClosed,
COUNT(ClaimID) CountClaimID,
COUNT(claimid) OVER (PARTITION BY MONT(DateClosed), YEAR(DateClosed)) AS CountPerMonth
FROM
#TestTable
GROUP BY
ClaimID, DateClosed
Perhaps something like this
Example
SELECT ClaimID
,cast(DateClosed AS date) AS DateClosed
,count(ClaimID) CountClaimID
,count(claimid) OVER ( PARTITION BY Month(DateClosed), year(DateClosed)) AS CountPerMonth
,case when DateClosed is null then 0 else count(DateClosed) over () / (select 0.0+count(distinct left(cast(DateClosed as date),7)) from #TestTable) end AS TotalAverage
FROM #TestTable
GROUP BY ClaimID,DateClosed
Returns

Multi - Columns OVERLAPPING DATES

;with cte as (
select Domain_Id, Starting_Date, End_Date
from Que_Date
union all
select t.Domain_Id, cte.Starting_Date, t.End_Date
from cte
join Que_Date t on cte.Domain_Id = t.Domain_Id and cte.End_Date = t.Starting_Date),
cte2 as (
select *, rn = row_number() over (partition by Domain_Id, End_Date order by Domain_Id)
from cte
)
select DISTINCT Domain_Id, Starting_Date, max(End_Date) enddate
from cte2
where rn=1
group by Domain_Id, Starting_Date
order by Domain_Id, Starting_Date;
select * from Que_Date
This is the code that I have wrote but i am getting an extra row i.e 2nd row is extra, the expected output should have only 1st, 3rd and 4th row as output so please help me with it.
I have attached an image showing Input, Excepted Output, and the output that I am getting.
You've got so many results in your first cte. Your first cte has consisting domains. So you cannot filter domains based on your cte. So you query has unnecessary rows.
Try this solution. Cte ConsistentDomains has just consistent domains. So based on this cte, we can get not overlapped results.
Create and fill data:
CREATE TABLE FooTable
(
Domain_ID INT,
Starting_Date DATE,
End_Date Date
)
INSERT INTO dbo.FooTable
(
Domain_ID,
Starting_Date,
End_Date
)
VALUES
( 1, -- Domain_ID - int
CONVERT(datetime,'01-01-2011',103), -- Starting_Date - date
CONVERT(datetime,'05-01-2011',103) -- End_Date - date
)
, (1, CONVERT(datetime,'05-01-2011',103), CONVERT(datetime,'07-01-2011',103))
, (1, CONVERT(datetime,'07-01-2011',103), CONVERT(datetime,'15-01-2011',103))
, (2, CONVERT(datetime,'11-05-2011',103), CONVERT(datetime,'12-05-2011',103))
, (2, CONVERT(datetime,'13-05-2011',103), CONVERT(datetime,'14-05-2011',103))
Query to find not overlapping results:
DECLARE #startDate varchar(50) = '2011-01-01';
WITH ConsistentDomains AS
(
SELECT
f.Domain_ID
, f.Starting_Date
, f.End_Date
FROM FooTable f
WHERE f.Starting_Date = #startDate
UNION ALL
SELECT
s.Domain_ID
, s.Starting_Date
, s.End_Date
FROM FooTable s
INNER JOIN ConsistentDomains cd
ON s.Domain_ID = cd.Domain_ID
AND s.Starting_Date = cd.End_Date
), ConsistentDomainsRownumber AS
(
SELECT
cd.Domain_ID
, cd.Starting_Date
, cd.End_Date
, ROW_NUMBER() OVER (PARTITION BY cd.Domain_ID ORDER BY cd.Starting_Date,
cd.End_Date) RN
FROM ConsistentDomains cd
)
SELECT cd.Domain_ID
, convert(varchar, cd.Starting_Date, 105) Starting_Date
, convert(varchar, cd.End_Date, 105) End_Date
FROM ConsistentDomainsRownumber cd WHERE cd.RN = 1
UNION ALL
SELECT
ft.Domain_ID
, convert(varchar, ft.Starting_Date, 105) Starting_Date
, convert(varchar, ft.End_Date, 105) End_Date
FROM dbo.FooTable ft WHERE ft.Domain_ID NOT IN (SELECT cd.Domain_ID FROM
ConsistentDomainsRownumber cd)
Output:
I used the same table creating script as provided by #stepup, but you can also get your outcome in this way.
CREATE TABLE testtbl
(
Domain_ID INT,
Starting_Date DATE,
End_Date Date
)
INSERT INTO testtbl
VALUES
(1, convert(date, '01-01-2011' ,103), convert(date, '05-01-2011',103) )
,(1, convert(date, '05-01-2011' ,103), convert(date, '07-01-2011',103) )
,(1, convert(date, '07-01-2011' ,103), convert(date, '15-01-2011',103) )
,(2, convert(date, '11-05-2011' ,103), convert(date, '12-05-2011',103) )
,(2, convert(date, '13-05-2011' ,103), convert(date, '14-05-2011',103) )
You can make use of self join and Firs_value and last value within the group to make sure that you are comparing within the same ID and overlapping dates.
select distinct t.Domain_ID,
case when lag(t1.starting_date)over (partition by t.Domain_id order by
t.starting_date) is not null
then first_value(t.Starting_Date) over (partition by t.domain_id order by
t.starting_date)
else t.Starting_Date end StartingDate,
case when lead(t.domain_id) over (partition by t.domain_id order by t.starting_date) =
t1.Domain_ID then isnull(last_value(t.End_Date) over (partition by t.domain_id order by t.end_date rows between unbounded preceding and unbounded following),t.End_Date)
else t.End_Date end end_date
from testtbl t
left join testtbl t1 on t.Domain_ID = t1.Domain_ID
and t.End_Date = t1.Starting_Date
and t.Starting_Date < t1.Starting_Date
Output:
Domain_ID StartingDate end_date
1 2011-01-01 2011-01-15
2 2011-05-11 2011-05-12
2 2011-05-13 2011-05-14

Count the number of times a date is contained between 2 date columns

I have a table that looks like this
ID start_dt end_dt
--------------------------
1 1951-12-05 1951-12-21
2 1951-12-19 1951-12-31
3 1957-12-05 1957-12-19
4 1995-12-06 1995-12-20
5 1996-06-24 1996-07-08
6 1997-05-12 1997-05-26
7 1997-10-07 1997-10-21
8 1997-12-25 1998-01-08
9 1998-01-19 1998-02-02
10 1998-08-05 1998-08-19
I'd like to know how many times each individual date is contained between start_dt and end_dt.
From my example, the result set should look something like this
date count
------------------
1951-12-05 1
1951-12-06 1
...
1951-12-19 2
1951-12-20 2
1951-12-21 2
...
1998-08-19 1
What would be the best way to do this?
EDIT: To clarify, I need each date that appears at least once in a date range (between start_dt and end_dt) to get a row in my result set and I want the number of ranges that this date fits in next to it
hope this helps
When you need to turn 2 values (a range) into a series of rows you can use a number table (see Aaron Bertrand's The SQL Server Numbers Table article if you aren't familiar with the idea).
I've used shorter and simpler data but you should get the idea.
declare #dates table (id int not null, start_dt date not null, end_dt date not null)
insert #dates values (1, '20160601', '20160603'),
(2, '20160603', '20160605'),
(3, '20160610', '20160612')
;with cte as (
select
row_number() over (order by so1.object_id) - 1 as n
from
sys.objects so1
cross join sys.objects so2
)
select
dateadd(d, c.n, d.start_dt) as [date],
count(*)
from
#dates d
join cte c on dateadd(d, c.n, d.start_dt) <= d.end_dt
group by
dateadd(d, c.n, d.start_dt)
order by
dateadd(d, c.n, d.start_dt)
If there are no more than a few days (< 80 or so, depending in your sys.objects table) between start_dt and end_dt, you can use this approach (inspired on Rhys').
DECLARE #dates TABLE (id int not null, start_dt date not null, end_dt date not null)
INSERT #dates VALUES
(1, '1951-12-05', '1951-12-21'),
(2, '1951-12-19', '1951-12-31'),
(3, '1957-12-05', '1957-12-19'),
(4, '1995-12-06', '1995-12-20'),
(5, '1996-06-24', '1996-07-08'),
(6, '1997-05-12', '1997-05-26'),
(7, '1997-10-07', '1997-10-21'),
(8, '1997-12-25', '1998-01-08'),
(9, '1998-01-19', '1998-02-02'),
(10, '1998-08-05', '1998-08-19');
WITH RawData AS (
SELECT
DATEADD(d, n.n, d.start_dt) AS [date]
FROM #dates d
INNER JOIN (
SELECT ROW_NUMBER() OVER (ORDER BY object_id) - 1 AS n FROM sys.objects
) n ON DATEADD(d, n.n, d.start_dt) <= d.end_dt
)
SELECT [date], COUNT(*) [count]
FROM RawData
GROUP BY [date]
ORDER BY [date]
I don't think this could take long even with 1000 date ranges. Perhaps you are using a table with more fields and even missing some index?
You could use a CTE
WITH CTE AS(SELECT start_dt AS dates FROM Table
UNION ALL
SELECT end_dt AS dates FROM Table)
SELECT CAST(dates as DATE) as Date, COUNT(dates) AS Count
FROM CTE c
GROUP BY c.dates
order by Count desc
Or perhaps you need something broader if your columns are of DATETIME data type. This way will GROUP BY the whole day:
WITH CTE AS(SELECT CAST(start_dt AS DATE) AS dates FROM Table
UNION ALL
SELECT CAST(end_dt AS DATE) AS dates FROM Table)
SELECT Dates as Date, COUNT(Dates) AS Count
FROM CTE c
GROUP BY c.dates
order by Count desc

Resources