SQL Query to get the monthly change percentage from daily quotes - sql

I have the below table and I would like to get the Monthly change % from it
I am using the below query for getting the monthly change:
SELECT
ct.Quote_Date, ht.Quote_Date AS htDate, ct.Quote_Price,
ht.Quote_Price AS [htPrice],
((ct.Quote_Price - ht.Quote_Price) / ht.Quote_Price) * 100 AS ChangePerc
FROM
#TempStock ct
LEFT JOIN
#TempStock ht ON CONVERT(DATE, CAST(ct.Quote_Date AS VARCHAR), 101) = DATEADD(MM, 1, CONVERT(DATE, CAST(ht.Quote_Date AS VARCHAR), 101))
ORDER BY
ct.Quote_Date DESC
Result of this query:
Everything working fine except when the ht.Quote_Date is Sunday or Saturday or a holiday for which the record is missing in the table. In this case the available date before the holiday should be considered so that i don't get the NULLs as shown in the result image above.
Could you please let me know the correct query to get the required result ?

I would suggest outer apply:
SELECT ct.Quote_Date, ht.Quote_Date AS htDate, ct.Quote_Price,
ht.Quote_Price AS [htPrice],
((ct.Quote_Price - ht.Quote_Price)/ht.Quote_Price)*100 AS ChangePerc
FROM #TempStock ct OUTER APPLY
(SELECT TOP 1 ht.*
FROM #TempStock ht
WHERE ht.Quote_Date <= DATEADD(month, -1, ct.Quote_Date)
ORDER BY ht.Quoate_Date DESC
) ht
ORDER BY ct.Quote_Date DESC;
Notes:
You should do date arithmetic using dates. There is no reason to cast back and forth to strings.
If the dates have time components, use cast(<col> as date) to get rid of the time component.

OUTER APPLY could help you:
SELECT ct.Quote_Date,
ht.Quote_Date AS htDate,
ct.Quote_Price,
ht.Quote_Price AS [htPrice],
((ct.Quote_Price - ht.Quote_Price)/ht.Quote_Price)*100 AS ChangePerc
FROM #TempStock ct
OUTER APPLY (
SELECT TOP 1 *
FROM #TempStock
WHERE CONVERT(DATE,CAST(ct.Quote_Date AS VARCHAR),101) >= DATEADD(MM,1, CONVERT(DATE,CAST(Quote_Date AS VARCHAR),101))
ORDER BY Quote_Date DESC
) ht
ORDER BY ct.Quote_Date DESC
WHERE clause in OUTER APPLY will bring first record with same or lesser date.

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.

Using the result of a SQL subquery in multiple joins

I have a subquery :-
SELECT TOP 1 Months.EndDate
FROM (SELECT TOP 1 *
FROM FinancialMonth
WHERE FinancialMonth.EndDate > DATEADD(MONTH, -12, GETDATE())
AND FinancialMonth.StartDate < GETDATE()
ORDER BY Period ASC) Months
ORDER BY Months.Period DESC
This returns the Month End Date and works for any number of months ago in the last year simply by changing the second TOP 1.
My problem is that I need to use this date in a number of LEFT JOIN statements where I compare it to two tables. I also need to return it in the final SELECT SUM statement.
By manually inputting the date to the LEFT JOIN queries I can run the main query and have a result back in under 1 second. However if I place this subquery against each LEFT JOIN it can take well over a minute to run. Given that I would like to run this query for each of the last 12 months this is going to tie the server up for an unacceptable amount of time.
Is there any way of running a query and then referencing this result within the LEFT JOIN subqueries without it running over and over. At present it appears to running well over 100k times.
Already i dont understand why you use 2 x top 1 (a top 1 in top 1 give 1 row), you query can be simplify to :
SELECT TOP 1 EndDate
FROM FinancialMonth
WHERE FinancialMonth.EndDate > DATEADD(MONTH, -12, GETDATE())
AND FinancialMonth.StartDate < GETDATE()
ORDER BY Period ASC
Now for what you want you can do Something like that:
with TblEndDate as (
SELECT TOP 1 EndDate
FROM FinancialMonth
WHERE FinancialMonth.EndDate > DATEADD(MONTH, -12, GETDATE())
AND FinancialMonth.StartDate < GETDATE()
ORDER BY Period ASC
)
select * from othertable f1
left outer join TblEndDate f2 on f1.DateInOthertable>=f2.EndDate

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

T SQL Datepart and Count - Show also weeks with zero count

