SQL Server FOR EACH Loop - sql

I have the following SQL query:
DECLARE #MyVar datetime = '1/1/2010'
SELECT #MyVar
This naturally returns '1/1/2010'.
What I want to do is have a list of dates, say:
1/1/2010
2/1/2010
3/1/2010
4/1/2010
5/1/2010
Then i want to FOR EACH through the numbers and run the SQL Query.
Something like (pseudocode):
List = 1/1/2010,2/1/2010,3/1/2010,4/1/2010,5/1/2010
For each x in List
do
DECLARE #MyVar datetime = x
SELECT #MyVar
So this would return:-
1/1/2010
2/1/2010
3/1/2010
4/1/2010
5/1/2010
I want this to return the data as one resultset, not multiple resultsets, so I may need to use some kind of union at the end of the query, so each iteration of the loop unions onto the next.
edit
I have a large query that accepts a 'to date' parameter, I need to run it 24 times, each time with a specific to date which I need to be able to supply (these dates are going to be dynamic) I want to avoid repeating my query 24 times with union alls joining them as if I need to come back and add additional columns it would be very time consuming.

SQL is primarily a set-orientated language - it's generally a bad idea to use a loop in it.
In this case, a similar result could be achieved using a recursive CTE:
with cte as
(select 1 i union all
select i+1 i from cte where i < 5)
select dateadd(d, i-1, '2010-01-01') from cte

