Nest With clause inside a select statement - sql

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

Related

Query to pick value depending on date

I have a table with exchange rates which update only when a new exchange rate comes, that is, the only the date that the new rate entered is recorded. however the system has logic to say if any date fall within a particular date, it picks the corresponding exchange rate
i would like to have a query which picks the required exchange rate given any date supplied, i.e., pick the rate from the period.
WITH ListDates(AllDates) AS
( SELECT cast('2015-11-01' as date) AS DATE
UNION ALL
SELECT DATEADD(DAY,1,AllDates)
FROM ListDates
WHERE AllDates < getdate())
SELECT ld.AllDates,cr.effective_from,cr.rate_against_base
FROM ListDates ld
left join CurrencyRatetable cr on cr.effective_from between cr.effective_from and ld.alldates
option (maxrecursion 0)
I guess you might want to achieve the required result using the window function LEAD. Following an example:
DECLARE #t TABLE(effective_from date, rate_against_base decimal(19,4))
INSERT INTO #t VALUES
('2000-01-01', 1.6)
,('2016-10-26', 1)
,('2020-07-13', 65.8765);
DECLARE #searchDate DATE = '2023-01-17';
WITH cte AS(
SELECT effective_from
,ISNULL(LEAD(effective_from) OVER (ORDER BY effective_from), CAST('2049-12-31' AS DATE)) AS effective_to
,rate_against_base
FROM #t
)
SELECT rate_against_base
FROM cte
WHERE #searchDate >= effective_from
AND #searchDate < effective_to
You can use a CROSS APPLY or OUTER APPLY together with a TOP 1 subselect.
Something like:
WITH ListDates(AllDates) AS (
SELECT cast('2015-11-01' as date) AS DATE
UNION ALL
SELECT DATEADD(DAY,1,AllDates)
FROM ListDates
WHERE AllDates < getdate()
)
SELECT ld.AllDates, cr.effective_from, cr.rate_against_base
FROM ListDates ld
OUTER APPLY (
SELECT TOP 1 *
FROM CurrencyRatetable cr
WHERE cr.effective_from <= ld.alldates
ORDER BY cr.effective_from DESC
) cr
ORDER BY ld.AllDates
option (maxrecursion 0)
Both CROSS APPLY or OUTER APPLY are like a join to a subselect. The difference is that CROSS APPLY is like an inner join and OUTER APPLY is like a left join.
Make sure that CurrencyRatetable has an index on effective_from for efficient access.
See this db<>fiddle.

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

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....)

How To Generate Gaps For Missing Data

I have data that is structured as follows:
Using the following CTE:
WITH DateRange AS
(
SELECT CAST('2012-01-24 06:00' AS DATETIME) DateValue
UNION ALL
SELECT DATEADD(mi, 1, DateValue)
FROM DateRange
WHERE DATEADD(mi, 1, DateValue) <= '2012-01-24 12:00'
)
SELECT DateValue FROM DateRange
OPTION (MAXRECURSION 0)
I have created a "Date Set" that look like this:
The Issue Is: There is some data in the original data-set that does not have a corresponding date in the CTE (In the provided example, 2012-01-24 6:00 is not present in the data).
For these missing data points, I would like to see "NULL" as their value. I thought I might be able to leverage the dates in the CTE I generated, but I'm not sure how I can do this.
I have tried something like this with no success:
SELECT C.DateValue, D.Value
FROM myCTE C
LEFT OUTER JOIN myData D ON C.DateValue = D.Date
ORDER BY C.DateValue ASC;
I can't replicate the issue you're seeing. I've created the query below and tested it and it works fine - can you test this in your environment?
Declare #var as Table (DateValue datetime)
Insert Into #var (DateValue) Values ('2012-01-24 06:01:00.000')
Insert Into #var (DateValue) Values ('2012-01-24 06:02:00.000')
Insert Into #var (DateValue) Values ('2012-01-24 06:03:00.000')
;WITH DateRange AS (
SELECT CAST('2012-01-24 06:00' AS DATETIME) DateValue
UNION ALL
SELECT DATEADD(mi, 1, DateValue)
FROM DateRange
WHERE DATEADD(mi, 1, DateValue) <= '2012-01-24 12:00'
)
SELECT dr.DateValue, v.DateValue
FROM DateRange dr
Left Outer Join #var v On dr.DateValue = v.DateValue
Order By v.DateValue
OPTION (MAXRECURSION 0)
You want a RIGHT OUTER JOIN, not a LEFT OUTER JOIN. The left join has nulls where the right-hand side is missing data; the left-hand side here is myCTE.

Can I use recursion in a Sql Server 2005 View?

I tried to use OPTION (MAXRECURSION 0) in a view to generate a list of dates.
This seems to be unsupported. Is there a workaround for this issue?
EDIT to Explain what I actually want to do:
I have 2 tables.
table1: int weekday, bool available
table2: datetime date, bool available
I want the result:
view1: date (here all days in this year), available(from table2 or from table1 when not in table2).
That means I have to apply a join on a date with a weekday.
I hope this explanation is understandable, because I actually use more tables with more fields in the query.
I found this code to generate the recursion:
WITH Dates AS
(
SELECT cast('2008-01-01' as datetime) Date
UNION ALL
SELECT Date + 1
FROM Dates
WHERE Date + 1 < DATEADD(yy, 1, GETDATE())
)
No - if you can find a way to do it within 100 levels of recusion (have a table of numbers), which will get you to within 100 recursion levels, you'll be able to do it. But if you have a numbers or pivot table, you won't need the recursion anyway...
See this question (but I would create a table and not a table-valued function), this question and this link and this link
You can use a CTE for hierarchical queries.
Here you go:
;WITH CTE_Stack(IsPartOfRecursion, Depth, MyDate) AS
(
SELECT
0 AS IsPartOfRecursion
,0 AS Dept
,DATEADD(DAY, -1, CAST('01.01.2012' as datetime)) AS MyDate
UNION ALL
SELECT
1 AS IsPartOfRecursion
,Parent.Depth + 1 AS Depth
--,DATEADD(DAY, 1, Parent.MyDate) AS MyDate
,DATEADD(DAY, 1, Parent.MyDate) AS MyDate
FROM
(
SELECT 0 AS Nothing
) AS TranquillizeSyntaxCheckBecauseWeDontHaveAtable
INNER JOIN CTE_Stack AS Parent
--ON Parent.Depth < 2005
ON DATEADD(DAY, 1, Parent.MyDate) < DATEADD(YEAR, 1, CAST('01.01.2012' as datetime))
)
SELECT * FROM CTE_Stack
WHERE IsPartOfRecursion = 1
OPTION (MAXRECURSION 367) -- Accounting for leap-years
;