Using the result of a SQL subquery in multiple joins - sql

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

Related

How can I get the count to display zero for months that have no records

I am pulling transactions that happen on an attribute (attribute ID 4205 in table 1235) by the date that a change happened to the attribute (found in the History table) and counting up the number of changes that occurred by month. So far I have
SELECT TOP(100) PERCENT MONTH(H.transactiondate) AS Month, COUNT(*) AS Count
FROM hsi.rmObjectInstance1235 AS O LEFT OUTER JOIN
hsi.rmObjectHistory AS H ON H.objectID = O.objectID
WHERE H.attributeid = 4205) AND Year(H.transaction date) = '2020'
GROUP BY MONTH(H.transactiondate)
And I get
Month Count
---------------
1 9
2 4
3 11
4 14
5 1
I need to display a zero for months June - December instead of excluding those months.
One option uses a recursive query to generate the dates, and then brings the original query with a left join:
with all_dates as (
select cast('2020-01-01' as date) dt
union all
select dateadd(month, 1, dt) from all_dates where dt < '2020-12-01'
)
select
month(d.dt) as month,
count(h.objectid) as cnt
from all_dates d
left join hsi.rmobjecthistory as h
on h.attributeid = 4205
and h.transaction_date >= d.dt
and h.transaction_date < dateadd(month, 1, d.dt)
and exists (select 1 from hsi.rmObjectInstance1235 o where o.objectID = h.objectID)
group by month(d.dt)
I am quite unclear about the intent of the table hsi.rmObjectInstance1235 in the query, as none of its column are used in the select and group by clauses; it it is meant to filter hsi.rmobjecthistory by objectID, then you can rewrite this as an exists condition, as shown in the above solution. Possibly, you might as well be able to just remove that part of the query.
Also, note that
top without order by does not really make sense
top (100) percent is a no op
As a consequence, I removed that row-limiting clause.

Trying to a Select Count where it needs to count the difference between a certain date and current date

It's not quite working for me!
My query is as follows:
SELECT COUNT (*) as [generic]
FROM [Log]
Where value IN (Select ID, tsSendDocument, sysReceivedFrom
WHERE sysReceivedFrom = 'generic' AND
DATEDIFF(hour, tsSendDocument, GetDate()) > 2)
So, what am I doing wrong here?
I want it to to count every time the tsSendDocument column is older than 2 hours. It will eventually give me a count that's equal to 1. I have a table set up to alert me if the value = 1, which means that the tsSendDocument is older than 2 hours.
Do this make any sense?
As per your comment, I've understood that you want to check if the last entry is older than 2 hours, so this should work:
SELECT TOP 1 CASE WHEN tsSendDocument < DATEADD(HOUR, -2, GETDATE()) THEN 1 ELSE 0 END AS [generic]
FROM [Log]
ORDER BY tsSendDocument DESC
I think you want only aggregation :
Select COUNT(*)
FROM Log
WHERE sysReceivedFrom = 'generic' AND
DATEDIFF(hour, tsSendDocument, GetDate()) > = 2;
subquery will only return one expression when you specified IN orNOT IN clause.

SQL Query to get the monthly change percentage from daily quotes

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.

Return records between now and previous week

I have the following SQL query in oracle:
SELECT * FROM
(
SELECT s.singleid,s.titel,a.naam,s.taal,SUM(b.aantal) AS "AANTAL VERKOCHT"
FROM singles s
JOIN artiesten a on a.artiestid = s.artiestid
JOIN bestellingen b on b.singleid = s.singleid
GROUP BY s.singleid,s.titel,a.naam,s.taal,b.datum
ORDER BY sum(b.aantal) DESC
)
WHERE ROWNUM <= 5
This works, but I need to return only the records where b.datum is between the time now, and previous week.
How do I do this?
You should be able to add a BETWEEN clause to your where:
WHERE b.datum between SYSDATE - 6 AND SYSDATE
You would want to extract from the BESTELLINGEN table only those rows where [datum] is greater than or equal to (today at midnight minus 7 days) and less than or equal to 'now' (or less than tomorrow at midnight, according to your requirement). I would probably make this set of Bestellingen rows an inline view and join the other tables to it, and then do your grouping.
In SQL Server the syntax is:
AND (b.datum > DATEADD(week, -1, GetDate()) and b.datum < GetDate())
I would assume the syntax is the same, or very similar, in Oracle.

SQL for counting events by date

