T-SQL loop over query results - tsql

I run a query select #id=table.id from table and I need to loop over the results so I can exec a store procedure for each row exec stored_proc #varName=#id,#otherVarName='test'
How can I do this in a T-SQL script?

You could use a CURSOR in this case:
DECLARE #id INT
DECLARE #name NVARCHAR(100)
DECLARE #getid CURSOR
SET #getid = CURSOR FOR
SELECT table.id,
table.name
FROM table
OPEN #getid
FETCH NEXT
FROM #getid INTO #id, #name
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC stored_proc #varName=#id, #otherVarName='test', #varForName=#name
FETCH NEXT
FROM #getid INTO #id, #name
END
CLOSE #getid
DEALLOCATE #getid
Modified to show multiple parameters from the table.

You could do something like this:
create procedure test
as
BEGIN
create table #ids
(
rn int,
id int
)
insert into #ids (rn, id)
select distinct row_number() over(order by id) as rn, id
from table
declare #id int
declare #totalrows int = (select count(*) from #ids)
declare #currentrow int = 0
while #currentrow < #totalrows
begin
set #id = (select id from #ids where rn = #currentrow + 1)
exec stored_proc #varName=#id, #otherVarName='test'
set #currentrow = #currentrow +1
end
END

My prefer solution is Microsoft KB 111401 http://support.microsoft.com/kb/111401.
The link refers to 3 examples:
This article describes various methods that you can use to simulate a cursor-like FETCH-NEXT logic in a stored procedure, trigger, or Transact-SQL batch.
/*********** example 1 ***********/
declare #au_id char( 11 )
set rowcount 0
select * into #mytemp from authors
set rowcount 1
select #au_id = au_id from #mytemp
while ##rowcount <> 0
begin
set rowcount 0
select * from #mytemp where au_id = #au_id
delete #mytemp where au_id = #au_id
set rowcount 1
select #au_id = au_id from #mytemp
end
set rowcount 0
/********** example 2 **********/
declare #au_id char( 11 )
select #au_id = min( au_id ) from authors
while #au_id is not null
begin
select * from authors where au_id = #au_id
select #au_id = min( au_id ) from authors where au_id > #au_id
end
/********** example 3 **********/
set rowcount 0
select NULL mykey, * into #mytemp from authors
set rowcount 1
update #mytemp set mykey = 1
while ##rowcount > 0
begin
set rowcount 0
select * from #mytemp where mykey = 1
delete #mytemp where mykey = 1
set rowcount 1
update #mytemp set mykey = 1
end
set rowcount 0

DECLARE #id INT
DECLARE #name NVARCHAR(100)
DECLARE #getid CURSOR
SET #getid = CURSOR FOR
SELECT table.id,
table.name
FROM table
WHILE 1=1
BEGIN
FETCH NEXT
FROM #getid INTO #id, #name
IF ##FETCH_STATUS < 0 BREAK
EXEC stored_proc #varName=#id, #otherVarName='test', #varForName=#name
END
CLOSE #getid
DEALLOCATE #getid

