I have a table of dates that is passed into a scalar function:
select getVal('1/1/1') returns a float.
I'm looping through my table of dates and setting #date,
then saving the result to #result and then performing a log on that value.
How can I get multiple results (with multiple scalar calls) without doing a loop? I've been told to try to stay away from doing loops inside of sql, and that there's a better way to perform this operation.
Example:
#DateTable
ID Dates
1 1/1/1
2 1/3/1
3 1/4/1
4 1/2/2
5 1/3/2
6 4/15/12
set #k = 1
while #k <= (select count(ID) from #DateTable)
begin
set #date = select Dates from #DateTable where ID = #k
set #result = (select mySchema.getVal(#date))
insert into #ResTable(result,logged) values (#result, LOG(#result))
set #k = #k + 1
end
You can simplify this to:
INSERT #ResTable (Result, Logged)
SELECT d.Result, LOG(Result)
FROM ( SELECT Result = mySchema.getVal(Dates)
FROM #DateTable
) AS d;
I suspect if you show what mySchema.getVal() does, it can probably be made more efficient still.
To answer the comment - If you are using SQL Server 2012 or newer you can use the LEAD/LAG functions, something like:
SELECT Dates,
PreviousDate = LAG(Dates) OVER (ORDER BY Dates),
NextDate = LEAD(Dates) OVER (ORDER BY Dates)
FROM #DateTable;
If you are using earlier versions you will need to use APPLY:
SELECT Dates,
PreviousDate = prev.Dates
FROM #DateTable AS dt
OUTER APPLY
( SELECT TOP 1 Dates
FROM #DateTable AS Prev
WHERE Prev.Dates < dt.Dates
ORDER BY Dates DESC
) AS prev;
Related
We have 3 nested selects that are each creating temporary table. The outer two go very fast. But the inner one (below takes about 1/4 second sometimes to execute. It's creating a table with 7 rows, each holding a date:
declare #StartDate datetime
declare #EndDate datetime
select #StartDate = cast(#Weeks_Loop_TheDate as date), #EndDate = cast((#Weeks_Loop_TheDate + 6) as date)
declare #temp3 table
(
TheDate datetime
)
while (#StartDate<=#EndDate)
begin
insert into #temp3
values (#StartDate )
select #StartDate=DATEADD(dd,1,#StartDate)
end
select * from #temp3
The params are set with a DateTime variable so the cast shouldn't be significant. And the populating should be trivial and fast. So any ideas why it's slow?
And is there a better way to do this? We need to get back a result set that is 7 dates in this range.
thanks - dave
Wouldn't this work? Loops/cursors are slow in SQL Server compared to set operations.
DECLARE #StartDate DATE = '2017-05-03';
SELECT DATEADD(DAY, RowNr, #StartDate)
FROM (SELECT ROW_NUMBER () OVER (ORDER BY object_id) - 1 AS RowNr FROM sys.objects) AS T
WHERE T.RowNr < 7;
Subquery will generate a sequence of numbers from 0 to n (amount of objects you have in database, it's always going to be more than 7, and if not, you can just CROSS JOIN inside).
Then just use DATEADD to add these generated numbers.
And finally limit amount of days you want to add in your WHERE clause.
And if you're going to use this quite often, you can wrap it up in a Inline Table-Valued Function.
CREATE FUNCTION dbo.DateTable (
#p1 DATE,
#p2 INT)
RETURNS TABLE
AS RETURN
SELECT DATEADD(DAY, RowNr, #p1) AS TheDate
FROM (SELECT ROW_NUMBER () OVER (ORDER BY object_id) - 1 AS RowNr FROM sys.objects) AS T
WHERE T.RowNr < #p2;
GO
And then query it like that:
SELECT *
FROM dbo.DateTable ('2017-05-03', 7);
Result in both cases:
+------------+
| TheDate |
+------------+
| 2017-05-03 |
| 2017-05-04 |
| 2017-05-05 |
| 2017-05-06 |
| 2017-05-07 |
| 2017-05-08 |
| 2017-05-09 |
+------------+
Yet another useful tool is a Numbers table. It can be created just like that (source: http://dataeducation.com/you-require-a-numbers-table/) :
CREATE TABLE Numbers
(
Number INT NOT NULL,
CONSTRAINT PK_Numbers
PRIMARY KEY CLUSTERED (Number)
WITH FILLFACTOR = 100
)
INSERT INTO Numbers
SELECT
(a.Number * 256) + b.Number AS Number
FROM
(
SELECT number
FROM master..spt_values
WHERE
type = 'P'
AND number <= 255
) a (Number),
(
SELECT number
FROM master..spt_values
WHERE
type = 'P'
AND number <= 255
) b (Number)
GO
Then you would not have to use ROW_NUMBER() and your function could be as follows:
ALTER FUNCTION dbo.DateTable (
#p1 DATE,
#p2 INT)
RETURNS TABLE
AS RETURN
SELECT DATEADD(DAY, Number, #p1) AS TheDate
FROM Numbers
WHERE Number < #p2;
GO
This is going to work like a charm and Numbers table could be reused in many other scenarios where you need a sequence of numbers to do some sort of calculations.
It shouldn't take more than a millisecond to run your script. There must be a server issue that requires investigation.
That said, this operation can be done as a more efficient set-based operation instead of looping. The example below uses a CTE to generate the number sequence. A utility numbers table facilitates set-based processing like this so I suggest you create a permanent table with a sequence of numbers (with number as the primary key) to improve performance further.
DECLARE #StartDate date = #Weeks_Loop_TheDate;
WITH numbers(n) AS (
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT 0)) - 1 FROM (VALUES(0),(0),(0),(0),(0),(0),(0)) AS a(b)
)
SELECT DATEADD(day, n, #StartDate)
FROM numbers
ORDER BY n;
Rather than using a variable such as #temp use a temp table (#) instead. The query analyser doesn't do optimizing well when using #temp.
I want to generate 5 random records from a field which is a datetime column and contains several records of (OrderDate) for a given date range using stored procedure for the table named Orders
CREATE PROCEDURE test
#StartDate DATETIME = NULL,
#EndDate DATETIME = NULL,
AS
BEGIN
SELECT OrderDate = DATEADD(......)
FROM Orders
END
May I get some help!
A while loop works ok for this purpose, especially if you're concerned with limiting your randomness to a bounded date range.
The downside is that potentially many insert queries get executed vs. a single insert for a recursive CTE as in the other answer.
create procedure dbo.spGenDates2
#MinDate datetime,
#MaxDate datetime,
#RecordCount int = 5
as
SET NOCOUNT ON;
DECLARE #Range int, #DayOffset int, #Cnt int
SET #Range = DATEDIFF(dd, #MinDate, #MaxDate)
SET #Cnt = 1
WHILE #Cnt <= #RecordCount
BEGIN
SET #DayOffset = RAND() * (#Range + 1)
INSERT INTO _test (Dt) VALUES(DATEADD(dd, #DayOffset, #MinDate))
SET #Cnt = #Cnt + 1
END
Based on your syntax I'm assuming you're using SQL Server...
Note that you cannot reliably use the sql random number generator function RAND() within the context of a single query because it does not get reseeded per row so you end up receiving the same, single random number for each row result. Instead, an approach using NEWID() converted into a numeric does the trick when generating random values within the execution of a single query.
Here's a procedure that will give you n number of sample dates in the near past.
create procedure dbo.spGenDates
#MaxDate datetime,
#RecordCount int = 5
as
WITH dates as (
SELECT DATEADD(MILLISECOND, ABS(CHECKSUM(NEWID())) * -1, #MaxDate) D,
1 as Cnt
UNION ALL
SELECT DATEADD(MILLISECOND, ABS(CHECKSUM(NEWID())) * -1, #MaxDate) D,
x.Cnt + 1 as Cnt
FROM dates x
WHERE x.Cnt < #RecordCount
)
INSERT INTO _test (Dt)
SELECT D
FROM dates
The wording of the question has been clarified (see comments on another answer) to be a desire to SELECT 5 random sample dates within a bounded range from a table.
A query like this will yield the desired result.
SELECT TOP (5) OrderDate
FROM Orders
WHERE OrderDate >= #StartDate
AND OrderDate < #EndDate
ORDER BY NEWID()
I 'm using SQL Server 2014 and I have a table Attendance. This table has 2 columns AttdDate and Status. I want to create a stored procedure that returns list of dates between 2 dates, AttdDate and status. And if the AttdDate is in this list (dates list) status should be true else status should be false.
Any advice? Thanks
CREATE PROCEDURE sp_Attendance #Start DATETIME, #End DATETIME
AS
BEGIN
DECLARE #NumDays INT;
-- This will give you the number of days between your start date and end date.
SELECT #NumDays = DATEDIFF(DAY, #Start, #End) + 1;
WITH CTE AS (
SELECT TOP (#Numdays)
/*
ROW_NUMBER() OVER (ORDER BY a.object_id) will give you an integer from 1 to #NumDays becuase of TOP (#NumDays)
ROW_NUMBER() OVER (ORDER BY a.object_id) - 1 will give you an integer from 0 to #NumDays
DATEADD(DAY, ROW_NUMBER(), #Start) -- This will add the integer from the row number statement to your start date.
i.e.
#Start + 0
#Start + 1
#Start + 2
etc
etc
#Start + #NumDays
*/
DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY a.object_id) - 1, #Start) AS AttdDate
FROM
sys.all_columns a
CROSS JOIN
sys.all_columns b)
SELECT
c.AttdDate,
CASE WHEN a.AttdDate IS NOT NULL THEN 1 ELSE 0 END AS Status
FROM
CTE c
LEFT JOIN
Attendance a
ON c.AttdDate = a.AttdDate;
END;
You can use a recursive query for this:
WITH q(d) AS
(
SELECT #mindate
UNION ALL
SELECT DATEADD(day, 1 d)
FROM q
WHERE d < #maxdate
)
SELECT *
FROM q
OPTION (MAXRECURSION 0)
, however, its performance would be quite poor for longer ranges.
I usually just create a table with all possible dates up to year 2100 in my SQL Server databases:
SELECT *
FROM dates
WHERE d BETWEEN #mindate AND #maxdate
I have a column of dates. They are all workdays. I would like to generate a list of dates that are 'n' days apart. For example, starting with the most recent date, I want to find the date n days before it, 2n days before it, 3n days before it, etc. I could use a while loop but I wanted to know if I could use SQL set operations instead. Can it be done?
Find the difference between the most_recent_date and dates in your table, then use the modulo function, where n is the interval.
SELECT date
FROM my_table
WHERE mod(most_recent_date - date, n) = 0
This is the perfect case for a CTE:
DECLARE #LastDate datetime;
DECLARE #N int;
DECLARE #NCoefficientMax;
SELECT #N = 1, #NCoefficientMax = 10;
SELECT #LastDate = MyDate
FROM MyTable
ORDER BY MyDate DESC
WITH mycte
AS
(
SELECT DATEADD(dd, #N, #LastDate) AS NextDate, #N AS NCoefficient
UNION ALL
SELECT DATEADD(dd, #N, NextDate), #N + NCoefficient AS NCoefficient
FROM mycte WHERE NCoefficient < #NCoefficientMax
)
SELECT NextDate FROM mycte
Where #NCoefficientMax is the max coefficient for N.
You can use the dateadd funcion
and make select with join to self table.
What that you need to do it -
Insert the result to temporary table,
with additional column then contain the row_number then order like the result
simple example:
declare #t1 table (d datetime, row int)
insert #t1
select d, row_number()over(order by d)
from T1
order by d
select T1A.*, datediff(day,T1A.d,T1B.d) as dif
from #t1 as T1A
left join #t1 as T1B on T1A.row = T1B.row-1
DECLARE #mostRecent datetime2
SELECT #mostRecent = MAX(dateColumn)
FROM table
SELECT columns
FROM table
WHERE (DATEDIFF(day, dateColumn, #mostRecent) % n) = 0
Supposing that I wanted to write table valued function in SQL that returns a table with the first day of every month between the argument dates, what is the simplest way to do this?
For example fnFirstOfMonths('10/31/10', '2/17/11') would return a one-column table with 11/1/10, 12/1/10, 1/1/11, and 2/1/11 as the elements.
My first instinct is just to use a while loop and repeatedly insert first days of months until I get to before the start date. It seems like there should be a more elegant way to do this though.
Thanks for any help you can provide.
Something like this would work without being inside a function:
DECLARE #LowerDate DATE
SET #LowerDate = GETDATE()
DECLARE #UpperLimit DATE
SET #UpperLimit = '20111231'
;WITH Firsts AS
(
SELECT
DATEADD(DAY, -1 * DAY(#LowerDate) + 1, #LowerDate) AS 'FirstOfMonth'
UNION ALL
SELECT
DATEADD(MONTH, 1, f.FirstOfMonth) AS 'FirstOfMonth'
FROM
Firsts f
WHERE
DATEADD(MONTH, 1, f.FirstOfMonth) <= #UpperLimit
)
SELECT *
FROM Firsts
It uses a thing called CTE (Common Table Expression) - available in SQL Server 2005 and up and other database systems.
In this case, I start the recursive CTE by determining the first of the month for the #LowerDate date specified, and then I iterate adding one month to the previous first of month, until the upper limit is reached.
Or if you want to package it up in a stored function, you can do so, too:
CREATE FUNCTION dbo.GetFirstOfMonth(#LowerLimit DATE, #UpperLimit DATE)
RETURNS TABLE
AS
RETURN
WITH Firsts AS
(
SELECT
DATEADD(DAY, -1 * DAY(#LowerLimit) + 1, #LowerLimit) AS 'FirstOfMonth'
UNION ALL
SELECT
DATEADD(MONTH, 1, f.FirstOfMonth) AS 'FirstOfMonth'
FROM
Firsts f
WHERE
DATEADD(MONTH, 1, f.FirstOfMonth) <= #UpperLimit
)
SELECT * FROM Firsts
and then call it like this:
SELECT * FROM dbo.GetFirstOfMonth('20100522', '20100831')
to get an output like this:
FirstOfMonth
2010-05-01
2010-06-01
2010-07-01
2010-08-01
PS: by using the DATE datatype - which is present in SQL Server 2008 and newer - I fixed the two "bugs" that Richard commented about. If you're on SQL Server 2005, you'll have to use DATETIME instead - and deal with the fact you're getting a time portion, too.
create function dbo.fnFirstOfMonths(#d1 datetime, #d2 datetime)
returns table as return
select dateadd(m,datediff(m,0,#d1)+v.number,0) as FirstDay
from master..spt_values v
where v.type='P' and v.number between 0 and datediff(m, #d1, #d2)
and dateadd(m,datediff(m,0,#d1)+v.number,0) between #d1 and #d2
GO
Notes
master..spt_values is a source for general purpose sequence numbers in SQL Server
dateadd(m, datediff(m is a technique for working out the first day of month for any date
+v.number is used to increase it by one month each time
0 and datediff(m, #d1, #d2) this condition gives us all the numbers we need to generate a first-of-month date for each month between #d1 and #d2, inclusive of both months
and dateadd(m,datediff(m,0,#d1)+v.number,0) between #d1 and #d2 the final filter to verify that the first-of-month date generated is between #d1 and #d2
Performance comparison against marc_s's code
Summary
8220 ms (CTE)
4173 ms (master..spt_values)
Test
declare #t table (dt datetime)
declare #d datetime
declare #i int
set nocount on
set #d = GETDATE()
set #i = 0
while #i < 10000
begin
insert #t select * from dbo.getfirstofmonth('20090102', '20100506')
delete #t
set #i = #i + 1
end
print datediff(ms, #d, getdate())
set #d = GETDATE()
set #i = 0
while #i < 10000
begin
insert #t select * from dbo.fnfirstofmonths('20090102', '20100506')
delete #t
set #i = #i + 1
end
print datediff(ms, #d, getdate())
Performante
It will loop just between the months involved (4 times in the example):
set dateformat mdy;
declare #date1 smalldatetime,#date2 smalldatetime,#i int
set #date1= '10-31-2010'
set #date2= '02-17-2011'
set #i=1
while(#i<=DATEDIFF(mm,#date1,#date2))
begin
select convert(smalldatetime,CONVERT(varchar(6),DATEADD(mm,#i,#date1),112)+'01',112)
set #i=#i+1
end
I realize this isn't a function, but I'm going to throw this into the mix anyway.
select cal_date from calendar
where day_of_month = 1
and cal_date between '2011-01-01' and '2012-01-01'
This calendar table runs on a PostgreSQL server at work. I'll port it to SQL Server tonight, and run some speed comparisons. (Why? Because this stuff is fun, that's why.)
Just in case anybody is still reading this ...
I cannot imaging that any of the aforementioned functions is faster than this:
declare #DatFirst date = '20101031', #DatLast date = '21110217';
declare #DatFirstOfFirstMonth date = dateadd(day,1-day(#DatFirst),#DatFirst);
select DatFirstOfMonth = dateadd(month,n,#DatFirstOfFirstMonth)
from (
select top (datediff(month,#DatFirstOfFirstMonth,#DatLast)+1)
n=row_number() over (order by (select 1))-1
from (values (1),(1),(1),(1),(1),(1),(1),(1)) a (n)
cross join (values (1),(1),(1),(1),(1),(1),(1),(1)) b (n)
cross join (values (1),(1),(1),(1),(1),(1),(1),(1)) c (n)
cross join (values (1),(1),(1),(1),(1),(1),(1),(1)) d (n)
) x