Auto generating dates based on a table - sql

I have a table in SQL server
And I want to generate each presentation day between StartDate and EndDate. Normally, I have to create a script, declare a cursor and loop through the cursor to create each individual date. But using cursor slows thing down considerably.
I wonder if anyone has a better idea using join
I am successful in generating date based on a startdate and enddate
SELECT d."CalendarDay" AS "PresenttionDate",
DATEPART(dw,d."CalendarDay") AS "PresentationDay"
FROM
(
SELECT StartDate-1+number AS "CalendarDay"
FROM master..spt_values
where type='P' and number<= DateDiff(day,StartDate,EndDate)
)d
I just do not know how to tie the StartDate and EndDate to the presentation table.
Basically, I am looking for the end results below:
without involving cursor. Is that possible?
Please advise.

I think this is sufficient:
with n as (
select row_number() over (order by (select null)) - 1 as n
from master..spt_values
)
select t.*, dateadd(day, n.n, t.startDate) as thedate
from t join
n
on dateadd(day, n.n, t.startDate) <= t.endDate;

Related

SQL temp date table generation

I wonder is there a way to generate a temp table containing dates but using between, because I have to use such a construction.
between Convert(datetime, '2022-01-01T00:00:00.000', 126) and Convert(datetime, '2022-03-04T23:59:59.998', 126)
I mean it should use between not StartDate,EndDate.
Another option which I think performs better than a recursive CTE
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT 0 AS I
UNION ALL
SELECT TOP (DATEDIFF(DAY, '20220101', '20220304'))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3),
Dates AS(
SELECT DATEADD(DAY, T.I, '20220101') AS Date
FROM Tally T)
SELECT D.Date
into #tmpDates
FROM Dates D
EDIT
I always have a calendar table in my database, so I can just join on that. This performs quite well and the queries are much easier
An option for a getting the temp table. Not 100% it will do what's required. Hope this helps though.
DECLARE #start_date date = '2022-01-01',
#end_date date = '2022-03-04'
;WITH cte AS (
SELECT #start_date as DateRet
UNION ALL
SELECT CAST(DATEADD(day,1,dateRet) as date)
FROM cte
WHERE dateret < #end_date
)
SELECT *
into #tmpDates
FROM cte

How to get daily open inventory with a dynamic end date without storing results in an aggregate table?

