Insert query in while loop in a function - sql

I need to write a SQL function that should return temp table with 2 columns. But I want to use while loop. I want to insert multiple insert queries to temp table.
But when I am using insert query in while loop in SQL function, it is giving the empty result. Following is my case.
Code something like this:
create function dbo.fn_GetSubTree1(#type as Varchar(50))
returns #tree table
(sizename Varchar(9) not null,shiptotal int)
as
BEGIN
Declare #maxsize int;
Declare #counter int=0;
WHILE #counter < 24
BEGIN
insert into #tree(sizename,shiptotal) select s.size,s.shp from style st join scale s on st.scale=s.scale and s.nrfkey='' and s.prepak=''
SET #counter = #counter + 1;
END
return
End

You have WHILE #counter < 24, but never initialise the counter to any value.
The line DECLARE #counter INT; doesn't initialise the variable to 0, it initialises it to NULL. Which means your first loop is checking NULL < 24 which isn't TRUE.
Try this...
Declare #counter int = 0;
...
WHILE #counter < 24
BEGIN
...
SET #counter = #counter + 1;
END
You also seem to have other issues, such as select ' + #Size + ','+#Shp + ' from ... making no apparent sense.
EDIT:
I recommend starting with making a simple case work and building it up from there. If it ever stops working, the problem is what ever you last changed.
CREATE FUNCTION dbo.fn_GetSubTree1(
#type AS NVARCHAR(50)
)
RETURNS #tree TABLE (
sizename NVARCHAR(9) NOT NULL,
shiptotal INT
)
AS
BEGIN
DECLARE #counter INT = 0;
WHILE (#counter < 24)
BEGIN
INSERT INTO
#tree(
sizename,
shiptotal
)
VALUES (
'Test',
#counter
)
SET #counter = #counter + 1;
END
RETURN
END

Related

SQL Server stored procedure with while condition containing table variable