I feel like I've seen this question asked before, but neither the SO search nor google is helping me... maybe I just don't know how to phrase the question. I need to count the number of events (in this case, logins) per day over a given time span so that I can make a graph of website usage. The query I have so far is this:
select
count(userid) as numlogins,
count(distinct userid) as numusers,
convert(varchar, entryts, 101) as date
from
usagelog
group by
convert(varchar, entryts, 101)
This does most of what I need (I get a row per date as the output containing the total number of logins and the number of unique users on that date). The problem is that if no one logs in on a given date, there will not be a row in the dataset for that date. I want it to add in rows indicating zero logins for those dates. There are two approaches I can think of for solving this, and neither strikes me as very elegant.
Add a column to the result set that lists the number of days between the start of the period and the date of the current row. When I'm building my chart output, I'll keep track of this value and if the next row is not equal to the current row plus one, insert zeros into the chart for each of the missing days.
Create a "date" table that has all the dates in the period of interest and outer join against it. Sadly, the system I'm working on already has a table for this purpose that contains a row for every date far into the future... I don't like that, and I'd prefer to avoid using it, especially since that table is intended for another module of the system and would thus introduce a dependency on what I'm developing currently.
Any better solutions or hints at better search terms for google? Thanks.
Frankly, I'd do this programmatically when building the final output. You're essentially trying to read something from the database which is not there (data for days that have no data). SQL isn't really meant for that sort of thing.
If you really want to do that, though, a "date" table seems your best option. To make it a bit nicer, you could generate it on the fly, using i.e. your DB's date functions and a derived table.
I had to do exactly the same thing recently. This is how I did it in T-SQL (
YMMV on speed, but I've found it performant enough over a coupla million rows of event data):
DECLARE #DaysTable TABLE ( [Year] INT, [Day] INT )
DECLARE #StartDate DATETIME
SET #StartDate = whatever
WHILE (#StartDate <= GETDATE())
BEGIN
INSERT INTO #DaysTable ( [Year], [Day] )
SELECT DATEPART(YEAR, #StartDate), DATEPART(DAYOFYEAR, #StartDate)
SELECT #StartDate = DATEADD(DAY, 1, #StartDate)
END
-- This gives me a table of all days since whenever
-- you could select #StartDate as the minimum date of your usage log)
SELECT days.Year, days.Day, events.NumEvents
FROM #DaysTable AS days
LEFT JOIN (
SELECT
COUNT(*) AS NumEvents
DATEPART(YEAR, LogDate) AS [Year],
DATEPART(DAYOFYEAR, LogDate) AS [Day]
FROM LogData
GROUP BY
DATEPART(YEAR, LogDate),
DATEPART(DAYOFYEAR, LogDate)
) AS events ON days.Year = events.Year AND days.Day = events.Day
Create a memory table (a table variable) where you insert your date ranges, then outer join the logins table against it. Group by your start date, then you can perform your aggregations and calculations.
The strategy I normally use is to UNION with the opposite of the query, generally a query that retrieves data for rows that don't exist.
If I wanted to get the average mark for a course, but some courses weren't taken by any students, I'd need to UNION with those not taken by anyone to display a row for every class:
SELECT AVG(mark), course FROM `marks`
UNION
SELECT NULL, course FROM courses WHERE course NOT IN
(SELECT course FROM marks)
Your query will be more complex but the same principle should apply. You may indeed need a table of dates for your second query
Option 1
You can create a temp table and insert dates with the range and do a left outer join with the usagelog
Option 2
You can programmetically insert the missing dates while evaluating the result set to produce the final output
WITH q(n) AS
(
SELECT 0
UNION ALL
SELECT n + 1
FROM q
WHERE n < 99
),
qq(n) AS
(
SELECT 0
UNION ALL
SELECT n + 1
FROM q
WHERE n < 99
),
dates AS
(
SELECT q.n * 100 + qq.n AS ndate
FROM q, qq
)
SELECT COUNT(userid) as numlogins,
COUNT(DISTINCT userid) as numusers,
CAST('2000-01-01' + ndate AS DATETIME) as date
FROM dates
LEFT JOIN
usagelog
ON entryts >= CAST('2000-01-01' AS DATETIME) + ndate
AND entryts < CAST('2000-01-01' AS DATETIME) + ndate + 1
GROUP BY
ndate
This will select up to 10,000 dates constructed on the fly, that should be enough for 30 years.
SQL Server has a limitation of 100 recursions per CTE, that's why the inner queries can return up to 100 rows each.
If you need more than 10,000, just add a third CTE qqq(n) and cross-join with it in dates.