try this:
declare #i tinyint = 0,
#count tinyint,
#id int,
#name varchar(max)
select #count = count(*) from table
while (#i < #count)
begin
select #id = id, #name = name from table
order by nr asc offset #i rows fetch next 1 rows only
exec stored_proc #varName = #id, #otherVarName = 'test', #varForName = #name
set #i = #i + 1
end

DECLARE #id INT
DECLARE #filename NVARCHAR(100)
DECLARE #getid CURSOR
SET #getid = CURSOR FOR
SELECT top 3 id,
filename
FROM table
OPEN #getid
WHILE 1=1
BEGIN
FETCH NEXT
FROM #getid INTO #id, #filename
IF ##FETCH_STATUS < 0 BREAK
print #id
END
CLOSE #getid
DEALLOCATE #getid

Related

TSQL - How to iterate a list of strings

I want to create a procedure that will insert all my jobs to the DB.
(a. All my jobs have equal characteristics. b. SSDT doesn't support jobs code management)
Now, I thought to create a script to insert all of them and as a c# developer I thought I need to initialize a list with their names.
I discovered while googling that the way to do it is with an in memory table and the best I could come with is this.
declare #jobsNames table(Id int, JobName nvarchar(100))
insert into #jobsNames (Id,JobName)
select 1,'JobName1' union
select 2,'JobName2' union
......
BEGIN TRANSACTION
DECLARE JobsCursor CURSOR FOR SELECT JobName FROM #jobsNames
OPEN JobsCursor
FETCH NEXT FROM JobsCursor INTO #JobName
WHILE ##Fetch_status = 0
BEGIN
.. do stuff
FETCH NEXT FROM JobsCursor INTO #JobName
WHILE ##Fetch_status = 0
END
COMMIT TRANSACTION
Question -
Is this the shortest/recommended way?
(It seems a hellotof code for a foreach)
declare #jobNames table(Id int, JobName nvarchar(100))
insert #jobNames values
(1, 'JobName1'),
(2, 'JobName2'),
--
(10, 'JobName10')
while exists(select 1 from #jobNames)
begin
declare #id int, #name nvarchar(100)
select top 1 #id = Id, #name = JobName from #jobNames
delete from #jobNames where Id = #Id
-- Do stuff here
end
Personally I avoid Cursors like the plague. Please make sure that you HAVE to iterate instead of doing your work set based. They don't call it RBAR for nothing.
DECLARE #counter INT, #max INT
SELECT #counter = 1, #max = max(id)
FROM #jobsNames
WHILE #counter <= #max
BEGIN
SELECT #val1 = val1 ... FROM #jobNames where ID = #counter
-- .. do stuff
SET #counter = #counter + 1
END

How does one use loops in TSQL?

In TSQL, I would like to change the following code from have to use hard coded dhomes to using a loop for optimization. My failed attempt at trying to add a loop is also included.
Declare #dhome Tinyint, #bp smallint, #lr smallint, #q smallint
// Set #dhome = 1
While(#dhome <= 3) // My attempt to add a loop
SELECT #lr = MAX(NQdDate), #q = NQd
FROM NQdHistory
WHERE dhomeId = #dhome
GROUP BY NQdDate, NQd
SELECT #bd = COUNT(*)
FROM bdhome
WHERE NQdDate= #lr AND dhomeID= #dhome
DELETE FROM ND1 WITH(XLOCK)
WHERE dhomeID= #dhome AND NQdDate= #lr
UPDATE NQdHistory
SET Nbd = #q - ##RowCount - #bp, NBd = #bp
WHERE NQdDate= #lr AND dhomeID= #dhome
Set #dhome = #dhome +1 //My attempt to end a loop
You're on the right track. You're missing your begin and end. Also, be sure to give #dhome a value. It looks like you started to and have it commented out on your third line:
Declare #dhome Tinyint, #bp smallint, #lr smallint, #q smallint
// Set #dhome = 1
While(#dhome <= 3) // My attempt to add a loop
begin
SELECT #lr = MAX(NQdDate), #q = NQd
FROM NQdHistory
WHERE dhomeId = #dhome
GROUP BY NQdDate, NQd
SELECT #bd = COUNT(*)
FROM bdhome
WHERE NQdDate= #lr AND dhomeID= #dhome
DELETE FROM ND1 WITH(XLOCK)
WHERE dhomeID= #dhome AND NQdDate= #lr
UPDATE NQdHistory
SET Nbd = #q - ##RowCount - #bp, NBd = #bp
WHERE NQdDate= #lr AND dhomeID= #dhome
Set #dhome = #dhome +1 //My attempt to end a loop
end
If you're familiar with C/C#/C++, think of T-SQL's Begin and End like curly braces { and }, if you're more familiar with VB Then and End If. Or more like pascals Begin and End. You get the idea :)
Missing a begin and end on your while.
WHILE (Transact-SQL)
Example 1
DECLARE #I INT,#COUNTVAR INT
SET #I = 1
DECLARE #Parent_Child TABLE(ID INT IDENTITY(1,1),ParentPositionID INT NULL,ChildPositionId Int)
INSERT INTO #Parent_Child(ParentPositionID,ChildPositionId)
SELECT DISTINCT PARENT_POSITION_ID,CHILD_POSITION_ID from tblPOSITION_HIERARCHY
--WHERE CHILD_POSITION_ID IN (--YOUR CONDITION IF ANY)
SELECT #COUNTVAR =COUNT(*) FROM #PTS_Parent_Child
DECLARE #int_SUPE_POSITION_ID INT, #int_CHILD_POSITION_ID INT
--loop through records here
WHILE #I <= #COUNTVAR
BEGIN
SELECT #int_SUPE_POSITION_ID=ParentPositionID,#int_CHILD_POSITION_ID=ChildPositionId FROM #Parent_Child WHERE ID=#I
--Whatever you want to do with records
SET #I=#I+1
END
Example 2
Just another approach if you are fine using temp tables.I have personally tested this and it will not cause any exception (even if temp table does not have any data.)
CREATE TABLE #TempTable
(
ROWID int identity(1,1) primary key,
HIERARCHY_ID_TO_UPDATE int,
)
--INSERT DATA INTO TEMP TABLE USING INSERT INTO CLAUSE OR FOR EAXMPLE BELOW
--INSERT INTO #TempTable VALUES(1)
--INSERT INTO #TempTable VALUES(2)
--INSERT INTO #TempTable VALUES(4)
--INSERT INTO #TempTable VALUES(6)
--INSERT INTO ##TempTable VALUES(8)
DECLARE #MAXID INT
SET #COUNTER =1
SELECT #MAXID=COUNT(*) FROM #TempTable
--PRINT #MAXID
WHILE (#MAXID > 0)
BEGIN
--DO THE PROCESSING HERE
SELECT #HIERARCHY_ID_TO_UPDATE =PT.HIERARCHY_ID_TO_UPDATE FROM #TempTable PT WHERE ROWID=#COUNTER
--PRINT '#MAXID VALUE '
--PRINT #MAXID
SET #MAXID=#MAXID-1
SET #COUNTER =#COUNTER + 1
End
If(OBJECT_ID('tempdb..#TempTable') IS NOT NULL)
BEGIN
DROP TABLE #TempTable
END

How to extract catalog name from connection string using TSQL?

Say I have the following string value:
declare #cs nvarchar(100) =
'Data Source=server\instance;Initial Catalog=MyDatabase;Integrated Security=True';
What is the T-SQL to extract the string MyDatabase from #cs?
Alternately, what T-SQL function(s) should I look at for a means to figure it out for myself?
It's not pretty and i'm sure you could do this faster using regex / clr patindex & substrings, but a very quick and easy way is to use the split functions below to do something like this:
Example Usage:
DECLARE #cs NVARCHAR(100) = 'Data Source=server\instance;Initial Catalog=MyDatabase;Integrated Security=True';
SELECT id ,
Data ,
dbo.fnParseString(2, '=', Data)
FROM dbo.fnc_Split(#cs, ';')
WHERE Data LIKE '%Initial Catalog%'
Returns:
2 Initial Catalog=MyDatabase MyDatabase
As for the split functions, the first splts strings into tables, and the second splits strings into columns:
Split into tables:
CREATE FUNCTION dbo.fnc_Split
(
#Data VARCHAR(2000) ,
#Sep VARCHAR(5)
)
RETURNS #Temp TABLE
(
Id INT IDENTITY(1, 1) ,
Data NVARCHAR(100)
)
AS
BEGIN
DECLARE #Cnt INT
SET #Cnt = 1
WHILE ( CHARINDEX(#Sep, #Data) > 0 )
BEGIN
INSERT INTO #Temp
( data
)
SELECT Data = LTRIM(RTRIM(SUBSTRING(#Data, 1, CHARINDEX(#Sep, #Data) - 1)))
SET #Data = SUBSTRING(#Data, CHARINDEX(#Sep, #Data) + 1, LEN(#Data))
SET #Cnt = #Cnt + 1
END
INSERT INTO #Temp
( data )
SELECT Data = LTRIM(RTRIM(#Data))
RETURN
END
GO
Split into columns:
CREATE FUNCTION dbo.fnParseString
(
#Section SMALLINT ,
#Delimiter CHAR ,
#Text VARCHAR(MAX)
)
RETURNS VARCHAR(8000)
AS
BEGIN
DECLARE #NextPos SMALLINT ,
#LastPos SMALLINT ,
#Found SMALLINT
--#### Uncomment the following 2 lines to emulate PARSENAME functionality
--IF #Section > 0
-- SELECT #Text = REVERSE(#Text)
SELECT #NextPos = CHARINDEX(#Delimiter, #Text, 1) ,
#LastPos = 0 ,
#Found = 1
WHILE #NextPos > 0
AND ABS(#Section) <> #Found
SELECT #LastPos = #NextPos ,
#NextPos = CHARINDEX(#Delimiter, #Text, #NextPos + 1) ,
#Found = #Found + 1
RETURN CASE
WHEN #Found <> ABS(#Section) OR #Section = 0 THEN NULL
--#### Uncomment the following lines to emulate PARSENAME functionality
--WHEN #Section > 0 THEN REVERSE(SUBSTRING(#Text, #LastPos + 1, CASE WHEN #NextPos = 0 THEN DATALENGTH(#Text) - #LastPos ELSE #NextPos - #LastPos - 1 END))
WHEN #Section > 0 THEN SUBSTRING(#Text, #LastPos + 1, CASE WHEN #NextPos = 0 THEN DATALENGTH(#Text) - #LastPos ELSE #NextPos - #LastPos - 1 END)
ELSE SUBSTRING(#Text, #LastPos + 1, CASE WHEN #NextPos = 0 THEN DATALENGTH(#Text) - #LastPos ELSE #NextPos - #LastPos - 1 END)
END
END
you might find this to be a lot easier, just change the from_table to your table and the connection string field to your field.
SELECT SUBSTRING(SUBSTRING([ConnectionString], CHARINDEX('Initial Catalog=', [ConnectionString]) + LEN('Initial Catalog='), 100), 0, CHARINDEX(';', SUBSTRING([ConnectionString], CHARINDEX('Initial Catalog=', [ConnectionString]) + LEN('Initial Catalog='), 100)))
AS DbName
FROM [from_table]
i assume after taking the strings you might want to do something like this:
(iteration over the names and query some dbs)
DECLARE #DbName varchar(max)
DECLARE #QaQuery NVARCHAR(MAX);
DECLARE MY_CURSOR CURSOR
LOCAL STATIC READ_ONLY FORWARD_ONLY
FOR
SELECT SUBSTRING(SUBSTRING([ConnectionString], CHARINDEX('Initial Catalog=', [ConnectionString]) + LEN('Initial Catalog='), 100), 0, CHARINDEX(';', SUBSTRING([ConnectionString], CHARINDEX('Initial Catalog=', [ConnectionString]) + LEN('Initial Catalog='), 100)))
AS Connections
FROM [ConnTable]
OPEN MY_CURSOR
FETCH NEXT FROM MY_CURSOR INTO #DbName
WHILE ##FETCH_STATUS = 0
BEGIN
--PRINT #DbName
SET #QaQuery = N'SELECT TOP 10 [RecordID],[SomeStuff]
FROM ['+#DbName+'].[dbo].[SomeTable]
WHERE [SomeStuff] IS NOT NULL'
EXEC sp_executesql #QaQuery;
FETCH NEXT FROM MY_CURSOR INTO #DbName
END
CLOSE MY_CURSOR
DEALLOCATE MY_CURSOR

Assign a list of integers to an #var

I can:
declare #idOrder int
set #idOrder = 21319
I want:
declare #idOrder int
set #idOrder = (21319, 21320)
for use in a series of statements where the 'WHERE' clause uses the IN operator
delete Orders
where idOrder in #idOrder
instead of
delete Orders
where idOrder in (21319, 21320)
You can't do that as long as it's an int, as that's not a valid value for that datatype. A datatype that could take several integers is a table
declare #idOrder table (id int)
insert into #idOrder values(21319)
insert into #idOrder values(21320)
delete from Orders where idOrder in (select id from #idOrder)
In SQL Server you can also
CREATE FUNCTION [dbo].[fn_ado_param_int] (#ado nvarchar(4000))
RETURNS #VALUES TABLE (ado int)AS
BEGIN
declare #Delim char(1)
set #Delim = ','
DECLARE #chrind INT
DECLARE #Piece nvarchar(4000)
SELECT #chrind = 1
WHILE #chrind > 0
BEGIN
SELECT #chrind = CHARINDEX(#Delim,#ado)
IF #chrind > 0
SELECT #Piece = LEFT(#ado,#chrind - 1)
ELSE
SELECT #Piece = #ado
INSERT #VALUES(ado) VALUES(#Piece)
SELECT #ado = RIGHT(#ado,LEN(#ado) - #chrind)
IF LEN(#ado) = 0 BREAK
END
RETURN
END
declare #idOrder varchar(500);
set #inOrder = "21319,2138,2138";
delete from Orders where id in (select ado from dbo.fn_ado_param_int(#idOrder));

How to get sp_executesql result into a variable?

I have a piece of dynamic SQL I need to execute, I then need to store the result into a variable.
I know I can use sp_executesql but can't find clear examples around about how to do this.
If you have OUTPUT parameters you can do
DECLARE #retval int
DECLARE #sSQL nvarchar(500);
DECLARE #ParmDefinition nvarchar(500);
DECLARE #tablename nvarchar(50)
SELECT #tablename = N'products'
SELECT #sSQL = N'SELECT #retvalOUT = MAX(ID) FROM ' + #tablename;
SET #ParmDefinition = N'#retvalOUT int OUTPUT';
EXEC sp_executesql #sSQL, #ParmDefinition, #retvalOUT=#retval OUTPUT;
SELECT #retval;
But if you don't, and can not modify the SP:
-- Assuming that your SP return 1 value
create table #temptable (ID int null)
insert into #temptable exec mysp 'Value1', 'Value2'
select * from #temptable
Not pretty, but works.
DECLARE #vi INT
DECLARE #vQuery NVARCHAR(1000)
SET #vQuery = N'SELECT #vi= COUNT(*) FROM <TableName>'
EXEC SP_EXECUTESQL
#Query = #vQuery
, #Params = N'#vi INT OUTPUT'
, #vi = #vi OUTPUT
SELECT #vi
DECLARE #tab AS TABLE (col1 VARCHAR(10), col2 varchar(10))
INSERT into #tab EXECUTE sp_executesql N'
SELECT 1 AS col1, 2 AS col2
UNION ALL
SELECT 1 AS col1, 2 AS col2
UNION ALL
SELECT 1 AS col1, 2 AS col2'
SELECT * FROM #tab
Return values are generally not used to "return" a result but to return success (0) or an error number (1-65K). The above all seem to indicate that sp_executesql does not return a value, which is not correct. sp_executesql will return 0 for success and any other number for failure.
In the below, #i will return 2727
DECLARE #s NVARCHAR(500)
DECLARE #i INT;
SET #s = 'USE [Blah]; UPDATE STATISTICS [dbo].[TableName] [NonExistantStatisticsName];';
EXEC #i = sys.sp_executesql #s
SELECT #i AS 'Blah'
SSMS will show this
Msg 2727, Level 11, State 1, Line 1
Cannot find index 'NonExistantStaticsName'.
If you want to return more than 1 value use this:
DECLARE #sqlstatement2 NVARCHAR(MAX);
DECLARE #retText NVARCHAR(MAX);
DECLARE #ParmDefinition NVARCHAR(MAX);
DECLARE #retIndex INT = 0;
SELECT #sqlstatement = 'SELECT #retIndexOUT=column1 #retTextOUT=column2 FROM XXX WHERE bla bla';
SET #ParmDefinition = N'#retIndexOUT INT OUTPUT, #retTextOUT NVARCHAR(MAX) OUTPUT';
exec sp_executesql #sqlstatement, #ParmDefinition, #retIndexOUT=#retIndex OUTPUT, #retTextOUT=#retText OUTPUT;
returned values are in #retIndex and #retText
Declare #variable int
Exec #variable = proc_name
DECLARE #ValueTable TABLE
(
Value VARCHAR (100)
)
SELECT #sql = N'SELECT SRS_SizeSetDetails.'+#COLUMN_NAME+' FROM SRS_SizeSetDetails WHERE FSizeID = '''+#FSizeID+''' AND SRS_SizeSetID = '''+#SRS_SizeSetID+'''';
INSERT INTO #ValueTable
EXEC sp_executesql #sql;
SET #Value='';
SET #Value = (SELECT TOP 1 Value FROM #ValueTable)
DELETE FROM #ValueTable
This worked for me:
DECLARE #SQL NVARCHAR(4000)
DECLARE #tbl Table (
Id int,
Account varchar(50),
Amount int
)
-- Lots of code to Create my dynamic sql statement
insert into #tbl EXEC sp_executesql #SQL
select * from #tbl
Here's something you can try
DECLARE #SqlStatement NVARCHAR(MAX) = ''
,#result XML
,#DatabaseName VARCHAR(100)
,#SchemaName VARCHAR(10)
,#ObjectName VARCHAR(200);
SELECT #DatabaseName = 'some database'
,#SchemaName = 'some schema'
,#ObjectName = 'some object (Table/View)'
SET #SqlStatement = '
SELECT #result = CONVERT(XML,
STUFF( ( SELECT *
FROM
(
SELECT TOP(100)
*
FROM ' + QUOTENAME(#DatabaseName) +'.'+ QUOTENAME(#SchemaName) +'.' + QUOTENAME(#ObjectName) + '
) AS A1
FOR XML PATH(''row''), ELEMENTS, ROOT(''recordset'')
), 1, 0, '''')
)
';
EXEC sp_executesql #SqlStatement,N'#result XML OUTPUT', #result = #result OUTPUT;
SELECT DISTINCT
QUOTENAME(r.value('fn:local-name(.)', 'VARCHAR(200)')) AS ColumnName
FROM #result.nodes('//recordset/*/*') AS records(r)
ORDER BY ColumnName
This was a long time ago, so not sure if this is still needed, but you could use ##ROWCOUNT variable to see how many rows were affected with the previous sql statement.
This is helpful when for example you construct a dynamic Update statement and run it with exec. ##ROWCOUNT would show how many rows were updated.
Here is the definition