I'm looking to see if there is a way to get the total daily inventory for open items in the past few months. Basically, each record has a start date and an end date. The start date is always the same. The end date will be null until it has been processed. Once processed, it is updated with a process date. Getting one day is fine, but I need to get the total volume, everyday, for a the last few months.
My current method of doing this is putting the results in an aggregate table. I can run the results one time through a while loop, then each day run whatever open volume there is from a stored procedure. This method works, but seems messy.
DECLARE #D AS DATE = '04/01/2019'
WHILE #D <= CAST(GETDATE() AS DATE)
BEGIN
INSERT INTO DBO.OPEN_INVENTORY
SELECT
#D OPEN_DATE
,COUNT(1) OPEN_VOLUME
FROM
DBO.INVENTORY_RECORDS
WHERE
#D BETWEEN START_DATE AND ISNULL(END_DATE,'12/31/2199')
SET #D = DATEADD(D,+1,#D)
END
I would like to reproduce these results without having to store the volumes into an aggregate table. Is there a way to accomplish this in a single select?
Yes, the best way would be to use what's known as a "Tally Table". They are extremely quick are building large sets of sequential data, and unlike a WHILE, CURSOR or rCTE, aren't recursive.
This is a big of a stab in the dark, as I have no sample data, but I think this is what you're after.
DECLARE #D AS DATE = '20190104';
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
FROM N N1, N N2, N N3), --1000 rows should be enough?
Dates AS(
SELECT DATEADD(DAY, T.I, #D) AS CalendarDate
FROM Tally T
WHERE DATEADD(DAY, T.I, #D) <= GETDATE())
SELECT D.CalendarDate,
COUNT(IR.YourIDColumn) AS OPEN_VOLUMNE
FROM Dates D
LEFT JOIN DBO.INVENTORY_RECORDS IR ON D.Date >= IR.START_DATE
AND (D.Date <= IR.END_DATE OR IR.END_DATE IS NULL)
GROUP BY D.CalendarDate;
If not, try to troubleshoot it yourself, and then supply sample and expected results if not.

adding a row for missing data

Between a date range 2017-02-01 - 2017-02-10, i'm calculating a running balance.
I have days where we have missing data, how would I include these missing dates with the previous days balance ?
Example data:
we are missing data for 2017-02-04,2017-02-05 and 2017-02-06, how would i add a row in the query with the previous balance?
The date range is a parameter, so could change....
Can i use something like the lag function?
I would be inclined to use a recursive CTE and then fill in the values. Here is one approach using outer apply:
with dates as (
select mind as dte, mind, maxd
from (select min(date) as mind, max(date) as maxd from t) t
union all
select dateadd(day, 1, dte), mind, maxd
from dates
where dte < maxd
)
select d.dte, t.balance
from dates d outer apply
(select top 1 t.*
from t
where t.date <= d.dte
order by t.date desc
) t;
You can generate dates using tally table as below:
Declare #d1 date ='2017-02-01'
Declare #d2 date ='2017-02-10'
;with cte_dates as (
Select top (datediff(D, #d1, #d2)+1) Dates = Dateadd(day, Row_Number() over (order by (Select NULL))-1, #d1) from
master..spt_values s1, master..spt_values s2
)
Select * from cte_dates left join ....
And do left join to your table and get running total
Adding to the date range & CTE solutions, I have created Date Dimension tables in numerous databases where I just left join to them.
There are free scripts online to create date dimension tables for SQL Server. I highly recommend them. Plus, it makes aggregation by other time periods much more efficient (e.g. Quarter, Months, Year, etc....)

Nest With clause inside a select statement

I have a recursive query using Common Table Expressions which gets the range of dates between a start and end date
WITH T(date) AS (
SELECT #StartDate UNION ALL
SELECT DateAdd(day,1,T.date) FROM T WHERE datediff(dd,T.date , #EndDate)>0 )
SELECT date FROM T OPTION (MAXRECURSION 32767))
Is there any way for me to nest this within another select statement without creating a temporary table?
I'm looking for a statement like so
select * from (WITH T(date) AS (
SELECT #StartDate UNION ALL
SELECT DateAdd(day,1,T.date) FROM T WHERE datediff(dd,T.date , #EndDate)>0 )
SELECT date FROM T OPTION (MAXRECURSION 32767)))
join
(select * from SomeTable where MyDate between #StartDate and #EndDate)
on //Some condition
I've tried this out in SQL Server and there is an
Incorrect Syntax near WITH
error being thrown.
By definition, CTE only exists within the scope of the query. So, is it necessary that a Temporary table is necessary to store the results of the CTE or can the above scenario also work?
You can use multiple CTEs by separating them with a comma, e.g:
WITH T(date) AS
(
SELECT #StartDate
UNION ALL
SELECT DateAdd(day,1,T.date)
FROM T
WHERE datediff(dd,T.date , #EndDate)>0
), T2 AS
(
SELECT date
FROM T
OPTION (MAXRECURSION 32767)
)
select * from t2
join
(select * from SomeTable where MyDate between #StartDate and #EndDate)
on //Some condition
For what it's worth though, using a recursive CTE to generate a list of dates is not the best way. The best way is to have a static calendar table, failing this you can generate a set of dates on the fly as follows:
SELECT TOP (DATEDIFF(DAY, #StartDate, #EndDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, #StartDate)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b;
This will be more efficient than looping through dates. For more information see:
Generate a set or sequence without loops – part 1
Generate a set or sequence without loops – part 2
Generate a set or sequence without loops – part 3
Create the CTE first, then select from it.
;WITH T(date) AS (
SELECT #StartDate UNION ALL
SELECT DateAdd(day,1,T.date) FROM T WHERE datediff(dd,T.date , #EndDate)>0
)
select * from T
join
(select * from SomeTable where MyDate between #StartDate and #EndDate) v
on //Some condition
OPTION (MAXRECURSION 32767)
If you're trying to create a date range, this may help : How to create a list of dates from a daterange without using a CTE

Multiple Joins on Temporary Table OR Subquery in SQL 2008

I have the following SQL which gets a season for each day in a range of dates, then groups each season by start and end date with number of nights. What it does is not important but my question is which is better, the way I've done it below or use the first select statement as a subquery each time #dateSeasons is used in the second query. Both ways seem to run the same but this way looks neater.
DECLARE #dateSeasons TABLE ([date] date, seasonID int)
INSERT INTO #dateSeasons
SELECT D.[date], S.ID
FROM #dates AS D
CROSS APPLY (
SELECT TOP 1 ID
FROM dbo.Seasons
WHERE bookingID = #bookingID
AND D.[date] BETWEEN startDate AND endDate
ORDER BY ID DESC
) AS S
SELECT MIN([date]), endDate, DATEDIFF(DAY, MIN([date]), DATEADD(DAY, 1, endDate)), seasonID
FROM (
SELECT S1.seasonID, S1.[date], (
SELECT MAX([date])
FROM #dateSeasons S2
WHERE S2.seasonID = S1.seasonID
AND NOT EXISTS (
SELECT NULL
FROM #dateSeasons S3
WHERE S3.[date] < S2.[date]
AND S3.[date] > S1.[date]
AND S3.seasonID <> S1.seasonID
)
) AS endDate
FROM #dateSeasons S1
) AS results
GROUP BY endDate, seasonID
ORDER BY MIN([date])
Looking neater is irrelevant in writing SQL Code. What looks elegant is often the worst possible way to solve the problem from a performance standpoint.
The only way to know for sure which is best is to first make sure both ways you are testing return the same results and then performance test them and check out the execution plans (or explain in mySQL). Techniques which make the query better are database specific as well. What works best to performance tune in SQL Server might be the worst possibility in Oracle.
Sometimes you can get better performance by using a common table expression (CTE):
WITH
dateSeasons ([date], [seasonID])
AS
(
SELECT D.[date], S.ID
FROM #dates AS D
CROSS APPLY (
SELECT TOP 1 ID
FROM dbo.Seasons
WHERE bookingID = #bookingID
AND D.[date] BETWEEN startDate AND endDate
ORDER BY ID DESC
) AS S
)
SELECT MIN([date]), endDate, DATEDIFF(DAY, MIN([date]), DATEADD(DAY, 1, endDate)), seasonID
FROM (
SELECT S1.seasonID, S1.[date], (
SELECT MAX([date])
FROM dateSeasons S2
WHERE S2.seasonID = S1.seasonID
AND NOT EXISTS (
SELECT NULL
FROM dateSeasons S3
WHERE S3.[date] < S2.[date]
AND S3.[date] > S1.[date]
AND S3.seasonID <> S1.seasonID
)
) AS endDate
FROM dateSeasons S1
) AS results
GROUP BY endDate, seasonID
ORDER BY MIN([date])