CTE and last known date processing - sql

Input
#StartDate = '01/25/2010'
#EndDate = '02/06/2010'
I have 2 CTEs in a stored procedure as follows:
with CTE_A as
(
[gives output A..Shown below]
),
with CTE_B as
(
Here,
I want to check if #StartDate is NOT in output A then replace it with the last known date. In this case, since #startdate is less than any date in output A hence #StartDate will become 02/01/2010.
Also to check if #EndDate is NOT in output A then replace it with the last known date. In this case, since #enddate is 02/06/2010 hence it will be replace with 02/05/2010.
// Here there is a query using #startDate and #EndDate.
)
output A
Name Date
A 02/01/2010
B 02/01/2010
C 02/05/2010
D 02/10/2010

You don't need a 2nd CTE (untested)
...
SELECT
StartDate, EndDate
FROM
(
SELECT TOP 1
A.Date AS StartDate
FROM
CTEA A
WHERE
A.[Date] >= #StartDate
ORDER BY
A.Date
) Bmin
CROSS JOIN
(
SELECT TOP 1
A.Date AS EndDate
FROM
CTEA A
WHERE
A.[Date] <= #EndDate
ORDER BY
A.Date DESC
) Bmax
You could use MAX/MIN too

Related

Join Generated Date Sequence

