How to build a recursive table in inline-table-valued function (iTVF)? - sql

I have four parameters to be accepted by the function:
#FromDate DATE, #ToDate DATE, #DataInterval INT, #RangeInterval INT
The result set is obtained using a CTE:
;WITH Dates_CTE (FromDt, ToDt) AS
(
SELECT
DATEADD(D,-#RangeInterval,#FromDate) AS [FromDt],
#FromDate AS [ToDt]
UNION ALL
SELECT
DATEADD(D,#DataInterval,D.FromDt) AS [FromDt],
DATEADD(D,#DataInterval,D.ToDt) AS [ToDt]
FROM Dates_CTE D
WHERE D.ToDt <= #ToDate
)
But using a CTE makes it a multi-table-valued function and it becomes a performance-killer.
Is there another way to achieve this in an inline function?
The example of the desired result with the input #FromDate = '20180701', #ToDate = '20180901', #DataInterval = 5, #RangeInterval = 30 is:
+------------+------------+
| FromDate | ToDate |
+------------+------------+
| 2018-06-01 | 2018-07-01 |
| 2018-06-06 | 2018-07-06 |
| 2018-06-11 | 2018-07-11 |
| 2018-06-16 | 2018-07-16 |
| 2018-06-21 | 2018-07-21 |
| 2018-06-26 | 2018-07-26 |
| 2018-07-01 | 2018-07-31 |
| 2018-07-06 | 2018-08-05 |
| 2018-07-11 | 2018-08-10 |
| 2018-07-16 | 2018-08-15 |
| 2018-07-21 | 2018-08-20 |
| 2018-07-26 | 2018-08-25 |
| 2018-07-31 | 2018-08-30 |
| 2018-08-05 | 2018-09-04 |
+------------+------------+
How can we achieve this result in an iTVF? Thanks in advance.

Really you need no recursion. Use a table of numbers. Here the table of numbers is generated on the fly it can be persisted in the db for better perfomance.
DECLARE #FromDate DATE, #ToDate DATE, #DataInterval INT, #RangeInterval INT;
SELECT #FromDate = '20180701', #ToDate = '20180901', #DataInterval = 5, #RangeInterval = 30;
-- table of 1000 numbers starting 0
with t0(n) as (
select n
from (
values (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)
) t(n)
),nmbs as(
select row_number() over(order by t1.n) - 1 n
from t0 t1, t0 t2, t0 t3
)
select
DATEADD(D, #DataInterval*n - #RangeInterval, #FromDate) FromDate
,DATEADD(D, #DataInterval*n, #FromDate) ToDate
from nmbs
where DATEADD(D, #DataInterval*n , #FromDate) <= #ToDate;
You may need to adjust where conditions per your requierments.
Demo with the persisted table of numbers (aka tally table).

You can use a tally table inside your inline table-value function.
CREATE TABLE Numbers (N INT PRIMARY KEY NOT NULL);
INSERT INTO Numbers
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) -1 N
FROM
(
VALUES (NULL), (NULL), (NULL), (NULL), (NULL)
) T1(V) CROSS JOIN
(
VALUES (NULL), (NULL), (NULL), (NULL), (NULL)
) T2(VV) CROSS JOIN
(
VALUES (NULL), (NULL), (NULL), (NULL), (NULL) --Add Values as needed
) T3(VVV);
Then create your function as
CREATE FUNCTION dbo.MyFunc
(
#FromDate DATE,
#ToDate DATE,
#DataInterval INT,
#RangeInterval INT
)
RETURNS TABLE
RETURN
SELECT DATEADD(Day, (#DataInterval * N) - #RangeInterval, #FromDate) FromDate,
DATEADD(Day, #DataInterval * N, #FromDate) ToDate
FROM Numbers
WHERE DATEADD(Day, #DataInterval * N, #FromDate) <= #ToDate;
And finally, use it as
SELECT *
FROM dbo.MyFunc ('20180701', '20180901', 5, 30);
Online Demo

you can use while :
declare #FromDate DATE = '20180701', #ToDate DATE= '20180901', #DataInterval INT= 5, #RangeInterval INT= 30,#date date ,#r int =0
declare #table table (d1 date, d2 date)
set #date= #FromDate
while (#date <#ToDate )
begin
insert into #table
select dateadd(d,#r,#FromDate ),DATEADD(D,-#RangeInterval+#r,#FromDate)
set #r=#r+#DataInterval
set #date = dateadd(d,#r,#FromDate )
end
select * from #table

Related

Split a record to multiple rows of record

I have table as below
Master Table
ID Name
1 Bubble
Child Table
ID MasterTableID StartDate EndDate Qty UnitMeasurement
1 1 1/2/2019 1/6/2019 1000 sqft
2 1 1/2/2019 1/4/2019 3000 sqft
I need to select the record above and show it in 5 rows since 1/2 - 1/6 were 5 months.
Date Qty
1/2/2019 200
1/3/2019 200
1/4/2019 200
1/5/2019 200
1/6/2019 200
Second row record to 3 rows record
Date Qty
1/2/2019 1000
1/3/2019 1000
1/4/2019 1000
I'm using SQL Server.
May I know it is possible to do so?
you can use Recursively + CTE and filter using inner join on id
CREATE TABLE T
([ID] int, [MasterTableID] int, [StartDate] datetime, [EndDate] datetime, [Qty] int, [UnitMeasurement] varchar(4))
;
INSERT INTO T
([ID], [MasterTableID], [StartDate], [EndDate], [Qty], [UnitMeasurement])
VALUES
(1, 1, '2019-01-02 00:00:00', '2019-01-06 00:00:00', 1000, 'sqft'),
(2, 1, '2019-01-02 00:00:00', '2019-01-04 00:00:00', 3000, 'sqft')
;
GO
2 rows affected
with cte as (
select [EndDate] as [Date],ID,datediff(day,[StartDate], [EndDate]) diff , [Qty] / (datediff(day,[StartDate], [EndDate]) + 1) as qty
from T
union all
select dateadd(day,-1,[Date]) [Date],T1.ID,T2.diff - 1 as diff,T2.qty
from T T1
inner join cte T2 on T1.ID = T2.ID
where diff >0
)
select ID,[Date],qty
from cte
order by ID,[Date]
GO
ID | Date | qty
-: | :------------------ | ---:
1 | 02/01/2019 00:00:00 | 200
1 | 03/01/2019 00:00:00 | 200
1 | 04/01/2019 00:00:00 | 200
1 | 05/01/2019 00:00:00 | 200
1 | 06/01/2019 00:00:00 | 200
2 | 02/01/2019 00:00:00 | 1000
2 | 03/01/2019 00:00:00 | 1000
2 | 04/01/2019 00:00:00 | 1000
db<>fiddle here
This is achievable using cte. since your dateformat is ddMMyyy, we need to convert this to MMddyyy so we can use dateadd(month...
CREATE TABLE #Temp
(id int, [StartDate] varchar(30), [EndDate] varchar(30), [Qty] int, [UnitMeasurement] varchar(4))
;
INSERT INTO #Temp
(id, [StartDate], [EndDate], [Qty], [UnitMeasurement])
VALUES
(1, '1/2/2019', '1/6/2019', 1000, 'sqft'),
(2, '1/2/2019', '1/4/2019', 3000, 'sqft')
;
GO
with cte as
(
Select id, cast(convert(varchar
, convert(datetime, [StartDate], 103), 101) as date) as startdate
, cast(convert(varchar
, convert(datetime, [EndDate], 103), 101) as date) as enddate
, [Qty]
, 1 as ctr from #Temp
union all
Select id, dateadd(month, 1, startdate), enddate, qty, ctr + 1
From cte
Where startdate < enddate
)
Select t1.id, qty/t2.ct, startdate from cte t1
cross apply (select count(1) ct, id from cte group by id) t2
where t2.id = t1.id
order by t1.id asc
Option (MaxRecursion 0)
drop table #Temp
output:
try like below for generating date
DECLARE #StartDate DATE = '1/2/2019'
, #EndDate DATE = '1/6/2019'
SELECT DATEADD(DAY, nbr - 1, #StartDate)
FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY c.object_id ) AS Nbr
FROM sys.columns c
) nbrs
WHERE nbr - 1 <= DATEDIFF(DAY, #StartDate, #EndDate)
or you can use recursion
Declare #FromDate Date = '1/2/2019',
#ToDate Date = '1/6/2019'
;With DateCte (Date) As
(
Select #FromDate Union All
Select DateAdd(Day, 1, Date)
From DateCte
Where Date <= #ToDate
)
Select Date
From DateCte
Option (MaxRecursion 0)

SQL Query Expanding Each Row Every Int. In Range Between COL_A and COL_B

My SQL Server has a TABLE. Both columns are integers, but represent dates in the form YYYYMM. I'd like to query this table and return a third column which, for every row, includes an integer in the form of YYYYMM for every year/month in the range of the two columns.
Here's the TABLE:
+------------+------------+
| beg_YYYYMM | end_YYYYMM |
+------------+------------+
| 201802 | 201805 |
| 201711 | 201801 |
+------------+------------+
Desired output:
+------------+------------+----------------+
| beg_YYYYMM | end_YYYYMM | month_in_range |
+------------+------------+----------------+
| 201802 | 201805 | 201802 |
| 201802 | 201805 | 201803 |
| 201802 | 201805 | 201804 |
| 201802 | 201805 | 201805 |
| 201711 | 201801 | 201711 |
| 201711 | 201801 | 201712 |
| 201711 | 201801 | 201801 |
+------------+------------+----------------+
Use a recursive CTE:
with cte as (
select beg_YYYYMM, end_YYYYMM,
convert(date, convert(varchar(255), beg_YYYYMM) + '01') as dte,
convert(date, convert(varchar(255), end_YYYYMM) + '01') as end_dte
from t
union all
select beg_YYYYMM, end_YYYYMM,
dateadd(month, 1, dte),
end_dte
from cte
where dte < end_dte
)
select beg_yyyymm, end_yyyymm,
year(dte) * 100 + month(dte) as yyyymm
from cte
order by dte;
Here is a db<>fiddle.
I really don't think I would actually deploy this but I wanted to know what it would look like. If you create a TVF that does the ugly work it isn't too bad. I used Aaron Bertrand's DateDim code with a quick modification to just get first of the month dates between the two dates that are passed in.
CREATE OR ALTER FUNCTION dbo.tvf_MonthRange (#beg_YYYYMM int, #end_YYYYMM int)
RETURNS #Results TABLE
(month_in_range int)
AS
BEGIN
--Have to convert ints to dates
DECLARE #BegDate DATE;
SET #BegDate = DATEFROMPARTS(CAST(SUBSTRING(CAST(#beg_YYYYMM AS varchar(6)),1,4) AS INT), CAST(SUBSTRING(CAST(#beg_YYYYMM AS varchar(6)),5,2) AS INT), 1);
--This needs to be the second day of the month for the code below to work.
DECLARE #EndDate DATE;
SET #EndDate = DATEFROMPARTS(CAST(SUBSTRING(CAST(#end_YYYYMM AS varchar(6)),1,4) AS INT), CAST(SUBSTRING(CAST(#end_YYYYMM AS varchar(6)),5,2) AS INT), 2);
INSERT INTO #Results
SELECT (DATEPART(YEAR, d) *100) + DATEPART(MONTH, d)
FROM
(
SELECT d = DATEADD(DAY, rn - 1, #BegDate)
FROM
(
SELECT TOP (DATEDIFF(DAY, #BegDate, #EndDate))
rn = ROW_NUMBER() OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
-- on my system this would support > 5 million days
ORDER BY s1.[object_id]
) AS x
) AS y
WHERE DATEPART(DAY, d) = 1;
RETURN;
END
Then you can call it like this.
DECLARE #Months TABLE (beg_YYYYMM int, end_YYYYMM int)
INSERT INTO #MONTHS SELECT 201802, 201805
INSERT INTO #MONTHS SELECT 201711, 201801
SELECT *
FROM #Months m
CROSS APPLY dbo.tvf_MonthRange(m.beg_YYYYMM, m.end_YYYYMM) mr ;
It is bad idea Monday right?
A solution using a tally table which may be faster on larger data sets.
with dates as(
select
201501 as beg_YYYYMM
,201504 as end_YYYYMM
union all
select '201711', '201801'
union all
select '201807', '201812'
),
--Tally table
ctedaterange AS (
SELECT top 15
rn = Row_number() OVER(ORDER BY (SELECT NULL)) -1
FROM sys.objects a
)
SELECT
dates.*
,months_in_range = convert(varchar(6), Dateadd(mm, rn, cast(cast(dates.beg_YYYYMM as varchar(8)) +'01' as date)), 112)
FROM dates
cross join ctedaterange
WHERE
rn <= Datediff(mm, cast(cast(dates.beg_YYYYMM as varchar(8)) +'01' as date), cast(cast(dates.end_YYYYMM as varchar(8)) +'01' as date))
ORDER BY
beg_YYYYMM
,Dateadd(mm, rn, cast(cast(dates.beg_YYYYMM as varchar(8)) +'01' as date))
Here's the db<>fiddle

sql server print all dates between two date columns

i have a table containing 3 columns. ID,Start_Date,End_Date.i want to print all the days between Start_Date and End_Date along with ID.
For Example i have table
+----+------------+------------+
| ID | Start_Date | End_Date |
+----+------------+------------+
| 1 | 2017-01-01 | 2017-01-05 |
+----+------------+------------+
and i want result like
+----+------------+
| ID | Date |
+----+------------+
| 1 | 2017-01-01 |
| 1 | 2017-01-02 |
| 1 | 2017-01-03 |
| 1 | 2017-01-04 |
| 1 | 2017-04-05 |
+----+------------+
Use Common table expression :
DECLARE #StartDT DATETIME = '2017-01-01'
DECLARE #EndDT DATETIME = '2017-01-05'
DECLARE #Id INT = 1
;WITH CTE (_id , _Date)AS
(
SELECT #Id , #StartDT
UNION ALL
SELECT #Id , DATEADD(DAY,1,_Date)
FROM CTE
WHERE _Date < #EndDT
)
SELECT * FROM CTE
Create a so called tally table (see e. g. here: https://dwaincsql.com/2014/03/27/tally-tables-in-t-sql/) and use it to create all dates between "from" and "to" date.
SELECT TOP 1000000 N=IDENTITY(INT, 1, 1)
INTO dbo.Tally
FROM master.dbo.syscolumns a CROSS JOIN master.dbo.syscolumns b;
go
declare #dateFrom datetime = '20170101';
declare #dateTo datetime = '20170105';
select dateadd(day, N - 1, #dateFrom)
from Tally
where N between 1 and datediff(day, #dateFrom, #dateTo) + 1
Or:
select dateadd(day, t.N - 1, o.DateFrom)
from Tally t
cross join OtherTable o
where t.N between 1 and datediff(day, o.DateFrom, o.DateTo) + 1
A tally table is very useful for such cases, it could also be filled with dates in a second column, starting from 1900-01-01 or so.
DECLARE #StartDT DATETIME = '2017-01-01'
DECLARE #EndDT DATETIME = '2017-01-05'
DECLARE #Id INT = 1
SELECT RANK() OVER (
ORDER BY (SELECT 1)) AS SeqNo
,CAST (Start_Date AS DATE) AS Start_Date
FROM (
SELECT #StartDT + Row_Number() OVER (ORDER BY Rno) - 1 AS Start_Date
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) - 1 AS Rno
FROM master..spt_values
) Dt
) Dt2
WHERE Dt2.Start_Date <= #EndDT
OutPut
SeqNo Start_Date
--------------------
1 2017-01-01
1 2017-01-02
1 2017-01-03
1 2017-01-04
1 2017-01-05

Create a calendar day table

I am trying to write a code where i can plug in a date and my table below will populate the expected date table with all the date for the particular month from CD1(Calendar Day 1) all the way to CD30 or CD31 or in February case CD28. I know i should begin my code with something like
Declare #startdate as datetime
Set #startdate = '20170401'
But after that I get confused with the DateAdd and DatePart code to create this query to produce the results
date rule | expected date |
----------------------------
| CD1 | 4/1/2017 |
| CD2 | 4/2/2017 |
| CD3 | 4/3/2017 |
| CD4 | 4/4/2017 |
| CD5 | 4/5/2017 |
| CD6 | 4/6/2017 |
Can anyone provide any assistance?
Try this,
Declare #startdate as datetime
Set #startdate = '20170401'
;with cte as
(
select #startdate dt,1 ruleid
union ALL
select dateadd(day,1,dt)
,ruleid+1
from cte
where
dt<dateadd(day,-1,dateadd(month, datediff(month,0,#startdate)+1,0))
)
select *,'CD'+cast(ruleid as varchar) CalenderRule
from cte
DECLARE #startdate datetime = '2017-04-01'
DECLARE #startdate_for_loop datetime
SET #startdate_for_loop = #startdate
CREATE TABLE #T (date_rule nvarchar(100), exp_date datetime)
declare #x int = 1
WHILE MONTH(#startdate) = MONTH(#startdate_for_loop)
BEGIN
INSERT INTO #T VALUES ('CD' + CAST(#x as nvarchar(max)), #startdate_for_loop)
SET #x = #x + 1
SET #startdate_for_loop = DATEADD(DD, 1, #startdate_for_loop)
END
SELECT * FROM #T
Try below query, this will give you the required output:
DECLARE #STARTDATE DATETIME
SET #STARTDATE= CAST(MONTH(CURRENT_TIMESTAMP) AS VARCHAR(100))+'/'+'01'+'/'+CAST(YEAR(CURRENT_TIMESTAMP) AS VARCHAR(100))
;WITH MONTHDATA
AS
(SELECT #STARTDATE MONTHDATE
UNION ALL
SELECT DATEADD(D,1,MONTHDATE) FROM MONTHDATA WHERE MONTHDATE<DATEADD(D,-1,DATEADD(M,1,#STARTDATE))
)
SELECT 'CD'+CAST( (ROW_NUMBER()OVER (ORDER BY MONTHDATE)) AS VARCHAR(100))DATE_RULE,CONVERT(VARCHAR,MONTHDATE,101)MONTHDATE FROM MONTHDATA
OUTPUT
----------------------
DATE_RULE MONTHDATE
----------------------
CD1 03/01/2017
CD2 03/02/2017
CD3 03/03/2017
.
.
.
CD29 03/29/2017
CD30 03/30/2017
CD31 03/31/2017
----------------------

T-SQL between periods gaps

I have some data on my table like:
DAY | QTY | Name
1/1/2010 | 1 | jack
5/1/2010 | 5 | jack
2/1/2010 | 3 | wendy
5/1/2010 | 2 | wendy
my goal is to have a SP requesting a period of time (example: '2010-1-1' to '2010-1-5'), and get no gaps.
Output example:
DAY | QTY | Name
1/1/2010 | 1 | jack
2/1/2010 | 0 | jack
3/1/2010 | 0 | jack
4/1/2010 | 0 | jack
5/1/2010 | 5 | jack
1/1/2010 | 3 | wendy
2/1/2010 | 0 | wendy
3/1/2010 | 0 | wendy
4/1/2010 | 2 | wendy
5/1/2010 | 0 | wendy
Any gaps is filled with 0-
I know that I can create a loop to will solve me the problem, but is very slow.
Does anyone have any ideas how to optimize this?
WITH DateRangeCTE([d]) AS
(
SELECT
CONVERT(DATETIME, '2010-01-01') AS [d]
UNION ALL
SELECT
DATEADD(d, 1, [d]) AS [d]
FROM
DateRangeCTE
WHERE [d] < DATEADD(d, -1, CONVERT(DATETIME, '2010-1-31'))
)
SELECT
DateRangeCTE.d, YourTable.Qty, YourTable.Name
FROM DateRangeCTE
LEFT JOIN YourTable ON DateRangeCTE.d = YourTable.DAY
If you get the error "The statement terminated. The maximum recursion 100 has been exhausted before statement completion." then use the maxrecursion hint.
Here's a solution that you can use if you don't know the date range in advance. It derives the date range based on the data. The solution uses a numbers table, which uses an existing table in the master database (spt_values).
WITH MinMax AS
( SELECT DISTINCT [Name],
MIN([DAY]) OVER () AS min_day, MAX([DAY]) OVER () AS max_day
FROM mytable
)
, DateRange AS
( SELECT MinMax.[Name], DATEADD(mm, n.number, MinMax.min_day) AS [Date]
FROM MinMax
JOIN master.dbo.spt_values n ON n.type = 'P'
AND DATEADD(mm, n.number, MinMax.min_day) <= MinMax.max_day
)
SELECT dr.[Name], COALESCE(mt.[qty], 0) AS [QTY], dr.Date
FROM DateRange dr
LEFT OUTER JOIN MyTable mt ON dr.Name = mt.Name AND mt.Day = dr.Date
ORDER BY dr.Name, dr.Date ;
Here's another way:
DECLARE #output TABLE (
DateValue datetime,
Qty varchar(50),
LastName varchar(25)
PRIMARY KEY (DateValue, LastName)
)
DECLARE #minMonth datetime, #maxMonth datetime, #lastName varchar(25)
-- whatever your business logic dictates for these
SET #minMonth = '01/01/2010'
SET #maxMonth = '12/01/2010';
with cte as (
SELECT #minMonth AS DateValue
UNION ALL
SELECT DATEADD(month, 1, DateValue)
FROM cte
WHERE DATEADD(month, 1, DateValue) <= #maxMonth
)
INSERT INTO #output (DateValue, Qty, LastName)
SELECT cte.DateValue,
ISNULL(tbl.Alias,0),
tbl.Name
FROM cte LEFT JOIN dbo.YourTable tbl ON tbl.[Day] = cte.Mth
UPDATE #output SET
LastName = CASE WHEN LastName IS NULL THEN #lastName ELSE LastName END,
#lastName = LastName
FROM #output
SELECT * FROM #output
I leave were the correct answer based on the help of everyone
-- dummy data
declare #table table
(
DAY datetime,
QTY int,
Name nvarchar (500) NULL
)
insert #table values('2010-1-1', 1, 'jack')
insert #table values('2010-1-3', 5, 'jack')
insert #table values('2010-1-2', 3 , 'wendy')
insert #table values('2010-1-6', 2 , 'wendy')
-- algorithm
DECLARE #output TABLE (
DAY datetime,
Qty int,
Name varchar(25)
)
DECLARE #minMonth datetime, #maxMonth datetime, #lastName varchar(25)
SET #minMonth = '2010-1-1'
SET #maxMonth = '2010-1-6';
WITH cte AS (
SELECT #minMonth AS DateValue
UNION ALL
SELECT DATEADD(day, 1, DateValue)
FROM cte
WHERE DATEADD(day, 1, DateValue) <= #maxMonth
)
INSERT INTO #output
SELECT
cte.DateValue,
ISNULL(tbl.qty,0),
tbl.Name
FROM
cte cross JOIN
#table tbl
update #output
set qty = 0
where cast(DAY as nvarchar)+'#'+cast(Qty as nvarchar)+'#'+Name in
(
select cast(DAY as nvarchar)+'#'+cast(Qty as nvarchar)+'#'+Name from #output
except
select cast(DAY as nvarchar)+'#'+cast(Qty as nvarchar)+'#'+Name from #table
)
SELECT DAY, sum(qty) as qty, Name
FROM #output
GROUP BY DAY, Name
order by 3,1
and the output that I pretend
2010-01-01 00:00:00.000 1 jack
2010-01-02 00:00:00.000 0 jack
2010-01-03 00:00:00.000 5 jack
2010-01-04 00:00:00.000 0 jack
2010-01-05 00:00:00.000 0 jack
2010-01-06 00:00:00.000 0 jack
2010-01-01 00:00:00.000 0 wendy
2010-01-02 00:00:00.000 3 wendy
2010-01-03 00:00:00.000 0 wendy
2010-01-04 00:00:00.000 0 wendy
2010-01-05 00:00:00.000 0 wendy
2010-01-06 00:00:00.000 2 wendy
Although the solution is correct, doesn't fit my need because recursion limitation.
Hopefully this script will help anyone with similar questions
Thank you to all