I have following SQL statement:
declare #dateFrom datetime = '2015-01-01';
declare #dateTo datetime = '2015-12-31';
select
DATEPART(WEEK, OrderDate) Week, Count(*) Number
from
table
where
OrderDate between #dateFrom and #dateTo
group by
DATEPART(WEEK, OrderDate)
order by
Week
It returns the number of orders per week, but if there were no orders at all this respective week is omitted.
How can I change the statement so it will also include weeks with 0 orders?
Gofr1 was on the right track but there are issues with the query.
1 - You do not want to use the datediff() of the begin and end as the stopping condition. It works for a whole year but will not work for partial ranges.
2 - I would add year to the key since that will allow you to handle cross year cases.
3 - You need to roll up the sales before using the Year Week Common Table Expression. Otherwise you just toss out the nulls again (order dates) with the WHERE clause.
Remember, logically the join is applied then the where clause.
The code below uses the Adventure Works 2012 DW database and obtains the correct answer.
Uses a tally table for some numbers.
Generates weekly dates and calculates year/week key for given range.
Rolls up sales from the fact table for given range.
Left joins the keys to the sales and turns null totals to zero.
Code:
-- Declare start and end date
DECLARE #dte_From datetime = '2005-07-01';
DECLARE #dte_To datetime = '2007-12-31';
-- About 200K numbers
WITH cte_Tally (n) as
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM sys.all_views a
CROSS JOIN sys.all_views b
),
-- Create year/week key
cte_YearWeekKey (MyKey) as
(
SELECT
year(dateadd(week, t.n, #dte_from)) * 1000 +
datepart(week, dateadd(week, t.n, #dte_from)) as MyKey
FROM
cte_Tally as t
WHERE
dateadd(week, t.n, #dte_from) < #dte_To
),
-- Must roll up here
cte_Sales (MyKey, MyTotal) as
(
SELECT
YEAR(F.OrderDate) * 1000 +
DATEPART(WEEK, F.OrderDate) as MyKey,
COUNT(*) as MyTotal
FROM
[AdventureWorksDW2012].[dbo].[FactResellerSales] F
WHERE
F.OrderDate between #dte_From and #dte_To
GROUP BY
YEAR(F.OrderDate) * 1000 +
DATEPART(WEEK, F.OrderDate)
)
-- Join the results
SELECT
K.MyKey, ISNULL(S.MyTotal, 0) as Total
FROM
cte_YearWeekKey as K
LEFT JOIN
cte_Sales as S
ON
k.MyKey = S.MyKey

SQL adding missing dates to query

I'm trying to add missing dates to a SQL query but it does not work.
Please can you tell me what I'm doing wrong.
I only have read only rights to database.
SQL query:
With cteDateGen AS
(
SELECT 0 as Offset, CAST(DATEADD(dd, 0, '2015-11-01') AS DATE) AS WorkDate
UNION ALL
SELECT Offset + 1, CAST(DATEADD(dd, Offset, '2015-11-05') AS DATE)
FROM cteDateGen
WHERE Offset < 100
), -- generate date from to --
cte AS (
SELECT COUNT(*) OVER() AS 'total' ,ROW_NUMBER()OVER (ORDER BY c.dt DESC) as row
, c.*
FROM clockL c
RIGHT JOIN cteDateGen d ON CAST(c.dt AS DATE) = d.WorkDate
WHERE
c.dt between '2015-11-01' AND '2015-11-05' and
--d.WorkDate BETWEEN '2015-11-01' AND '2015-11-05'
and c.id =10
) -- select user log and add missing dates --
SELECT *
FROM cte
--WHERE row BETWEEN 0 AND 15
--option (maxrecursion 0)
I think your problem is simply the dates in the CTE. You can also simplify it a bit:
With cteDateGen AS (
SELECT 0 as Offset, CAST('2015-11-01' AS DATE) AS WorkDate
UNION ALL
SELECT Offset + 1, DATEADD(day, 1, WorkDate) AS DATE)
-----------------------------------^
FROM cteDateGen
WHERE Offset < 100
), -- generate date from to --
cte AS
(SELECT COUNT(*) OVER () AS total,
ROW_NUMBER() OVER (ORDER BY c.dt DESC) as row,
c.*
FROM cteDateGen d LEFT JOIN
clockL c
ON CAST(c.dt AS DATE) = d.WorkDate AND c.id = 10
-----------------------------------------------^
WHERE d.WorkDate between '2015-11-01' AND '2015-11-05'
) -- select user log and add missing dates --
SELECT *
FROM cte
Notes:
Your query used a constant for the second date in the CTE. The constant was different from the first constant. Hence, it was missing some days.
I think that LEFT JOIN is much easier to follow than RIGHT JOIN. LEFT JOIN is basically "keep all rows in the first table".
The WHERE clause was undoing the outer join in any case. The c.id logic needs to move to the ON clause.
The date arithmetic in the first CTE was unnecessarily complex.