Here is an option with a table variable:
DECLARE #MyVar TABLE(Val DATETIME)
DECLARE #I INT, #StartDate DATETIME
SET #I = 1
SET #StartDate = '20100101'
WHILE #I <= 5
BEGIN
INSERT INTO #MyVar(Val)
VALUES(#StartDate)
SET #StartDate = DATEADD(DAY,1,#StartDate)
SET #I = #I + 1
END
SELECT *
FROM #MyVar
You can do the same with a temp table:
CREATE TABLE #MyVar(Val DATETIME)
DECLARE #I INT, #StartDate DATETIME
SET #I = 1
SET #StartDate = '20100101'
WHILE #I <= 5
BEGIN
INSERT INTO #MyVar(Val)
VALUES(#StartDate)
SET #StartDate = DATEADD(DAY,1,#StartDate)
SET #I = #I + 1
END
SELECT *
FROM #MyVar
You should tell us what is your main goal, as was said by #JohnFx, this could probably be done another (more efficient) way.

You could use a variable table, like this:
declare #num int
set #num = 1
declare #results table ( val int )
while (#num < 6)
begin
insert into #results ( val ) values ( #num )
set #num = #num + 1
end
select val from #results

This kind of depends on what you want to do with the results. If you're just after the numbers, a set-based option would be a numbers table - which comes in handy for all sorts of things.
For MSSQL 2005+, you can use a recursive CTE to generate a numbers table inline:
;WITH Numbers (N) AS (
SELECT 1 UNION ALL
SELECT 1 + N FROM Numbers WHERE N < 500
)
SELECT N FROM Numbers
OPTION (MAXRECURSION 500)

declare #counter as int
set #counter = 0
declare #date as varchar(50)
set #date = cast(1+#counter as varchar)+'/01/2013'
while(#counter < 12)
begin
select cast(1+#counter as varchar)+'/01/2013' as date
set #counter = #counter + 1
end

Off course an old question. But I have a simple solution where no need of Looping, CTE, Table variables etc.
DECLARE #MyVar datetime = '1/1/2010'
SELECT #MyVar
SELECT DATEADD (DD,NUMBER,#MyVar)
FROM master.dbo.spt_values
WHERE TYPE='P' AND NUMBER BETWEEN 0 AND 4
ORDER BY NUMBER
Note : spt_values is a Mircrosoft's undocumented table. It has numbers for every type. Its not suggestible to use as it can be removed in any new versions of sql server without prior information, since it is undocumented. But we can use it as quick workaround in some scenario's like above.

[CREATE PROCEDURE [rat].[GetYear]
AS
BEGIN
-- variable for storing start date
Declare #StartYear as int
-- Variable for the End date
Declare #EndYear as int
-- Setting the value in strat Date
select #StartYear = Value from rat.Configuration where Name = 'REPORT_START_YEAR';
-- Setting the End date
select #EndYear = Value from rat.Configuration where Name = 'REPORT_END_YEAR';
-- Creating Tem table
with [Years] as
(
--Selecting the Year
select #StartYear [Year]
--doing Union
union all
-- doing the loop in Years table
select Year+1 Year from [Years] where Year < #EndYear
)
--Selecting the Year table
selec]

Related

How to update a table using while loops and waitfor delay to insert current date with a one second delay between records?

I'd like to create a table that contains two columns (id int, today datetime) and, using while loops, to insert the current date every 1 second. However, the resulting table shows the same time for all rows. Below is my code. Can anyone help me understand what I'm doing wrong, please? Thank you!
declare #mytable table (id int, today datetime)
declare #id int=1
declare #today datetime=getdate()
while #id<10
begin
waitfor delay '00:00:01'
insert into #mytable values (#id,#today)
set #id=#id+1
end
The reason every row has the same value is because you aren't setting the value of #Today anywhere apart from before your WHILE loop. GETDATE() returns a scalar value, and setting a variable to that value means it will be set the value that GETDATE() returned at the time the SET was run. The value of the variable won't change after time has passed. For example:
DECLARE #d datetime;
SET #d = GETDATE();
SELECT #d, GETDATE(); --Will return very similar values
WAITFOR DELAY '00:00:05';
SELECT #d, GETDATE(); --#d will have the same value as before, as its value is static, but GETDATE()'s value will have changed.
To do what you're after, I don't see any need for the variable for #Today, this would work fine:
DECLARE #mytable table (id int,
today datetime);
DECLARE #id int = 1;
WHILE #id < 10
BEGIN
WAITFOR DELAY '00:00:01';
INSERT INTO #mytable
VALUES (#id, GETDATE());
SET #id = #id + 1;
END;
However a loop is a bad choice anyway, as an RDBMS excels at set based operations, not iterative. You would be far better to achieve what you're after by doing:
DECLARE #mytable table (id int,
today datetime);
DECLARE #id int = 1;
WITH N AS (
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I
FROM N N1
CROSS JOIN N N2 --Not actually eneded here, but shows how to increase row count
)
INSERT INTO #mytable (id,
today)
SELECT TOP 10
T.I + #ID,
DATEADD(SECOND, T.I, GETDATE())
FROM Tally T
ORDER BY T.I;
This builds an inline tally table, and then inserts a value for a row for 10 ID, and adds 1 second to each incremented ID.

Inserting data with SQL

Similar questions have already appeard here, but it seems like I'm doing the same as in other instructions, but it doesn't work. So I have
Declare #Counter Int
Set #Counter = 1
while #Counter <= 1000
Begin
insert into Kiso_task_table ([Numbers],[Square_root])
values ( #Counter, Sqrt(#Counter));
Set #Counter = #Counter + 1;
CONTINUE;
End
SELECT TOP (1000) [Numbers],[Square_root]
FROM [Kiso_task].[dbo].[Kiso_task_table]
and it should give me Numbers from 1 to 1000 and their square roots, respectively - instead it produces "1" all the times? Do you know what is wrong?
Your error is in the type of variable to convert the square root, it must be of the 'float' type
CREATE TABLE #Kiso_task_table
(
[Numbers] INT,
[Square_root] FLOAT,
);
GO
DECLARE #Counter INT
SET #Counter = 1
WHILE #Counter <= 1000
BEGIN
INSERT INTO #Kiso_task_table ([Numbers],[Square_root]) VALUES ( #Counter, Sqrt(#Counter));
SET #Counter = #Counter + 1
CONTINUE
END
SELECT TOP (1000) [Numbers],[Square_root]
FROM #Kiso_task_table
SQRT (Transact-SQL)
Your approach is procedural thinking. Try to start thinking set-based. That means: No CURSOR, no WHILE, no loopings, no do this and then this and finally this. Let the engine know, what you want to get back and let the engine decide how to do this.
DECLARE #mockupTarget TABLE(Number INT, Square_root DECIMAL(12,8));
WITH TallyOnTheFly(Number) AS
(
SELECT TOP 1000 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values
)
INSERT INTO #mockupTarget(Number,Square_root)
SELECT Number,SQRT(Number)
FROM TallyOnTheFly;
SELECT * FROM #mockupTarget ORDER BY Number;
The tally-cte will create a volatile set of 1000 numbers. This is inserted in one single statement.
Btw
I tested your code against my mockup table and it was working just fine...

Getting FRACTIONAL difference (YEARS) between two dates in T SQL

I need to detect a difference between two dates, such that when Date_1 = 12-jan-2010 and Date_2 = 01-jan-2016 I would NOT get 6 but a number < 6.
SELECT DATEDIFF(YEAR,'12-jan-2010','01-jan-2016')
DATEDIFF returns 6 in the above case.
It depends on required precision, try following:
SELECT DATEDIFF(DAY, #d1, #d2)/365.25
I guess this resolves,
DECLARE #STARTDATE DATE='12-jan-2010'
DECLARE #ENDDATE DATE='01-jan-2016'
DECLARE #TOTALDAY DECIMAL(18,2)=DATEDIFF(day,#STARTDATE,#ENDDATE)
DECLARE #AVGYEAR DECIMAL(18,2)= ((365*DATEDIFF(YEAR,#STARTDATE,#ENDDATE))+
dbo.LEAP_YEAR(#STARTDATE,#ENDDATE))/CONVERT(DECIMAL(18,2),
DATEDIFF(YEAR,#STARTDATE,#ENDDATE))
SELECT CONVERT(decimal(18,2),#TOTALDAY/#AVGYEAR) AS DiffDate ---This will provide you result in decimal
this the function which return no of leap years between dates.
ALTER FUNCTION LEAP_YEAR(#START DATE,#END DATE)
RETURNS INT
AS BEGIN
DECLARE #COUNT INT = 0,#Z INT = DATEPART(YYYY,#START)
DECLARE #X INT =DATEPART(YYYY,#START)
DECLARE #Y INT =DATEPART(YYYY,#END)
IF (DATEPART(MM,#START) >2)
SET #X=#X+1
IF (DATEPART(MM,#END) <2)
SET #Y=#Y-1
WHILE (#X <= #Y)
BEGIN
SET #COUNT = #COUNT +
(CASE WHEN (#X%4 = 0 AND #X%100 !=0) OR #X%400 = 0
THEN 1
ELSE 0 END)
SET #X = #X + 1
END
RETURN #COUNT
END
If I've got it right. Just Add result of DATEDIFF (6 in this case) to the start date if it > the end date then just subtract 1 year so you will get 5 full years:
DECLARE #StartD DATETIME;
DECLARE #FinalD DATETIME;
SET #StartD = '12-jan-2010';
SET #FinalD = '11-jan-2016';
SELECT DATEDIFF(YEAR,#StartD,#FinalD)
- CASE WHEN DATEADD(YEAR,DATEDIFF(YEAR,#StartD,#FinalD),#StartD)>#FinalD
THEN 1 ELSE 0 END
First of all, I wish to thank all those that spent time trying to provide a simple and reliable solution. Finally, I decided to resolve it as follows:
Supposed that I want to know if #n full years passed between two dates, then:
DECLARE #n INT ;
DECLARE #Old_Date DATETIME ;
DECLARE #New_Date DATETIME ;
SET #n = <some_value> ;
SET #Old_Date = <some_value> ;
SET #New_Date = <some_value> ;
IF (DATEADD(YEAR ,#n , #Old_Date) <= #New_Date)
SET #Result = 'YES' ;
ELSE
SET #Result = 'NO' ;
[of course, a check needs to be included to verify that #Old_Date < #New_Date]
I can't say this can be proved (mathematically) as correct in all possible scenarios, but it provides the needed answer to me.
Thanks to all again.

SQL query with start and end dates - what is the best option?

I am using MS SQL Server 2005 at work to build a database. I have been told that most tables will hold 1,000,000 to 500,000,000 rows of data in the near future after it is built... I have not worked with datasets this large. Most of the time I don't even know what I should be considering to figure out what the best answer might be for ways to set up schema, queries, stuff.
So... I need to know the start and end dates for something and a value that is associated with in ID during that time frame. SO... we can the table up two different ways:
create table xxx_test2 (id int identity(1,1), groupid int, dt datetime, i int)
create table xxx_test2 (id int identity(1,1), groupid int, start_dt datetime, end_dt datetime, i int)
Which is better? How do I define better? I filled the first table with about 100,000 rows of data and it takes about 10-12 seconds to set up in the format of the second table depending on the query...
select y.groupid,
y.dt as [start],
z.dt as [end],
(case when z.dt is null then 1 else 0 end) as latest,
y.i
from #x as y
outer apply (select top 1 *
from #x as x
where x.groupid = y.groupid and
x.dt > y.dt
order by x.dt asc) as z
or
http://consultingblogs.emc.com/jamiethomson/archive/2005/01/10/t-sql-deriving-start-and-end-date-from-a-single-effective-date.aspx
Buuuuut... with the second table.... to insert a new row, I have to go look and see if there is a previous row and then if so update its end date. So... is it a question of performance when retrieving data vs insert/update things? It seems silly to store that end date twice but maybe...... not? What things should I be looking at?
this is what i used to generate my fake data... if you want to play with it for some reason (if you change the maximum of the random number to something higher it will generate the fake stuff a lot faster):
declare #dt datetime
declare #i int
declare #id int
set #id = 1
declare #rowcount int
set #rowcount = 0
declare #numrows int
while (#rowcount<100000)
begin
set #i = 1
set #dt = getdate()
set #numrows = Cast(((5 + 1) - 1) *
Rand() + 1 As tinyint)
while #i<=#numrows
begin
insert into #x values (#id, dateadd(d,#i,#dt), #i)
set #i = #i + 1
end
set #rowcount = #rowcount + #numrows
set #id = #id + 1
print #rowcount
end
For your purposes, I think option 2 is the way to go for table design. This gives you flexibility, and will save you tons of work.
Having the effective date and end date will allow you to have a query that will only return currently effective data by having this in your where clause:
where sysdate between effectivedate and enddate
You can also then use it to join with other tables in a time-sensitive way.
Provided you set up the key properly and provide the right indexes, performance (on this table at least) should not be a problem.
for anyone who can use LEAD Analytic function of SQL Server 2012 (or Oracle, DB2, ...), retrieving data from the 1st table (that uses only 1 date column) would be much much quicker than without this feature:
select
groupid,
dt "start",
lead(dt) over (partition by groupid order by dt) "end",
case when lead(dt) over (partition by groupid order by dt) is null
then 1 else 0 end "latest",
i
from x

Best way to calculate Max/Min of N columns in SQL Server

Ok, firstly I've seen this thread. But none of the solutions are very satisfactory. The nominated answer looks like NULLs would break it, and the highest-rated answer looks nasty to maintain.
So I was wondering about something like the following :
CREATE FUNCTION GetMaxDates
(
#dte1 datetime,
#dte2 datetime,
#dte3 datetime,
#dte4 datetime,
#dte5 datetime
)
RETURNS datetime
AS
BEGIN
RETURN (SELECT Max(TheDate)
FROM
(
SELECT #dte1 AS TheDate
UNION ALL
SELECT #dte2 AS TheDate
UNION ALL
SELECT #dte3 AS TheDate
UNION ALL
SELECT #dte4 AS TheDate
UNION ALL
SELECT #dte5 AS TheDate) AS Dates
)
END
GO
Main problems I see are that if there are only 3 fields to compare, you'd still have to specify NULL for the other 2, and if you wanted to extend it to six comparisons it would break existing use. If it was a parameterized stored procedure you could specify a default for each parameter, and adding new parameters wouldn't break existing references. The same method could also obviously be extended to other datatypes or stuff like Min or Avg. Is there some major drawback to this that I'm not spotting? Note that this function works whether some, all or none of the values passed to it are nulls or duplicates.
You can solve null issue with ISNULL function:
SELECT ISNULL(#dte1,0) AS TheDate
UNION ALL
SELECT ISNULL(#dte2,0) AS TheDate
UNION ALL
SELECT ISNULL(#dte3,0) AS TheDate
UNION ALL
SELECT ISNULL(#dte4,0) AS TheDate
UNION ALL
SELECT ISNULL(#dte5,0) AS TheDate) AS Dates
But it will only work with MAX functions.
Here is another suggestion: http://www.sommarskog.se/arrays-in-sql-2005.html
They suggest comma delimited values in a form of string.
The function takes as many parameters as you wish and looks like this:
CREATE FUNCTION GetMaxDate
(
#p_dates VARCHAR(MAX)
)
RETURNS DATETIME
AS
BEGIN
DECLARE #pos INT, #nextpos INT, #date_tmp DATETIME, #max_date DATETIME, #valuelen INT
SELECT #pos = 0, #nextpos = 1
SELECT #max_date = CONVERT(DATETIME,0)
WHILE #nextpos > 0
BEGIN
SELECT #nextpos = charindex(',', #p_dates, #pos + 1)
SELECT #valuelen = CASE WHEN #nextpos > 0
THEN #nextpos
ELSE len(#p_dates) + 1
END - #pos - 1
SELECT #date_tmp = CONVERT(DATETIME, substring(#p_dates, #pos + 1, #valuelen))
IF #date_tmp > #max_date
SET #max_date = #date_tmp
SELECT #pos = #nextpos
END
RETURN #max_date
END
And calling:
DECLARE #dt1 DATETIME
DECLARE #dt2 DATETIME
DECLARE #dt3 DATETIME
DECLARE #dt_string VARCHAR(MAX)
SET #dt1 = DATEADD(HOUR,3,GETDATE())
SET #dt2 = DATEADD(HOUR,-3,GETDATE())
SET #dt3 = DATEADD(HOUR,5,GETDATE())
SET #dt_string = CONVERT(VARCHAR(50),#dt1,21)+','+CONVERT(VARCHAR(50),#dt2,21)+','+CONVERT(VARCHAR(50),#dt3,21)
SELECT dbo.GetMaxDate(#dt_string)
Why not just:
SELECT Max(TheDate)
FROM
(
SELECT #dte1 AS TheDate WHERE #dte1 IS NOT NULL
UNION ALL
SELECT #dte2 AS TheDate WHERE #dte2 IS NOT NULL
UNION ALL
SELECT #dte3 AS TheDate WHERE #dte3 IS NOT NULL
UNION ALL
SELECT #dte4 AS TheDate WHERE #dte4 IS NOT NULL
UNION ALL
SELECT #dte5 AS TheDate WHERE #dte5 IS NOT NULL) AS Dates
That shoud take care of the null problem without introducing any new values
I would pass the Dates in XML (you could use varchar/etc, and convert to the xml datatype too):
DECLARE #output DateTime
DECLARE #test XML
SET #test = '<VALUES><VALUE>1</VALUE><VALUE>2</VALUE></VALUES>'
DECLARE #docHandle int
EXEC sp_xml_preparedocument #docHandle OUTPUT, #doc
SET #output = SELECT MAX(TheDate)
FROM (SELECT t.value('./VALUE[1]','DateTime') AS 'TheDate'
FROM OPENXML(#docHandle, '//VALUES', 1) t)
EXEC sp_xml_removedocument #docHandle
RETURN #output
That would address the issue of handling as many possibilities, and I wouldn't bother putting nulls in the xml.
I'd use a separate parameter to specify the datetype rather than customize the xml & supporting code every time, but you might need to use dynamic SQL for it to work.
A better option is to restructure the data to support column based min/max/avg as this is what SQL is best at.
In SQL Server 2005 you can use the UNPIVOT operator to perform the transformation.
Not always appropriate for every problem, but can make things easier if you can use it.
See:
http://msdn.microsoft.com/en-us/library/ms177410.aspx
http://blogs.msdn.com/craigfr/archive/2007/07/17/the-unpivot-operator.aspx
If you have to do it over one row only, it doesn't matter how you will do it (everything would be fast enough).
For selecting Min/Max/Avg value of several columns PER ROW, solution with UNPIVOT should be much faster than UDF
an other possibility is to create a custom table type, like this:
CREATE TYPE [Maps].[TblListInt] AS TABLE( [ID] [INT] NOT NULL )
then,
CREATE FUNCTION dbo.GetMax(#ids maps.TblListInt READONLY) RETURNS INT
BEGIN
RETURN (select max(id) from #ids)
END
Of course, you can swap "int" with your required type.