I have a table where the name of the country changes regularly, like my_table_US_NA, my_table_CAN_NA, my_table_MEX_NA and so on:
create table my_table_US_NA(id int)
insert into my_table_US_NA(id) values (1)
insert into my_table_US_NA(id) values (2)
insert into my_table_US_NA(id) values (3)
insert into my_table_US_NA(id) values (4)
select * from my_table_US_NA
id
----
1
2
3
4
I have a stored procedure like this:
create procedure my_looping_procedure (#Country varchar(10))
as
begin
declare #MyTable varchar(50), #COUNTER int
set #COUNTER = 1
set #MyTable = concat('my_table_', #Country, '_NA')
while (#COUNTER <= (select max(id) from #MyTable))
begin
set #COUNTER = #COUNTER + 1
print #COUNTER
end
end
When I try to compile the procedure, I get this error:
Msg 1087, Level 16, State 1, Procedure my_looping_procedure, Line 15 [Batch Start Line 0]
Must declare the table variable "#MyTable"
I tried moving the while loop into its own little variable:
create procedure my_looping_procedure (#Country varchar(10))
as
begin
declare #MyTable varchar(50),
#sql_loop varchar(max),
#COUNTER int
set #COUNTER = 1
set #MyTable = concat('my_table_', #Country, '_NA')
-- inner variable here
select #sql_loop = '
while (' + #COUNTER + '<= (select max(id) from ' + #MyTable + '))
begin
set ' + #COUNTER + ' = ' + #COUNTER + ' + 1
print ' + #COUNTER + '
end'
exec(#sql_loop)
end
That compiles but returns an error when I try to execute it exec my_looping_procedure:
Msg 245, Level 16, State 1, Procedure my_looping_procedure, Line 16 [Batch Start Line 26]
Conversion failed when converting the varchar value 'WHILE (' to data type int.
I tried declaring and setting all the variables inside #sql_loop:
alter procedure my_looping_procedure (#Country varchar(10))
as
begin
declare #sql_loop varchar(max)
select #sql_loop = '
declare
#MyTable varchar(50),
#COUNTER INT
SET #COUNTER = 1
set #MyTable = concat(''my_table_'', ' + #Country + ', ''_NA'')
WHILE (#COUNTER <= (select max(id) from ' + #MyTable + '))
BEGIN
SET #COUNTER = #COUNTER + 1
print #COUNTER
end'
exec(#sql_loop)
end
This compiles but still errors on execution:
Msg 1087, Level 16, State 1, Line 38
Must declare the table variable "#MyTable".
I then declared the #MyTable variable in the beginning again:
alter procedure my_looping_procedure (#Country varchar(10))
as
begin
declare
#MyTable varchar(50),
#sql_loop varchar(max)
set #MyTable = concat('my_table_', #Country, '_NA')
select #sql_loop = '
declare
#MyTable varchar(50),
#COUNTER INT,
#Country varchar(10),
SET #COUNTER = 1
set #MyTable = concat(''my_table_'', ' + #Country + ', ''_NA'')
WHILE (#COUNTER <= (select max(id) from ' + #MyTable + ' ))
BEGIN
SET #COUNTER = #COUNTER + 1
print #COUNTER
end'
exec(#sql_loop)
end
This actually compiles but complains about the country:
Msg 207, Level 16, State 1, Line 37
Invalid column name 'US'.
Finally, I commented out the initial table set statement:
alter procedure my_looping_procedure (#Country varchar(10))
as
begin
declare
#MyTable varchar(50),
#sql_loop varchar(max)
-- set #MyTable = concat('my_table_', #Country, '_NA')
select #sql_loop = '
declare
#MyTable varchar(50),
#COUNTER INT,
#Country varchar(10),
#MaxCount int
SET #COUNTER = 1
set #MyTable = concat(''my_table_'', ' + #Country + ', ''_NA'')
WHILE (#COUNTER <= (select max(id) from ' + #MyTable + ' ))
BEGIN
SET #COUNTER = #COUNTER + 1
print #COUNTER
end'
exec(#sql_loop)
end
This compiles AND runs, but does nothing.
Can anybody figure out what I'm doing wrong?
Some background:
This is an example of the problem with the parameter and the while loop, not the actual code. As for why it's done this way, the initial design was just for one hard-coded country. When more countries were added, the scripts were copied with new countries hard-coded.
The initial designer is no longer with the company. My current task is just to make a generic piece of code that can be used no matter how many more countries we add. There are hundreds of scripts like this and very little time and few resources on the project.
I genuinely appreciate the suggestions of using a temp table, but the tables are used in other processes. Until we iron out the underlying issues with the process, we are stuck with this design.
Without questioning why you are doing it this way (but those comments are very useful and should be carefully considered). Here is your working code:
create table #my_table_US_NA(id int);
insert into #my_table_US_NA(id) values (1),(2),(3),(4);
declare #MyTable varchar(50), #Country varchar(10);
set #Country = 'US';
set #MyTable = quotename(concat('#my_table_', #Country, '_NA'));
declare #Sql nvarchar(max) = 'declare #COUNTER INT = 1; WHILE (#COUNTER <= (select max(id) from [' + #MyTable + ']))
BEGIN
SET #COUNTER = #COUNTER + 1
print #COUNTER
end';
exec(#Sql);
drop table #my_table_US_NA;
Note 1: I've added quotename as per Larnu's suggestion to avoid the possibility of injection.
Note 2: Your table design doesn't align with how relational databases are intended to be used. You wouldn't normally have a separate table for each country, you would normally have a country column which allows you to segment the table by country. No good design should end up relying on dynamic SQL, sure you might need it for some edge cases but not your main business flow.
I think that you want to gather the data from the country-specific table and then loop through the country-specific data. I would take the approach of using a "temp" table so that you can insert data from a dynamic SQL statement. Here's what I mean:
create procedure my_looping_procedure as
begin
create table #MyTable (id int)
declare #COUNTER int, #Country varchar(3), #MyTable varchar(50), #sql varchar(100)
SET #COUNTER = 1
set #Country = 'US'
set #MyTable = concat('my_table_', #Country, '_NA')
set #sql = 'insert #MyTable (id) select * from ' + #MyTable
exec(#sql)
WHILE (#COUNTER<= (select max(id) from #MyTable))
BEGIN
SET #COUNTER = #COUNTER + 1
print #COUNTER
end
end
go
exec my_looping_procedure
I eventually resolved the issue by declaring the expression inside the while loop as a text string, like so: set #WhileExpr = concat('#COUNTER <= (select max(id) from ', #MyTable) then using it inside the while parenthesis WHILE (' + #WhileExpr + '))
I apologize for wasting your time.

Variable in SQL while Loop doesn't increase, I'm using SAP Business One

DECLARE #Counter int
SET #Counter = 4
WHILE #Counter <= 10
BEGIN
select #counter
SET #Counter = #counter + 1
END
this is my code, and I'm only getting a 4 as result. If I change my set # counter = 4 to set #counter=5, I get only 5 in the end. I'm using SAP business one query generator. How is this not working? If I put select #counter after END I actually get a 11....so the loop actually runs
Your code seems right to me... The problem might be in the manner you wrote the variable names. Try using same capitalization when its a single variable: #counter and #Counter.
Also try to use select instead of set. They do pretty much the same thing, but might work better in your case.
SELECT #Counter = #Counter + 1
Edit after reading comments:
If your select outside the loop shows 11, try to print inside the loop instead of selecting just to check if it shows 4 too.
DECLARE #counter int
SET #counter = 4
WHILE #counter<= 10
BEGIN
PRINT #counter
SELECT #counter = #counter + 1
END
SAP Business One query manager just shows the result of the first select statement. In your case, you could store all the data in a temp table and select that table when you get out of the loop.
declare #temp table (mynum int)
DECLARE #Counter int
SET #Counter = 4
WHILE #Counter <= 10
BEGIN
insert into #temp select #counter
SET #Counter = #counter + 1
END
select * from #temp

sql use while loop to fetch values and run query on them

I have long query which is ran based on two value parameters (contcode, datime).
I am using this while loop to fetch these values in order from another table, run the query and insert the result into a final table:
DECLARE #r TABLE (id int IDENTITY (1,1),lnr char (9), pday datetime)
INSERT INTO #r (lnr, pday)
SELECT TOP 1 lnr, pday FROM memrepay
DECLARE #counter int SET #counter = 1
DECLARE #contcode char (9)
DECLARE #datime datetime
WHILE #counter <= (SELECT COUNT(*) FROM #r)
BEGIN
--long query
END
SET #counter = #counter + 1
END
SELECT * FROM #finaltable
However long query only works when I fetch 1 row:
SELECT TOP 1 contcode, datime FROM #tbl
And if I don't use TOP 1, I get the error message of:
"Subquery returned more than 1 value...."
How can I fetch those two or more values in order, run the long query based each , not getting this error?
I think you just need to move the SET #counter = #counter + 1 up between the BEGIN...END block under your WHILE loop. That's probably why it only works for TOP 1, since 1 is <= COUNT(*).
DECLARE #r TABLE (id int IDENTITY (1,1),lnr char (9), pday datetime)
INSERT INTO #r (lnr, pday)
SELECT TOP 1 lnr, pday FROM memrepay
DECLARE #counter int SET #counter = 1
DECLARE #contcode char (9)
DECLARE #datime datetime
WHILE #counter <= (SELECT COUNT(*) FROM #r)
BEGIN
--long query
SET #counter = #counter + 1
END
END
SELECT * FROM #finaltable

SQL transform semi-colon list into relationship table

I currently have a code table containing a list of types (Type_ID, Description), but they are saved in another table as ID;;ID;;ID...etc
I am looking for a script that will take those ID's and place them in a relationship table corresponding to there Type ID
For example in table A the Type_ID entries could look like:
1;;2;;4
1
3;;4
1;;2;;3;;4
I am completely stumped on how to accomplish this and any help is appreciated.
First of all, I would probably recommend going the UDF route (so that you don't reinvent the wheel). However, given that this sounds like a one-off activity, you could just use the following:
declare #output table (parentKey int, value int)
declare #values table (idx int identity(1, 1), parentKey int, value varchar(255))
-- Modify the below query to capture the data from your table
insert into #values (parentKey, value) values(1, '1;;2;;4'),(2, '1'),(3, '3;;4'),(4, '1;;2;;3;;4')
declare #i int
declare #cnt int
select #i = MIN(idx) - 1, #cnt = MAX(idx) from #values
while(#i < #cnt)
begin
select #i = #i + 1
declare #value varchar(255)
declare #key int
select #value = value, #key = parentKey from #values where idx = #i
declare #idx int
declare #next int
select #idx = 1
while(#idx <= LEN(#value))
begin
select #next = CHARINDEX(';;', #value, #idx)
if(#next > #idx)
begin
insert into #output (parentKey, value) values(#key, SUBSTRING(#value, #idx, #next - #idx))
select #idx = #next + 2
end
else
begin
insert into #output (parentKey, value) values(#key, SUBSTRING(#value, #idx, LEN(#value) - #idx + 1))
select #idx = LEN(#value) + 1
end
end
end
select * from #output
The #output table variable now contains the mapping you're looking for. You can either copy from that to your destination at the end, or you can remove #output from the query and substitute equivalent inserts directly into your relationship table.
Probably the easiest way is to use a UDF (User Defined Function), such as the Split functions outlined here.

T-SQL: Looping through an array of known values

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))