SQL Checking for NULL and incrementals - sql

I'd like to check if there is anything to return given a number to check against, and if that query returns no entries, increase the number until an entry is reached and display that entry. Currently, the code looks like this :
SELECT *
FROM news
WHERE DATEDIFF(day, date, getdate() ) <= #url.d#
ORDER BY date desc
where #url.d# is an integer being passed through (say 31). If that returns no results, I'd like to increase the number stored in #url.d# by 1 until an entry is found.

This kind of incremental querying is just not efficient. You'll get better results by saying - "I'll never need more than 100 results so give me these" :
SELECT top 100 *
FROM news
ORDER BY date desc
Then filtering further on the client side if you want only a particular day's items (such as the items with a common date as the first item in the result).
Or, you could transform your multiple query request into a two query request:
DECLARE
#theDate datetime,
#theDate2 datetime
SET #theDate = (SELECT Max(date) FROM news)
--trim the time off of #theDate
SET #theDate = DateAdd(dd, DateDiff(dd, 0, #theDate), 0)
SET #theDate2 = DateAdd(dd, 1, #theDate)
SELECT *
FROM news
WHERE #theDate <= date AND date < #theDate2
ORDER BY date desc

In MySQL:
SELECT news.*,
(
SELECT COUNT(*)
FROM news
WHERE date < DATEADD(day, GETDATE(), -#url.d#)
)
FROM news
WHERE date >= DATEADD(day, GETDATE(), -#url.d#)
ORDER BY
date DESC
LIMIT 1
In SQL Server:
SELECT TOP 1
news.*,
(
SELECT COUNT(*)
FROM news
WHERE date < DATEADD(day, GETDATE(), -#url.d#)
)
FROM news
WHERE date >= DATEADD(day, GETDATE(), -#url.d#)
ORDER BY
date DESC
Note that using this syntax makes your query sargable, that is an index can be used to filter on date efficiently.

First, I think you will probably want to avpod using the DateDiff function in your where clause, instead, compute the desired cutoff date and do use any computations on the date column within the where clause, this will be more efficient, so rather than
WHERE DATEDIFF(day, date, getdate() ) <= #url.d#
you would have something like
WHERE date >= #cutoffDate
where #cutoffDate is a computed date based on #url.d#
Now, as for grabbing the correct cutoff date. My assumption is that under normal circumstances, there will be articles returned from the request otherwise you would just grab articles from the most recent date. So, the approach that I would take would be to grab the OLDEST of the computed cutoff date (based on #url.d# and the MOST RECENT article date. Something like
-- #urld == #url.d
-- compute the cutoff date as the OLDEST of the most recent article and
-- the date based on #url.d
declare #cutoff datetime
select #cutoff = DateAdd(dd,-1*#urld,GetDate())
select #cutoff
select #cutoff = min(cutoffDate)
from
(SELECT Max(date) as cutoffDate from News
UNION
select #cutoff) Cutoff
-- grab the articles with dates that are more recent than the cutoff date
select *
from News
WHERE date >= #cutoff
I'm also guessing that you would probably want to round to midnight for the dates (which I didn't do here). This is a multi-query approach and should probably be implemented in a single stored procedure ... if this is what you are looking for.
Good luck with the project!

If you wanted the one row:
SELECT t.*
FROM NEWS t
WHERE t.id = (SELECT MAX(n.id)
FROM NEWS n
WHERE n.date BETWEEN DATEADD(day, -:url.d, getDate()) AND getDate())
It might not be obvious that the DATEADD is using a negative in order to go back however many number of days desired.
If you wanted all the rows in that date:
SELECT t.*
FROM NEWS t
WHERE t.date BETWEEN DATEADD(day, -:url.d, getDate()) AND getDate())

Related

Generate List of dates between 2 dates for each Id

I have a table with PersonId's that each have a FirstSubscription date and LastSubscriptionDate.
What I need to do is between those 2 dates, generate 1 date for each month. This is for reporting purposes on the front end, as this data will end up inside PowerBI and I need these dates to join to a ReportingCalendar.
This Calendar is accessible by SQL so it can be used in this calculation. I am using it to generate the dates (using first of the month) between the First and LastSubDate but I need to find a way to join this with the rest of the ID's that way I get a list of date for each ID.
Here is my code to generate the dates.
DECLARE #MinDate DATE
DECLARE #MaxDate DATE
SET #MinDate = '2020-08-31'
SET #MaxDate = '2022-08-30'
SELECT DATEADD(month, DATEDIFF(month, 0, date), 0)
FROM dbo.ReportingCalendar
WHERE Date >= #MinDate
AND Date < #MaxDate
GROUP BY
DATEADD(month, DATEDIFF(month, 0, date), 0)
My PersonSubscription table looks like this
|PersonId|FirstSubDate|LastSubDate|
|--------|------------|-----------|
|1186 |8/31/2020 |8/30/2022 |
|2189 |7/30/2019 |7/31/2021 |
So I would want to end up with an output where each PersonId has 1 entry for each month between those 2 dates. So PersonId has 25 entries from 8/2020 until 8/2022. We don't care about the actual date of the sub since this data is looked at monthly and will primarily be looked at using a Distinct Count each month, so we only care if they were subbed at any time in that month.
I just needed to do a Cross Apply.
I took my code that got me all of the PersonId's and their FirstSubDate and LastSubDate and then did a cross apply to the code I listed above, referencing the MinDate and MaxDate with the FirstSubDate and LastSubDate.

How to use select, max date and declare in a sql query?

I am trying to use declare and max in variable. This is the query below:
Declare #MAX_BUF Datetime
Set #OpeningStock = (
SELECT #MAX_BUF = MAX(end_date) FROM [IBIS].[buf_stk]
WHERE SUBSTRING(CONVERT(VARCHAR,end_date ,112 ),1,6)<SUBSTRING(CONVERT(VARCHAR,getdate() ,112 ),1,6);
SELECT COUNT(1) AS Opening_Stock
FROM [IBIS].[buf_stk] AS bs(NOLOCK)
WHERE CAST(end_date AS DATE)=#MAX_BUF
)
I am getting syntax error in '=' and '(end_date)'. Please let me know if this can be resolved.
Without seeing your table and data I can't be confident in rewriting your SQL correctly, however all you need to do to "make it work", I suspect, is amend slightly to:
SELECT #MAX_BUF = MAX(end_date) FROM [IBIS].[buf_stk]
WHERE SUBSTRING(CONVERT(VARCHAR,end_date ,112 ),1,6)<SUBSTRING(CONVERT(VARCHAR,getdate() ,112 ),1,6);
SELECT #OpeningStock=COUNT(*)
FROM [IBIS].[buf_stk] AS bs(NOLOCK) --<< Remove this!
WHERE CAST(end_date AS DATE)=#MAX_BUF
Having said that, you could combine these into a single query. Your manipulation of dates as strings is not sargable and will force the optimizer to scan your table/index.
If you want to find rows where end_date is prior to 1st day of the month then just compare as dates (assuming your end_date is an actual date data type)
where end_date < DATEADD(month, DATEDIFF(month, 0, GetDate()), 0)
And lose the nolock hint, unless you prefer your data to be randomly incorrect.
Use two statements. I'm not a fan of converting dates to strings for date arithmetic. You seem to want the maximum date from before this month. So, DATEDIFF() provides one method (and there are more efficient methods if you have an index on end_date):
Set ##MAX_BUF = (SELECT CAST(MAX(end_date) as date)
FROM [IBIS].[buf_stk]
WHERE DATEDIFF(month, end_date, getdate()) >= 1
);
Set #OpeningStock = (SELECT COUNT(1) AS Opening_Stock
FROM [IBIS].[buf_stk] bs
WHERE CAST(end_date AS DATE) = #MAX_BUF
);
You can also do this as a single statement:
select top (1)
#max_buf = CAST(end_date as date),
#OpeningStock = COUNT(*)
from FROM [IBIS].[buf_stk] bs
where end_date < dateadd(day, 1 - day(getdate()), convert(date, getdate()))
group by CAST(end_date as date)
order by CAST(end_date as date) desc;
Note that this also changes the date comparison to be friendlier to the optimizer.
You can actually do this all in one statement and one scan of the table:
SELECT #MAX_BUF = MAX(end_date), #OpeningStock = COUNT(1)
FROM
(SELECT TOP (1) WITH TIES
CAST(end_date AS date) end_date
FROM [IBIS].[buf_stk]
WHERE end_date >= DATEADD(month, 1, getdate())
ORDER BY CAST(end_date AS date) DESC
) t;
Notes:
Don't use NOLOCK, it has many unintended effects and can cause incorrect results
Switch round the WHERE predicate in order to hit an index if you have one. Don't make the server do algebra for you, it's not very good at it.

SQL Loop Count of people in program during specified duration

I'm not sure if there should be a loop for this or what the easiest approach would be.
My data consists of a list of people participating in our program. They have various start and end dates, but the following equation is able to capture the number of people who participated on a specific date:
DECLARE #PopulationDate DATETIME = '2018-06-01 05:00:00';
select count(People)
FROM Program_Log
WHERE
START_TIME <= #PopulationDate
AND (END_TIME >= #PopulationDate OR END_TIME IS NULL)`
Is there a way I can loop in different date values to get the number of program participants each day for an entire year?
Multiple years?
One simple way is to use a CTE to generate the dates and then a left join to bring in the data. For instance, the following gets the counts as of the first of the month for this year:
with dates as (
select cast('2018-01-01' as date) as dte
union all
select dateadd(month, 1, dte)
from dates
where dte < getdate()
)
select d.dte, count(pl.people)
from dates d left join
program_log pl
on pl.start_time <= d.dte and (pl.end_time >= d.dte or pl.end_time is null)
group by d.dte
order by d.dte;
Note that this will work best for a handful of dates. If you want more than 100, you need to add option (maxrecursion 0) to the end of the query.
Also, count(people) is highly suspicious. Perhaps you mean sum(people) or something similar.

How to store n number of SELECT results into variable?

So, i have this statement that returns OrderDates of all orders made by a customer. I'm trying to store the orderdates into a variable or multiple variable, to calculate average.
I know that for a specific result we can do something like this
DECLARE #tempvar DATE
SET #tempvar = (SELECT OrderDate From Orders WHERE CustID = '1234')
But issue is, what if select statement returns variable number of results (variable rows), like 0 results or 2 results or 4?
I know we can get number of rows returned by a select statement by ##ROWCOUNT variable.
To put my issue in simpler words, i need to store newest and oldest date returned by select statement, and divide it by number of rows returned.
If your main objective is to get the average of the dates, for that you don't need to store the dates to a variable, you can do it directly like following.
SELECT CAST(AVG(CAST(OrderDate AS FLOAT)) AS DATETIME) FROM Orders WHERE CustID = '1234'
You can also store the average of all date inside a variable, you can do it like following.
DECLARE #AvgDate DATETIME
SELECT #AvgDate = CAST(AVG(CAST(OrderDate AS FLOAT)) AS DATETIME)
FROM Orders WHERE CustID = '1234'
Still if you want to store the dates into some variable, in that case to store more than 1 value better choose a table datatype as following.
DECLARE #OrderDates TABLE(OrderDate DATETIME)
INSERT INTO #OrderDates(OrderDate)
SELECT OrderDate From [Orders] WHERE CustID = '1234'
--TO GET AVG
SELECT CAST(AVG(CAST(OrderDate AS FLOAT)) AS DATETIME) FROM #OrderDates ;
-- RETURNS ALL ROW
SELECT * FROM #OrderDates
Edit :
To put my issue in other words, i need to get number of days between
highest and smallest date returned by SELECT statement
To find the days between MAX date and Min Date, you can do like following.
SELECT DATEDIFF(day, MIN(OrderDate), MAX(OrderDate)) FROM #OrderDates
OR
SELECT DATEDIFF(day, MIN(OrderDate), MAX(OrderDate)) FROM Orders WHERE CustID = '1234'
I think this gets you what you want in just one line of code:
select dateadd(d, avg(datediff(d, '1970-01-01', OrderDate)), '19700101') from Orders where CustID = '1234'
In response to comment below, try:
select
dateadd(d, datediff(d, '1970-01-01', max(OrderDate)) - datediff(d, '1970-01-01', min(OrderDate)) / count(*), '1970-01-01') as requested_calc,
dateadd(d, avg(datediff(d, '1970-01-01', OrderDate)), '19700101') as avg_date
from Orders
where CustID = '1234'
But for the life of me, I don't understand why you would want this calc. So I kept the other too.
I hope this helps.

SQL Count to include zero values

I have created the following stored procedure that is used to count the number of records per day between a specific range for a selected location:
[dbo].[getRecordsCount]
#LOCATION as INT,
#BEGIN as datetime,
#END as datetime
SELECT
ISNULL(COUNT(*), 0) AS counted_leads,
CONVERT(VARCHAR, DATEADD(dd, 0, DATEDIFF(dd, 0, Time_Stamp)), 3) as TIME_STAMP
FROM HL_Logs
WHERE Time_Stamp between #BEGIN and #END and ID_Location = #LOCATION
GROUP BY DATEADD(dd, 0, DATEDIFF(dd, 0, Time_Stamp))
but the problem is that the result does not show the days where there are zero records, I pretty sure that it has something to do with my WHERE statement not allowing the zero values to be shown but I do not know how to over come this issue.
Thanks in advance
Neil
Not so much the WHERE clause, but the GROUP BY. The query will only return data for rows that exist. That means when you're grouping by the date of the timestamp, only days for which there are rows will be returned. SQL Server can't know from context that you want to "fill in the blanks", and it wouldn't know what with.
The normal answer is a CTE that produces all the days you want to see, thus filling in the blanks. This one's a little tricky because it requires a recursive SQL statement, but it's a well-known trick:
WITH CTE_Dates AS
(
SELECT #START AS cte_date
UNION ALL
SELECT DATEADD(DAY, 1, cte_date)
FROM CTE_Dates
WHERE DATEADD(DAY, 1, cte_date) <= #END
)
SELECT
cte_date as TIME_STAMP,
ISNULL(COUNT(HL_Logs.Time_Stamp), 0) AS counted_leads,
FROM CTE_Dates
LEFT JOIN HL_Logs ON DATEADD(dd, 0, DATEDIFF(dd, 0, Time_Stamp)) = cte_date
WHERE Time_Stamp between #BEGIN and #END and ID_Location = #LOCATION
GROUP BY cte_date
Breaking it down, the CTE uses a union that references itself to recursively add one day at a time to the previous date and remember that date as part of the table. If you ran a simple statement that used the CTE and just selected * from it, you'd see a list of dates between start and end. Then, the statement joins this list of dates to the log table based on the log timestamp date, while preserving dates that have no log entries using the left join (takes all rows from the "left" side whether they have matching rows on the "right" side or not). Finally, we group by date and count instead and we should get the answer you want.
When there is no data to count, there is no row to return.
If you want to include empty days as a 0, you need to create a table (or temporary table, or subquery) to store the days, and left join to your query from that.
eg: something like
SELECT
COUNT(*) AS counted_leads,
CONVERT(VARCHAR, DATEADD(dd, 0, DATEDIFF(dd, 0, Time_Stamp)), 3) as TIME_STAMP
FROM
TableOfDays
left join
HL_Logs
on TableOfDays.Date = convert(date,HL_Logs.Time_Stamp)
and ID_Location = #LOCATION
WHERE TableOfDays.Date between #BEGIN and #END
GROUP BY DATEADD(dd, 0, DATEDIFF(dd, 0, Time_Stamp))
Use a left outer join. Such as
select count(stuff_ID), extra_NAME
from dbo.EXTRAS
left outer join dbo.STUFF on suff_EXTRA = extra_STUFF
group by extra_NAME
I just recently has a similar task and used this as a backdrop to my work. However, as explained by robwilliams I too, couldn't get it KeithS solution to work. Mine task was slightly different I was doing it by hours vs days but I think the solution to the neilrudds question would be
DECLARE #Start as DATETIME
,#End as DATETIME
,#LOCATION AS INT;
WITH CTE_Dates AS
(
SELECT #Start AS cte_date, 0 as 'counted_leads'
UNION ALL
SELECT DATEADD(DAY, 1, cte_date) as cte_date, 0 AS 'counted_leads'
FROM CTE_Dates
WHERE DATEADD(DAY, 1, cte_date) <= #End
)
SELECT cte_date AS 'TIME_STAMP'
,COUNT(HL.ID_Location) AS 'counted_leads'
FROM CTE_Dates
LEFT JOIN HL_Logs AS HL ON CAST(HL.Time_Stamp as date) = CAST(cte_date as date)
AND DATEPART(day, HL.Time_Stamp) = DATEPART(day,cte_date)
AND HL.ID_Location = #LOCATION
group by cte_date
OPTION (MAXRECURSION 0)