SQL to get a row for start and end date for each year given a start date and number of years - sql

I have the following data in a SQL table:
+------------------------------------+
| ID YEARS START_DATE |
+------------------------------------+
| ----------- ----------- ---------- |
| 1 5 2020-12-01 |
| 2 8 2020-12-01 |
+------------------------------------+
Trying to create a SQL that would expand the above data and give me a start and end date for each year depending on YEARS and START_DATE from above table. Sample output below:
+-----------------------------------------------+
| ID YEAR DATE_START DATE_END |
+-----------------------------------------------+
| ----------- ----------- ---------- ---------- |
| 1 1 2020-12-01 2021-11-30 |
| 1 2 2021-12-01 2022-11-30 |
| 1 3 2022-12-01 2023-11-30 |
| 1 4 2023-12-01 2024-11-30 |
| 1 5 2024-12-01 2025-11-30 |
| 2 1 2020-12-01 2021-11-30 |
| 2 2 2021-12-01 2022-11-30 |
| 2 3 2022-12-01 2023-11-30 |
| 2 4 2023-12-01 2024-11-30 |
| 2 5 2024-12-01 2025-11-30 |
| 2 6 2025-12-01 2026-11-30 |
| 2 7 2026-12-01 2027-11-30 |
| 2 8 2027-12-01 2028-11-30 |
+-----------------------------------------------+

I would use an inline tally for this, as they are Far faster than a recursive CTE solution. Assuming you have low values for Years:
WITH YourTable AS(
SELECT *
FROM (VALUES(1,5,CONVERT(date,'20201201')),
(2,8,CONVERT(date,'20201201')))V(ID,Years, StartDate))
SELECT ID,
V.I + 1 AS [Year],
DATEADD(YEAR, V.I, YT.StartDate) AS StartDate,
DATEADD(DAY, -1, DATEADD(YEAR, V.I+1, YT.StartDate)) AS EndDate
FROM YourTable YT
JOIN (VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10))V(I) ON YT.Years > V.I;
If you have more than 10~ years you can use either create a tally table, or create an large one inline in a CTE. This would start as:
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I --remove the -1 if you don't want to start from 0
FROM N N1, N N2) --100 rows, add more Ns for more rows
...
Of course, I doubt you have 1,000 of years of data.

You can use a recursive CTE:
with cte as (
select id, 1 as year, start_date,
dateadd(day, -1, dateadd(year, 1, start_date)) as end_date,
years as num_years
from t
union all
select id, year + 1, dateadd(year, 1, start_date),
dateadd(day, -1, dateadd(year, 1, start_date)) as end_date,
num_years
from cte
where year < num_years
)
select id, year, start_date, end_date
from cte;
Here is a db<>fiddle.

In a query, you can use the following:
DATEADD(YEAR, 1, DATE_START) - 1
to add this to the table you can just create the extra column, and set it equal to the value of the above, e.g.
UPDATE MyTable
SET DATE_END = DATEADD(YEAR, 1, DATE_START) - 1

If you are working with sql server, then you can try to use operator CROSS APPLY with master.dbo.spt_values table to get list of numbers and generate dates:
select ID,T.number+1 as YEAR,
--generate date_start using T.number
dateadd(year,T.number,START_DATE)date_start,
--generate end_date: adding 1 year to start date
dateadd(dd,-1,dateadd(year,1,dateadd(year,T.number,START_DATE)))date_end
from Table
cross apply
master.dbo.spt_values T
where T.type='P' and T.number<YEARS

Related

How to get week date (7days interval) from SQL Server

