T-SQL: Looping through an array of known values - sql

Here's my scenario:
Let's say I have a stored procedure in which I need to call another stored procedure on a set of specific ids; is there a way to do this?
i.e. instead of needing to do this:
exec p_MyInnerProcedure 4
exec p_MyInnerProcedure 7
exec p_MyInnerProcedure 12
exec p_MyInnerProcedure 22
exec p_MyInnerProcedure 19
Doing something like this:
*magic where I specify my list contains 4,7,12,22,19*
DECLARE my_cursor CURSOR FAST_FORWARD FOR
*magic select*
OPEN my_cursor
FETCH NEXT FROM my_cursor INTO #MyId
WHILE ##FETCH_STATUS = 0
BEGIN
exec p_MyInnerProcedure #MyId
FETCH NEXT FROM my_cursor INTO #MyId
END
My Main goal here is simply maintainability (easy to remove/add id's as the business changes), being able to list out all Id's on a single line... Performance shouldn't be as big of an issue

declare #ids table(idx int identity(1,1), id int)
insert into #ids (id)
select 4 union
select 7 union
select 12 union
select 22 union
select 19
declare #i int
declare #cnt int
select #i = min(idx) - 1, #cnt = max(idx) from #ids
while #i < #cnt
begin
select #i = #i + 1
declare #id = select id from #ids where idx = #i
exec p_MyInnerProcedure #id
end

What I do in this scenario is create a table variable to hold the Ids.
Declare #Ids Table (id integer primary Key not null)
Insert #Ids(id) values (4),(7),(12),(22),(19)
-- (or call another table valued function to generate this table)
Then loop based on the rows in this table
Declare #Id Integer
While exists (Select * From #Ids)
Begin
Select #Id = Min(id) from #Ids
exec p_MyInnerProcedure #Id
Delete from #Ids Where id = #Id
End
or...
Declare #Id Integer = 0 -- assuming all Ids are > 0
While exists (Select * From #Ids
where id > #Id)
Begin
Select #Id = Min(id)
from #Ids Where id > #Id
exec p_MyInnerProcedure #Id
End
Either of above approaches is much faster than a cursor (declared against regular User Table(s)). Table-valued variables have a bad rep because when used improperly, (for very wide tables with large number of rows) they are not performant. But if you are using them only to hold a key value or a 4 byte integer, with a index (as in this case) they are extremely fast.

use a static cursor variable and a split function:
declare #comma_delimited_list varchar(4000)
set #comma_delimited_list = '4,7,12,22,19'
declare #cursor cursor
set #cursor = cursor static for
select convert(int, Value) as Id from dbo.Split(#comma_delimited_list) a
declare #id int
open #cursor
while 1=1 begin
fetch next from #cursor into #id
if ##fetch_status <> 0 break
....do something....
end
-- not strictly necessary w/ cursor variables since they will go out of scope like a normal var
close #cursor
deallocate #cursor
Cursors have a bad rep since the default options when declared against user tables can generate a lot of overhead.
But in this case the overhead is quite minimal, less than any other methods here. STATIC tells SQL Server to materialize the results in tempdb and then iterate over that. For small lists like this, it's the optimal solution.

You can try as below :
declare #list varchar(MAX), #i int
select #i=0, #list ='4,7,12,22,19,'
while( #i < LEN(#list))
begin
declare #item varchar(MAX)
SELECT #item = SUBSTRING(#list, #i,CHARINDEX(',',#list,#i)-#i)
select #item
--do your stuff here with #item
exec p_MyInnerProcedure #item
set #i = CHARINDEX(',',#list,#i)+1
if(#i = 0) set #i = LEN(#list)
end

I usually use the following approach
DECLARE #calls TABLE (
id INT IDENTITY(1,1)
,parameter INT
)
INSERT INTO #calls
select parameter from some_table where some_condition -- here you populate your parameters
declare #i int
declare #n int
declare #myId int
select #i = min(id), #n = max(id) from #calls
while #i <= #n
begin
select
#myId = parameter
from
#calls
where id = #i
EXECUTE p_MyInnerProcedure #myId
set #i = #i+1
end

CREATE TABLE #ListOfIDs (IDValue INT)
DECLARE #IDs VARCHAR(50), #ID VARCHAR(5)
SET #IDs = #OriginalListOfIDs + ','
WHILE LEN(#IDs) > 1
BEGIN
SET #ID = SUBSTRING(#IDs, 0, CHARINDEX(',', #IDs));
INSERT INTO #ListOfIDs (IDValue) VALUES(#ID);
SET #IDs = REPLACE(',' + #IDs, ',' + #ID + ',', '')
END
SELECT *
FROM #ListOfIDs

Make a connection to your DB using a procedural programming language (here Python), and do the loop there. This way you can do complicated loops as well.
# make a connection to your db
import pyodbc
conn = pyodbc.connect('''
Driver={ODBC Driver 13 for SQL Server};
Server=serverName;
Database=DBname;
UID=userName;
PWD=password;
''')
cursor = conn.cursor()
# run sql code
for id in [4, 7, 12, 22, 19]:
cursor.execute('''
exec p_MyInnerProcedure {}
'''.format(id))

Related

Using cursor to loop through a table variable in SQL Server

I have a parameter of a stored procedure which gets some data in the format
1/1/2018-2/1/2018,2/1/2018-3/1/2018,3/1/2018-4/1/2018,4/1/2018-5/1/2018,5/1/2018-6/1/2018,6/1/2018-7/1/2018,7/1/2018-8/1/2018,8/1/2018-9/1/2018,9/1/2018-10/1/2018,
10/1/2018-11/1/2018,11/1/2018-12/1/2018,12/1/2018-12/31/2018
I have a function which splits the data based on the , character and stores the results into a table variable as shown here:
declare #SPlitDates table(ItemNumber int, Item nvarchar(max))
insert into #SPlitDates
select *
from dbo.SPlitFunction(#RequestData, ',')
After this I have to perform certain operations on the data range so I use cursors to loop through the temp table as shown below
DECLARE cur CURSOR FOR
SELECT Item
FROM #SPlitDates
ORDER BY ItemNumber
OPEN cur
FETCH NEXT FROM cur INTO #monthStart
WHILE ##FETCH_STATUS = 0
BEGIN
-- Some operation
END
The max data points that I will get in the temp table is the date range for 12 months.
My question is that could I be using something else apart from cursors to improve performance or it doesn't matter when the dataset is really this small.
Thanks
Edit - To show operation inside the cursor
declare #SPlitDates table(ItemNumber int, Item nvarchar(max))
insert into #SPlitDates
select *
from dbo.SPlitFunction(#RequestData, ',')
declare #SPlitDatesData table (ItemNumber varchar(100), Item nvarchar(max))
declare #SPlitDatesAvgData table(Code nvarchar(100), Val decimal(18,2))
declare #dataFilter as nvarchar(max),
#SQL as nvarchar(max);
declare #monthStart nvarchar(100)
declare #count int
set #count = 0
--Declaring a cursor to loop through all the dates as defined in the requested quarter
DECLARE cur CURSOR FOR
SELECT Item
FROM #SPlitDates
ORDER BY ItemNumber
OPEN cur
FETCH NEXT FROM cur INTO #monthStart
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #Period NVARCHAR(100)
SET #Period = #monthStart
INSERT INTO #SPlitDatesData
--split the dates to get the start and the end dates
SELECT *
FROM dbo.SPlitFunction(#Period, '-')
DECLARE #PeriodStart NVARCHAR(100)
DECLARE #PeriodEnd NVARCHAR(100)
SET #PeriodStart = (SELECT Item FROM #SPlitDatesData WHERE ItemNumber = 1)
SET #PeriodEnd = (SELECT Item FROM #SPlitDatesData WHERE ItemNumber = 2)
DELETE FROM #SPlitDatesData
--add the start and end dates to the filter
SET #dataFilter = 'StatusDate between convert(datetime,('''+#PeriodStart+'''))
and DATEADD(dy, 1, convert(datetime,('''+#PeriodEnd+''')))'
SET #count = #count +1;
SET #SQL = 'INSERT INTO #BidAverageCycleCalculation (SortOrder, Code, Data)
VALUES (#count,
''SL Payroll'',(select dbo.GetAverageCycleBetweenBids('''+#PeriodStart+''',
'''+#PeriodEnd+''',''SL''))
)'
EXEC SP_ExecuteSQL #SQL, N'#count int', #count;
SET #count = #count +1;
SET #SQL = 'INSERT INTO #BidAverageCycleCalculation (SortOrder, Code, Data)
VALUES (#count,
''GV Payroll'',(select dbo.GetAverageCycleBetweenBids('''+#PeriodStart+''',
'''+#PeriodEnd+''',''GV''))
)'
EXEC SP_ExecuteSQL #SQL , N'#count int', #count;
SET #count = #count +1;
SET #SQL = 'Insert into #BidAverageCycleCalculation (SortOrder,Code,Data)
Values (#count,
''Global Payroll'',(select dbo.GetAverageCycleBetweenBids('''+#PeriodStart+''',
'''+#PeriodEnd+''',''GVS''))
)'
EXEC SP_ExecuteSQL #SQL, N'#count int', #count;
SET #count = #count +1;
SET #SQL = 'Insert into #BidAverageCycleCalculation (SortOrder,Code,Data)
Values (#count,
''TimeHCM'',(select dbo.GetAverageCycleBetweenBids('''+#PeriodStart+''',
'''+#PeriodEnd+''',''Time''))
)'
EXEC SP_ExecuteSQL #SQL, N'#count int', #count;
delete from #SPlitDatesAVgData
FETCH NEXT FROM cur INTO #monthStart
END
CLOSE cur
DEALLOCATE cur
This uses two parts - first convert your string into a table, then do bulk inserts into your destination. No need for cursors.
** Please excuse any syntax errors as I'm doing it without access to your actual tables or functions so cant test it, but you get the idea
declare #in varchar(max)
set #in= '1/1/2018-2/1/2018,2/1/2018-3/1/2018,3/1/2018-4/1/2018,4/1/2018-5/1/2018,5/1/2018-6/1/2018,6/1/2018-7/1/2018,7/1/2018-8/1/2018,8/1/2018-9/1/2018,9/1/2018-10/1/2018,10/1/2018-11/1/2018,11/1/2018-12/1/2018,12/1/2018-12/31/2018'
declare #xml xml;
set #xml= convert(xml,'<r><f>'+replace(replace(#in,',','</t></r><r><f>'),'-','</f><t>') +'</t></r>')
declare #t table(id int identity, f date, t date)
insert #t
select
Tbl.Col.value('f[1]', 'date') f,
Tbl.Col.value('t[1]', 'date') t
FROM #xml.nodes('//r') Tbl(Col)
select * from #t
declare #count int;
select #count=count(*) from #t
INSERT INTO #BidAverageCycleCalculation (SortOrder, Code, Data)
select id, 'SL Payroll',(select dbo.GetAverageCycleBetweenBids(f,t,'SL')) from #t
INSERT INTO #BidAverageCycleCalculation (SortOrder, Code, Data)
select id+#count,'GV Payroll',(select dbo.GetAverageCycleBetweenBids(f,t,'GV')) from #t
INSERT INTO #BidAverageCycleCalculation (SortOrder, Code, Data)
select id+#count*2,'Global Payroll',(select dbo.GetAverageCycleBetweenBids(f,t,'GVS')) from #t
INSERT INTO #BidAverageCycleCalculation (SortOrder, Code, Data)
select id+#count*3,'TimeHCM',(select dbo.GetAverageCycleBetweenBids(f,t,'Time')) from #t

Printing all values of two columns in sql stored procedure

I have been trying to print all values of two columns of table using loop in sql stored procedure but no luck yet.
CREATE PROCEDURE [usp_my_procedure_name]
AS
SET NOCOUNT ON;
BEGIN
DECLARE #User_ID INT =16
DECLARE #ID INT
DECLARE #Count INT
DECLARE #Count1 INT
DECLARE #Code VARCHAR(500)
SELECT #Count1= MAX(ID), #Count = MIN(ID)
FROM ABC
WHERE ID = 10 AND Code NOT LIKE '%ABC%'
WHILE (#Count <= #count1)
BEGIN
SELECT #ID = (ID), #Code = Code
FROM ABC
WHERE ID = 10 AND Code NOT LIKE '%ABC%
PRINT #ID
PRINT #Code
SET #Count = #Count + 1
END
END
Also how to optimize it further as i have to traverse for 7k records
Try this, and share with us what it gives you, and what the ideal result would look like (also share some of the input rows from ABC).
CREATE PROCEDURE [usp_my_procedure_name]
AS
SET NOCOUNT ON;
BEGIN
SELECT Distinct ID, Code
FROM ABC
WHERE Code NOT LIKE '%ABC%
ORDER BY ID
END

SQL Server INSERT in Loop

I have this SQL Script:
DECLARE #Loop INT
SET #Loop = 1
DECLARE #Result table([1] int, [2] int,[3] int,[4] int,[5]);
WHILE (#Loop <=5)
BEGIN
INSERT INTO #Result(#Loop)
SELECT Id FROM Students WHERE Id=#Loop
SET #Loop= #Loop+ 1
END
I got an error in this line:
INSERT INTO #Result(#Loop)
Is it possible to use this way to insert data into column names using loop ? I mean dynamicaly
Thanks
This is as close as I can get to what you have.
I'm using dynamic sql to build the insert sql.
I'm using a temporary table #Result instead of a variable #Result because the scope of the #Result variable would mean that this technique would not work.
DECLARE #Loop INT;
SET #Loop = 1;
CREATE TABLE #Result([1] int, [2] int, [3] int, [4] int, [5] int);
DECLARE #Sql AS NVARCHAR(MAX);
DECLARE #LoopNVARCHAR AS NVARCHAR(10);
WHILE (#Loop <= 5)
BEGIN
SET #LoopNVARCHAR = CAST(#Loop AS NVARCHAR(10));
SET #Sql = N'INSERT INTO #Result([' + #LoopNVARCHAR + ']) SELECT Id FROM Students WHERE Id = ' + #LoopNVARCHAR;
exec (#Sql)
SET #Loop = #Loop + 1
END
SELECT * FROM #Result
DROP TABLE #Result

How do I loop through a set of records in SQL Server?

How do I loop through a set of records from a select statement?
Say I have a few records that I wish to loop through and do something with each record. Here's a primitive version of my select statement:
select top 1000 * from dbo.table
where StatusID = 7
By using T-SQL and cursors like this :
DECLARE #MyCursor CURSOR;
DECLARE #MyField YourFieldDataType;
BEGIN
SET #MyCursor = CURSOR FOR
select top 1000 YourField from dbo.table
where StatusID = 7
OPEN #MyCursor
FETCH NEXT FROM #MyCursor
INTO #MyField
WHILE ##FETCH_STATUS = 0
BEGIN
/*
YOUR ALGORITHM GOES HERE
*/
FETCH NEXT FROM #MyCursor
INTO #MyField
END;
CLOSE #MyCursor ;
DEALLOCATE #MyCursor;
END;
This is what I've been doing if you need to do something iterative... but it would be wise to look for set operations first. Also, do not do this because you don't want to learn cursors.
select top 1000 TableID
into #ControlTable
from dbo.table
where StatusID = 7
declare #TableID int
while exists (select * from #ControlTable)
begin
select top 1 #TableID = TableID
from #ControlTable
order by TableID asc
-- Do something with your TableID
delete #ControlTable
where TableID = #TableID
end
drop table #ControlTable
Small change to sam yi's answer (for better readability):
select top 1000 TableID
into #ControlTable
from dbo.table
where StatusID = 7
declare #TableID int
while exists (select * from #ControlTable)
begin
select #TableID = (select top 1 TableID
from #ControlTable
order by TableID asc)
-- Do something with your TableID
delete #ControlTable
where TableID = #TableID
end
drop table #ControlTable
By using cursor you can easily iterate through records individually and print records separately or as a single message including all the records.
DECLARE #CustomerID as INT;
declare #msg varchar(max)
DECLARE #BusinessCursor as CURSOR;
SET #BusinessCursor = CURSOR FOR
SELECT CustomerID FROM Customer WHERE CustomerID IN ('3908745','3911122','3911128','3911421')
OPEN #BusinessCursor;
FETCH NEXT FROM #BusinessCursor INTO #CustomerID;
WHILE ##FETCH_STATUS = 0
BEGIN
SET #msg = '{
"CustomerID": "'+CONVERT(varchar(10), #CustomerID)+'",
"Customer": {
"LastName": "LastName-'+CONVERT(varchar(10), #CustomerID) +'",
"FirstName": "FirstName-'+CONVERT(varchar(10), #CustomerID)+'",
}
}|'
print #msg
FETCH NEXT FROM #BusinessCursor INTO #CustomerID;
END
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,
)
--create some testing data
--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, #Counter INT
SET #COUNTER = 1
SELECT #MAXID = COUNT(*) FROM #TempTable
WHILE (#COUNTER <= #MAXID)
BEGIN
--DO THE PROCESSING HERE
SELECT #HIERARCHY_ID_TO_UPDATE = PT.HIERARCHY_ID_TO_UPDATE
FROM #TempTable AS PT
WHERE ROWID = #COUNTER
SET #COUNTER = #COUNTER + 1
END
IF (OBJECT_ID('tempdb..#TempTable') IS NOT NULL)
BEGIN
DROP TABLE #TempTable
END
You could choose to rank your data and add a ROW_NUMBER and count down to zero while iterate your dataset.
-- Get your dataset and rank your dataset by adding a new row_number
SELECT TOP 1000 A.*, ROW_NUMBER() OVER(ORDER BY A.ID DESC) AS ROW
INTO #TEMPTABLE
FROM DBO.TABLE AS A
WHERE STATUSID = 7;
--Find the highest number to start with
DECLARE #COUNTER INT = (SELECT MAX(ROW) FROM #TEMPTABLE);
DECLARE #ROW INT;
-- Loop true your data until you hit 0
WHILE (#COUNTER != 0)
BEGIN
SELECT #ROW = ROW
FROM #TEMPTABLE
WHERE ROW = #COUNTER
ORDER BY ROW DESC
--DO SOMTHING COOL
-- SET your counter to -1
SET #COUNTER = #ROW -1
END
DROP TABLE #TEMPTABLE
this way we can iterate into table data.
DECLARE #_MinJobID INT
DECLARE #_MaxJobID INT
CREATE TABLE #Temp (JobID INT)
INSERT INTO #Temp SELECT * FROM DBO.STRINGTOTABLE(#JobID,',')
SELECT #_MinJID = MIN(JobID),#_MaxJID = MAX(JobID) FROM #Temp
WHILE #_MinJID <= #_MaxJID
BEGIN
INSERT INTO Mytable
(
JobID,
)
VALUES
(
#_MinJobID,
)
SET #_MinJID = #_MinJID + 1;
END
DROP TABLE #Temp
STRINGTOTABLE is user define function which will parse comma separated data and return table. thanks
I think this is the easy way example to iterate item.
declare #cateid int
select CateID into [#TempTable] from Category where GroupID = 'STOCKLIST'
while (select count(*) from #TempTable) > 0
begin
select top 1 #cateid = CateID from #TempTable
print(#cateid)
--DO SOMETHING HERE
delete #TempTable where CateID = #cateid
end
drop table #TempTable

Return value from exec(#sql)

I want get the value from Exec(#sql) and assign to #Rowcount(int)
Here is my query:
'SET #RowCount = (select count(*)
FROM dbo.Comm_Services
WHERE CompanyId = '+cast(#CompanyId as char)+' and '+#condition+')'
On the one hand you could use sp_executesql:
exec sp_executesql N'select #rowcount=count(*) from anytable',
N'#rowcount int output', #rowcount output;
On the other hand you could use a temporary table:
declare #result table ([rowcount] int);
insert into #result ([rowcount])
exec (N'select count(*) from anytable');
declare #rowcount int = (select top (1) [rowcount] from #result);
DECLARE #nReturn int = 0
EXEC #nReturn = Stored Procedure
Was playing with this today... I believe you can also use ##ROWCOUNT, like this:
DECLARE #SQL VARCHAR(50)
DECLARE #Rowcount INT
SET #SQL = 'SELECT 1 UNION SELECT 2'
EXEC(#SQL)
SET #Rowcount = ##ROWCOUNT
SELECT #Rowcount
Then replace the SELECT 1 UNION SELECT 2 with your actual select without the count. I'd suggest just putting 1 in your select, like this:
SELECT 1
FROM dbo.Comm_Services
WHERE....
....
(as opposed to putting SELECT *)
Hope that helps.
that's my procedure
CREATE PROC sp_count
#CompanyId sysname,
#codition sysname
AS
SET NOCOUNT ON
CREATE TABLE #ctr
( NumRows int )
DECLARE #intCount int
, #vcSQL varchar(255)
SELECT #vcSQL = ' INSERT #ctr FROM dbo.Comm_Services
WHERE CompanyId = '+#CompanyId+' and '+#condition+')'
EXEC (#vcSQL)
IF ##ERROR = 0
BEGIN
SELECT #intCount = NumRows
FROM #ctr
DROP TABLE #ctr
RETURN #intCount
END
ELSE
BEGIN
DROP TABLE #ctr
RETURN -1
END
GO
If i understand you correctly, (i probably don't)
'SELECT #RowCount = COUNT(*)
FROM dbo.Comm_Services
WHERE CompanyId = ' + CAST(#CompanyId AS CHAR) + '
AND ' + #condition