Currently I'm trying to join a date table to a ledger table so I can fill the gaps of the ledger table whenever there are no transactions in certain instances (e.g. there are transactions on March 1st and in March 3rd, but no transaction in March 2nd. And by joining both tables March 2nd would appear in the ledger table but with 0 for the variable we're analyzing.)
The challenge is that I can't create a Date object/table/dimension because I don't have permissions to create tables in the database. Therefore I've been generating a date sequence with this code:
DECLARE #startDate date = CAST('2016-01-01' AS date),
#endDate date = CAST(GETDATE() AS date);
SELECT DATEADD(day, number - 1, #startDate) AS [Date]
FROM (
SELECT ROW_NUMBER() OVER (
ORDER BY n.object_id
)
FROM sys.all_objects n
) S(number)
WHERE number <= DATEDIFF(day, #startDate, #endDate) + 1;
So, is there the possibility to join both tables into the same statement? Let's say the ledger table looks like this:
SELECT
date,cost
FROM ledger
I'd assume it can be done by using a subquery but I don't know how.
Thank you.
There is a very good article by Aaron Bertrand showing several methods for generating a sequence of numbers (or dates) in SQL Server: Generate a set or sequence without loops – part 1.
Try them out and see for yourself which is faster or more convenient to you. (spoiler - Recursive CTE is rather slow)
Once you've picked your preferred method you can wrap it in a CTE (common-table expression).
Here I'll use your method from the question
WITH
CTE_Dates
AS
(
SELECT
DATEADD(day, number - 1, #startDate) AS dt
FROM (
SELECT ROW_NUMBER() OVER (
ORDER BY n.object_id
)
FROM sys.all_objects n
) S(number)
WHERE number <= DATEDIFF(day, #startDate, #endDate) + 1
)
SELECT
...
FROM
CTE_Dates
LEFT JOIN Ledger ON Ledger.dt = CTE_Dates.dt
;
You can use your generated date sequence as a CTE and LEFT JOIN that to your ledger table. For example:
DECLARE #startDate date = CAST('2020-02-01' AS date);
DECLARE #endDate date = CAST(GETDATE() AS date);
WITH dates AS (
SELECT DATEADD(day, number - 1, #startDate) AS [Date]
FROM (
SELECT ROW_NUMBER() OVER (
ORDER BY n.object_id
)
FROM sys.all_objects n
) S(number)
WHERE number <= DATEDIFF(day, #startDate, #endDate) + 1
)
SELECT dates.Date, COALESCE(ledger.cost, 0)
FROM dates
LEFT JOIN (VALUES ('2020-02-02', 14), ('2020-02-05', 10)) AS ledger([Date], [cost]) ON dates.Date = ledger.Date
Output:
Date cost
2020-02-01 0
2020-02-02 14
2020-02-03 0
2020-02-04 0
2020-02-05 10
2020-02-06 0
Demo on dbfiddle

Generate data between two range date by some values

I have 2 dates, StartDate and EndDate:
Declare #StartDate date='2018/01/01', #Enddate date ='2018/12/31'
Then there is some data with a date and value in a mytable table:
----------------------------
ID date value
----------------------------
1 2018/02/14 4
2 2018/09/26 7
3 2017/09/20 2
data maybe start before 2018 and if it exist before #startdate get before values
else get 0
I'm looking to get a result that looks like this:
-----------------------------------
fromdate todate value
-----------------------------------
2018/01/01 2018/02/13 2
2018/02/14 2018/09/25 4
2018/09/26 2018/12/31 7
The first fromdate comes from #StartDate and the last todate is from #Enddate, and the other data should be generated.
I'm hoping to get this in an SQL query. I use sql-server 2016
You could use a CTE to create your full range of dates, and then LEAD to create the ToDate column:
DECLARE #FromDate date = '20180101',
#ToDate date = '20181231';
WITH VTE AS(
SELECT ID,
CONVERT(date,[date]) [date], --This is why using keywords for column names is a bad idea
[value]
FROM (VALUES(1,'20180214',4),
(2,'20180926',7),
(3,'20170314',4))V(ID,[date],[value])),
Dates AS(
SELECT [date]
FROM VTE V
WHERE V.[date] BETWEEN #FromDate and #ToDate
UNION ALL
SELECT [date]
FROM (VALUES(#FromDate))V([date]))
SELECT D.[date] AS FromDate,
LEAD(DATEADD(DAY, -1,D.[date]),1,#ToDate) OVER (ORDER BY D.[date]) AS ToDate,
ISNULL(V.[value],0) AS [value]
FROM Dates D
LEFT JOIN VTE V ON D.[date] = V.[date];
db<>fiddle
with cte as
(
select 0 as row_num, #StartDate as start_date, 0 as val
UNION
select ROW_NUMBER() OVER(ORDER BY start_date) as row_num, * from input
)
select curr.start_date
, DATEADD(day,-1,ISNULL(nex.start_date,DATEADD(day,1,#Enddate))) as end_date
, curr.val
from cte curr
left join cte nex on curr.row_num = nex.row_num - 1;
You can find the simulation here: https://rextester.com/EIAXW23839

return a result even if there is no data for that day/ week/ month

I have an MSSQL database and I want to get a value for each day/ week/ month in separate queries.
I got this working just fine except for intervals where there is no data, it wont return anything. And since im putting this in a graph, I want it to display a 0 or a NULL at least instead of jumping days or weeks etc.
I dont know if it will be different for each query but here is my daily query:
select CAST(Placements.CreatedOn AS DATE) AS
date,SUM(Placements.CommissionPerc * (Placements.PlacementFee / 100)) AS value
from [placements]
where [Placements].[CreatedOn] >= '2018-06-07' and [Placements].[CreatedOn] < '2018-06-12'
group by CAST(Placements.CreatedOn AS DATE)
order by CAST(Placements.CreatedOn AS DATE) ASC
This returns a result like:
So it returns 0 for when the data is actually 0 but when its missing, theres nothing like for days 9, 10 and 12
How can i fix this? thanks
Using a recursive CTE you can generate a list of dates.
Which can then be used to LEFT JOIN your table.
Example:
WITH DATES2018 AS
(
SELECT CAST('2018-01-01' AS DATE) AS [date]
UNION ALL
SELECT DATEADD(day, 1, [date])
FROM DATES2018
WHERE [date] < CAST('2018-12-31' AS DATE)
)
SELECT
d.[Date],
SUM(p.CommissionPerc * (p.PlacementFee / 100.0)) AS [value]
FROM DATES2018 AS d
LEFT JOIN [Placements] AS p ON CAST(p.CreatedOn AS DATE) = d.[Date]
WHERE d.[Date] BETWEEN '2018-06-07' AND '2018-06-11'
GROUP BY d.[Date]
ORDER BY d.[Date] ASC
OPTION (MAXRECURSION 366)
But you could also just add a new permanent table with all dates.
And use that table to left join your table.
Btw, if variables are used for the start and end date then that SQL can be optimized.
DECLARE #StartDate DATE = '2018-06-07';
DECLARE #EndDate DATE = '2018-06-11';
WITH DATES AS
(
SELECT #StartDate AS [date]
UNION ALL
SELECT DATEADD(day, 1, [date])
FROM DATES
WHERE [date] < #EndDate
)
SELECT
d.[Date],
SUM(p.CommissionPerc * (p.PlacementFee / 100.0)) AS [value]
FROM DATES AS d
LEFT JOIN [Placements] AS p
ON p.CreatedOn BETWEEN CAST(#StartDate AS DATETIME) AND CAST(DATEADD(day, 1, #EndDate) AS DATETIME) AND
CAST(p.CreatedOn AS DATE) = d.[Date]
GROUP BY d.[Date]
ORDER BY d.[Date] ASC
OPTION (MAXRECURSION 0)
A permanent calendar table would be best but here's and example that uses a CTE to create the dates needed for a LEFT JOIN. This uses a maximum of 1,000 days but can be extended as needed.
DECLARE
#StartDate date = '2018-06-07'
, #EndDate date = '2018-06-12';
WITH
t10 AS (SELECT n FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) t(n))
,t1k AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) - 1 AS num FROM t10 AS a CROSS JOIN t10 AS b CROSS JOIN t10 AS c)
,calendar AS (SELECT DATEADD(day, num, #StartDate) AS calendar_date
FROM t1k
WHERE num <= DATEDIFF(day, #StartDate, #EndDate)
)
SELECT
calendar.calendar_date AS date
, SUM( COALESCE(Placements.CommissionPerc * (Placements.PlacementFee / 100),0 ) ) AS value
FROM calendar
LEFT JOIN [placements] ON [Placements].[CreatedOn] = calendar.calendar_date
GROUP BY calendar.calendar_date
ORDER BY calendar.calendar_date ASC;

SQL Cross Join getting all dates between date range

I have a table with the following structure:
ID: StartDate: EndDate
I want to show all dates in the date range for each ID.
Eg
ID = 1: StartDate = 01/01/2018: EndDate = 03/01/2018
ID: 1 01/01/2018
ID: 1 02/01/2018
ID: 1 03/01/2018
I think i need to use a cross join but im unsure how to create this for multiple rows?
Here is the CTE for SQL Server, the syntax is somewhat different:
declare #startdate date = '2018-01-01';
declare #enddate date = '2018-03-18';
with
dates as (
select #startdate as [date]
union all
select dateadd(dd, 1, [date]) from dates where [date] < #enddate
)
select [date] from dates
So i ended up using a date table and just cross referencing that
select *
from Date d
inner join WorkingTable w
on d.Date >= w.StartDate
and d.date < w.EndDate
In standard SQL you can use a recursive CTE:
with recursive dates as (
select date '2018-01-01' as dte
union all
select dte + interval '1 day'
from dates
where dte < date '2018-01-03'
)
select dte
from dates;
The exact syntax (whether recursive is needed and date functions) differ among databases. Not all databases support this standard functionality.
Now got this for only one id..,
create table #dateTable(id int, col1 date, col2 date)
insert into #dateTable values(1,'05-May-2018','08-May-2018') ,(2,'05-May-2018','05-May-2018')
select *from #dateTable
with cte(start, ends) as(
select start = (select top 1 col1 from #dateTable), ends = (select top 1 col2 from #dateTable)
union all
select DATEADD(dd,1,start),ends from cte where start <> ends
)select start from cte option (maxrecursion 10)
I'm still working... I update soon...!

Filling empty dates with stored procedure in sql

I have found similar questions that have been answered but I can't seem to get it working. I have the following SQL query but I want to fill the missing dates with 0-values
SELECT
Lines.Item,
CAST(Lines.Date AS Date) AS SalesDate,
ABS(SUM(Lines.Invoiced)) AS QtySoldOnDate
FROM
Lines
WHERE
Lines.Invoiced < 0
AND Lines.Item = 'a158wa'
AND Lines.Date >= '2014-01-01'
AND Lines.Date <= '2014-12-31'
GROUP BY
Lines.Item, Lines.Date
I have found the following post, but I can't seem to get it working/figure out how to merge the two queries: What is the most straightforward way to pad empty dates in sql results (on either mysql or perl end)?
Any help would be greatly appreciated.
The easiest way is to get a list of 365 values for forming the dates. One way is with master..spt_values, something like this:
with dates as (
select dateadd(day, row_number() over (order by (select null)),
cast('2014-01-01' as date)) as thedate
from master..spt_values
)
SELECT l.Item, d.thedate AS SalesDate,
ABS(SUM(l.Invoiced)) AS QtySoldOnDate
FROM dates d left join
Lines l
ON l.Date = d.thedate and
l.Invoiced < 0 AND
l.Item = 'a158wa'
WHERE d.thedate BETWEEN '2014-01-01' AND Lines.Date <= '2014-12-31'
GROUP BY l.Item, d.theDate;
Note: You can also read the Number column directly from master..spt_values if you use type = 'P'. I'm likely to forget the type part, so I just used row_number(). Perhaps Microsoft could add a view called Numbers that did this for us.
You can Create storedprocedure with 2 input parameters and 1 output parameters
and you can check if row exists then do your job and in the else part you can set output parameter as o-values
CREATE PROCEDURE yourprocedure
#startDate Date,
#endDate Date,
#OutputPara as nvarchar(100) output
AS
BEGIN
IF EXISTS( Select Lines.Item FROM Lines [Lines.Date] Where [Lines.Date] >= #startDate AND [Lines.Date] <= #endDate)
BEGIN
SELECT
Lines.Item,
COALESCE(CAST(Lines.Date AS Date),'0') AS SalesDate,
ABS(SUM(Lines.Invoiced)) AS QtySoldOnDate
FROM
Lines
WHERE
Lines.Invoiced < 0
AND Lines.Item = 'a158wa'
AND [Lines.Date] >= #startDate
AND [Lines.Date] <= #endDate
GROUP BY
Lines.Item, [Lines.Date]
Set #OutputPara =' result exists'
END
ELSE
BEGIN
SET #OutputPara='0-results';
END
END
GO
I'm assuming you want the days where nothing is sold, to have its own row with 0 as it's value. This should do it perhaps with a few tweaks from you since I don't have your table or any data.
DECLARE #startDate DATE,
#endDate DATE;
SET #startDate = '2014-01-01';
SET #endDate = '2014-12-31';
--Generates table of each day in range
WITH cte_dates
AS
(
SELECT #startDate AS startDate
UNION ALL
SELECT DATEADD(DAY,1,startDate)
FROM cte_dates
WHERE startDate <= #endDate
)
SELECT cte_dates.startDate,
Lines.Item,
CAST([Lines.Date] AS Date) AS SalesDate,
ISNULL(ABS(SUM(Lines.Invoiced)),0) AS QtySoldOnDate
FROM cte_dates
--Left join makes it where if there is no date in Lines, then cte_dates will be there with nulls for columns in your table Lines
LEFT JOIN Lines
ON cte_dates.startDate = Lines.[Date]
WHERE
Lines.Invoiced < 0
AND Lines.Item = 'a158wa'
AND Lines.Date BETWEEN #startDate AND #endDate
GROUP BY Lines.Item,Lines.[Date],cte_dates.startDate
--It's a recursive CTE. This allows it recursively iterate enough times to generate the list of dates
OPTION (MAXRECURSION 0)
Theoretical results:
StartDate Item SalesDate QtySOldOnDate
---------------------------------------------------
2014-01-01 Item1 2014-01-01 500
2014-01-02 NULL NULL 0
2014-01-03 Item2 2014-01-03 250