I have a table tbl_Customer_Reg and columns are
| ID | Cust_Name | Registration_Date
+------+-----------+------------------
| 1001 | Mr. ABC | 2021-03-13
| 1002 | Mr. MNO | 2021-02-03
| 1003 | Mr. QWE | 2021-04-12
| 1004 | Mr. XYZ | 2021-01-17
Customers are subscribed to a weekly plan. That means every week they get a message and renew every week according to Registration_Date. That means Mr. XYZ renew every week 17,24,31 January 2021 and so on.
Now I want to know the current date (2021-08-20) or before what is the week date of Mr. XYZ.
I think you should create table-function (use cross apply in query) which used cursor or CTE or while.
I write a simple with CTE(maxrecursion 200). You can try this:
CREATE OR ALTER FUNCTION dbo.fnGetWeeklyDateWithDate(#date date)
RETURNS #Result TABLE(WeekDate date, Lev int)
AS
begin
;WITH CTE
AS
(
SELECT #date WeekDate, 1 Lev
UNION ALL
SELECT DATEADD( Week, 1, WeekDate), Lev + 1
FROM CTE c
WHERE DATEADD( Week, 1, WeekDate) <= CAST(GETDATE() as date)
AND Lev < 200
)
INSERT INTO #Result
SELECT WeekDate, Lev
FROM CTE
OPTION (maxrecursion 200)
RETURN
END
result:
A little bit of date arithmetic should do the trick:
SELECT *,
LastRenewal = DATEADD(day, DATEDIFF(day, GETDATE(), Registration_Date) % 7, CAST(GETDATE() AS date)),
NextRenewal = DATEADD(day, DATEDIFF(day, GETDATE(), Registration_Date) % 7 + 7, CAST(GETDATE() AS date))
FROM YourTable;

Count rows for every day between two date columns SQL Server

I have a table 'events' :
id | name | date_start | date_end
---+------+------------+------------
1 | aaa | 2020-01-01 | 2020-01-03
2 | bbb | 2020-01-02 | 2020-01-05
And I want to count rows for every day between date_start and date_end from that table, for example:
from the first row I want result looks like this:
date | count
-----------+-------
2020-01-01 | 1
2020-01-02 | 1
2020-01-03 | 1
from the second row:
date | count
---------------------
2020-01-02 | 1
2020-01-03 | 1
2020-01-04 | 1
2020-01-05 | 1
So the final result should look like this:
date | count
-----------+-------
2020-01-01 | 1
2020-01-02 | 2
2020-01-03 | 2
2020-01-04 | 1
2020-01-05 | 1
Is it possible to do with sql or should I calculate it on server? That table will have thousand of rows so I dont want to load all of them when I need only that result.
Using ASP.NET Core 3.1 with SQL Server.
You can use recursive cte :
with cte as (
select t.date_start, t.date_end
from table t
union all
select dateadd(day, 1, date_start), date_end
from cte c
where c.date_start < c.date_end
)
select date_start, count(*) as cnt
from cte c
group by date_start
option (maxrecursion 0)
A recursive CTE is convenient here (if you don't have a numbers table or a calendar table on-hand).
with cte as (
select id, name, date_start, date_end
from t
union all
select id, name, dateadd(day, 1, date_start), date_end
from t
where date_start < date_end
)
select date_start as date, count(*)
from cte
group by date_start
order by date_start;
If any of the periods are longer than 100 days, you need to add option (maxrecursion) to the query.
This is usually done with calendar talbe. See sample data below and the query:
declare #tbl table (id int, name varchar(20), date_start date, date_end date)
insert into #tbl values
( 1 , 'aaa' , '2020-01-01' , '2020-01-03'),
( 2 , 'bbb' , '2020-01-02' , '2020-01-05');
-- set boundaries for calendar
declare #startDate date, #endDate date;
select #startDate = min(date_start), #endDate = max(date_end) from #tbl;
;with calendar as (
select #startDate dates
union all
select dateadd(day, 1, dates) from calendar
where dates < #endDate
)
select c.dates, count(*) from calendar c
join #tbl t on c.dates between t.date_start and t.date_end
group by c.dates

How to return same row multiple times with multiple conditions

My knowledge is pretty basic so your help would be highly appreciated.
I'm trying to return the same row multiple times when it meets the condition (I only have access to select query).
I have a table of more than 500000 records with Customer ID, Start Date and End Date, where end date could be null.
I am trying to add a new column called Week_No and list all rows accordingly. For example if the date range is more than one week, then the row must be returned multiple times with corresponding week number. Also I would like to count overlapping days, which will never be more than 7 (week) per row and then count unavailable days using second table.
Sample data below
t1
ID | Start_Date | End_Date
000001 | 12/12/2017 | 03/01/2018
000002 | 13/01/2018 |
000003 | 02/01/2018 | 11/01/2018
...
t2
ID | Unavailable
000002 | 14/01/2018
000003 | 03/01/2018
000003 | 04/01/2018
000003 | 08/01/2018
...
I cannot pass the stage of adding week no. I have tried using CASE and UNION ALL but keep getting errors.
declare #week01start datetime = '2018-01-01 00:00:00'
declare #week01end datetime = '2018-01-07 00:00:00'
declare #week02start datetime = '2018-01-08 00:00:00'
declare #week02end datetime = '2018-01-14 00:00:00'
...
SELECT
ID,
'01' as Week_No,
'2018' as YEAR,
Start_Date,
End_Date
FROM t1
WHERE (Start_Date <= #week01end and End_Date >= #week01start)
or (Start_Date <= #week01end and End_Date is null)
UNION ALL
SELECT
ID,
'02' as Week_No,
'2018' as YEAR,
Start_Date,
End_Date
FROM t1
WHERE (Start_Date <= #week02end and End_Date >= #week02start)
or (Start_Date <= #week02end and End_Date is null)
...
The new table should look like this
ID | Week_No | Year | Start_Date | End_Date | Overlap | Unavail_Days
000001 | 01 | 2018 | 12/12/2017 | 03/01/2018 | 3 |
000002 | 02 | 2018 | 13/01/2018 | | 2 | 1
000003 | 01 | 2018 | 02/01/2018 | 11/01/2018 | 6 | 2
000003 | 02 | 2018 | 02/01/2018 | 11/01/2018 | 4 | 1
...
business wise i cannot understand what you are trying to achieve. You can use the following code though to calculate your overlapping days etc. I did it the way you asked, but i would recommend a separate table, like a Time dimension to produce a "cleaner" solution
/*sample data set in temp table*/
select '000001' as id, '2017-12-12'as start_dt, ' 2018-01-03' as end_dt into #tmp union
select '000002' as id, '2018-01-13 'as start_dt, null as end_dt union
select '000003' as id, '2018-01-02' as start_dt, '2018-01-11' as end_dt
/*calculate week numbers and week diff according to dates*/
select *,
DATEPART(WK,start_dt) as start_weekNumber,
DATEPART(WK,end_dt) as end_weekNumber,
case
when DATEPART(WK,end_dt) - DATEPART(WK,start_dt) > 0 then (DATEPART(WK,end_dt) - DATEPART(WK,start_dt)) +1
else (52 - DATEPART(WK,start_dt)) + DATEPART(WK,end_dt)
end as WeekDiff
into #tmp1
from
(
SELECT *,DATEADD(DAY, 2 - DATEPART(WEEKDAY, start_dt), CAST(start_dt AS DATE)) [start_dt_Week_Start_Date],
DATEADD(DAY, 8 - DATEPART(WEEKDAY, start_dt), CAST(start_dt AS DATE)) [startdt_Week_End_Date],
DATEADD(DAY, 2 - DATEPART(WEEKDAY, end_dt), CAST(end_dt AS DATE)) [end_dt_Week_Start_Date],
DATEADD(DAY, 8 - DATEPART(WEEKDAY, end_dt), CAST(end_dt AS DATE)) [end_dt_Week_End_Date]
from #tmp
) s
/*cte used to create duplicates when week diff is over 1*/
;with x as
(
SELECT TOP (10) rn = ROW_NUMBER() --modify the max you want
OVER (ORDER BY [object_id])
FROM sys.all_columns
ORDER BY [object_id]
)
/*final query*/
select --*
ID,
start_weekNumber+ (r-1) as Week,
DATEPART(YY,start_dt) as [YEAR],
start_dt,
end_dt,
null as Overlap,
null as unavailable_days
from
(
select *,
ROW_NUMBER() over (partition by id order by id) r
from
(
select d.* from x
CROSS JOIN #tmp1 AS d
WHERE x.rn <= d.WeekDiff
union all
select * from #tmp1
where WeekDiff is null
) a
)a_ext
order by id,start_weekNumber
--drop table #tmp1,#tmp
The above will produce the results you want except the overlap and unavailable columns. Instead of just counting weeks, i added the number of week in the year using start_dt, but you can change that if you don't like it:
ID Week YEAR start_dt end_dt Overlap unavailable_days
000001 50 2017 2017-12-12 2018-01-03 NULL NULL
000001 51 2017 2017-12-12 2018-01-03 NULL NULL
000001 52 2017 2017-12-12 2018-01-03 NULL NULL
000002 2 2018 2018-01-13 NULL NULL NULL
000003 1 2018 2018-01-02 2018-01-11 NULL NULL
000003 2 2018 2018-01-02 2018-01-11 NULL NULL

SQL select MAX data within each 12 hours

I have a table called temp. In this table I have Date and Value.
Date | Value
2016/04/01 07:00am | 1
2016/04/01 09:00am | 2
2016/04/01 11:00am | 3
...
2016/04/01 07:00pm | 5
2016/04/01 11:00pm | 2
...
2016/04/02 07:00am | 10
2016/04/02 09:00am | 13
2016/04/02 11:00am | 1
...
2016/04/02 07:00pm | 32
2016/04/02 09:00pm | 40
I would like to return:
Date | Value
04/01/2016 11:00am | 3
04/01/2016 07:00pm | 5
04/02/2016 09:00am | 13
04/02/2016 09:00pm | 40
The idea is to group in 12 hour intervals and then find the max value of said group.
So far I have:
SELECT t.date, max(t.value)
FROM temp t
WHERE t.Date between DATEADD(hour, 7, '04/01/2016') and DATEADD(minute, 1859, '04/02/2016')
GROUP BY DATEPART(Hour, t.date)%12, t.date
ORDER BY Date
But it returns all the data, no 12 hour groups.
Any ideas?
You don't want MAX as you don't want to group by the date, you want the single instance of the datetime that has the largest value. Therefore you can use ROW_NUMBER with a PARTITION based on the date and AM/PM period to get the row with the largest value in that period (ORDER BY t.value DESC):
SELECT date, value
FROM
(SELECT t.date,
t.value,
ROW_NUMBER()
OVER(PARTITION BY CAST(t.date AS date), CASE WHEN DATEPART(hour, t.date) < 12 THEN 0 ELSE 1 END
ORDER BY t.value DESC) AS rownum
FROM temp t
WHERE t.Date between DATEADD(hour, 7, '04/01/2016') and DATEADD(minute, 1859, '04/02/2016')
) max_val
WHERE max_val.rownum = 1
ORDER BY Date

List of years between two dates

I have a table with columns for a start- and enddate.
My goal is to get a list of each year in that timespan for each row, so
+-------------------------+
| startdate | enddate |
+------------+------------+
| 2004-08-01 | 2007-01-08 |
| 2005-06-02 | 2007-05-08 |
+------------+------------+
should output this:
+-------+
| years |
+-------+
| 2004 |
| 2005 |
| 2006 |
| 2007 |
| 2005 |
| 2006 |
| 2007 |
+-------+
I have problems now to create the years in between the two dates. My first approach was to use a UNION (order of dates is irrelevant), but the years in between are missing in this case...
Select
Extract(Year From startdate)
From
table1
Union
Select
Extract(Year From enddate)
From
table1
Thanks for any advises!
Row Generator technique
SQL> WITH DATA1 AS(
2 SELECT TO_DATE('2004-08-01','YYYY-MM-DD') STARTDATE, TO_DATE('2007-01-08','YYYY-MM-DD') ENDDATE FROM DUAL UNION ALL
3 SELECT TO_DATE('2005-06-02','YYYY-MM-DD') STARTDATE, TO_DATE('2007-05-08','YYYY-MM-DD') ENDDATE FROM DUAL
4 ),
5 DATA2 AS(
6 SELECT EXTRACT(YEAR FROM STARTDATE) ST, EXTRACT(YEAR FROM ENDDATE) ED FROM DATA1
7 ),
8 data3
9 AS
10 (SELECT level-1 line
11 FROM DUAL
12 CONNECT BY level <=
13 (SELECT MAX(ed-st) FROM data2
14 )
15 )
16 SELECT ST+LINE FROM
17 DATA2, DATA3
18 WHERE LINE <= ED-ST
19 ORDER BY 1
20 /
ST+LINE
----------
2004
2005
2005
2006
2006
2007
6 rows selected.
SQL>
Try this Query
; with CTE as
(
select datepart(year, '2005-12-25') as yr
union all
select yr + 1
from CTE
where yr < datepart(year, '2013-11-14')
)
select yr
from CTE
Try this:
Create a table with years as follow:
CREATE TABLE tblyears(y int)
INSERT INTO tblyears VALUES (1900);
INSERT INTO tblyears VALUES (1901);
INSERT INTO tblyears VALUES (1902);
and so on until
INSERT INTO tblyears VALUES (2100)
So, you'll write this query:
SELECT y.y
FROM tblyears y
JOIN table1 t
ON y.y >= EXTRACT(year from startdate)
AND y.y <= EXTRACT(year from enddate)
ORDER BY y.y
Show SqlFiddle