Update table via xml input - sql

I have a stored proc that inserts new records in my 2008 SQL server table via xml input:
CREATE PROCEDURE ins_AddBinsToBox
#BoxId BIGINT,
#BinIds XML
AS
BEGIN
INSERT INTO WebServiceBoxDetails
(
BinId,
BoxId
)
SELECT
ParamValues.ID.value('.','VARCHAR(20)'),
#BoxId
FROM
#binIds.nodes('/Bins/id') AS ParamValues(ID)
This works great for inserting new rows, the thing i'm confused about is updating (via UPDATE statement) this same table with new xml input?
Table:
Id(PK) BoxNumber BinId
(bigint) (bigint) (int)
_______________________
1 12 334
2 12 445
3 12 776
4 16 223
5 16 669
Command to be used:
EXEC upd_Box #binIds='<Bins><id>7848</id><id>76554</id><id>71875</id></Bins>', #BoxId=12

Sorry for the delay. Here is the answer for your second part(this should work for both update and insert)
DECLARE #binIds AS XML = '<Bins><id>44</id><id>55</id><id>66</id><id>77</id></Bins>'
DECLARE #id INT
DECLARE #newid INT
DECLARE #count INT = 1
DECLARE #BinIdTable TABLE(RowNumber INT, BinId INT)
DECLARE #BoxNumber INT = 12
DECLARE #RowCount INT = 0
INSERT #BinIdTable(RowNumber, BinId)
SELECT ROW_NUMBER() OVER(ORDER BY ID), ParamValues.ID.value('.','INT')
FROM
#binIds.nodes('/Bins/id') AS ParamValues(ID)
SELECT #RowCount = COUNT(*) FROM #BinIdTable
DECLARE MyCursor CURSOR FOR
SELECT id FROM WebServiceBoxDetails WHERE BoxNumber = #BoxNumber ORDER BY id
OPEN MyCursor
FETCH NEXT FROM MyCursor
INTO #id
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #newid = B.BinId
FROM #BinIdTable B
WHERE RowNumber = #count
UPDATE WebServiceBoxDetails
SET BinId = #newid
WHERE Id = #id
SET #count = #count + 1
FETCH NEXT FROM MyCursor
INTO #id
END
CLOSE MyCursor
DEALLOCATE MyCursor
IF #RowCount >= #count
BEGIN
INSERT INTO WebServiceBoxDetails
SELECT #BoxNumber, BinId
FROM #BinIdTable
WHERE RowNumber >= #count
END
Let me know how it goes.

You can use the NODES method again.
UPDATE WebServiceBoxDetails
SET BinID = ParamValues.ID.value('#BinID','VARCHAR(20)')
FROM
#bindIDs.nodes('/Bins/id') AS ParamValues(ID)
JOIN WebServiceBoxDetails w ON w.ID = ParamValues.ID.value('#id','VARCHAR(20)')
WHERE w.BoxNumber = #BoxID

Try this
DECLARE #binIds AS XML = '<Bins><id>7848</id><id>76554</id><id>71875</id></Bins>'
DECLARE #id INT
DECLARE #newid INT
DECLARE #count INT = 1
DECLARE #BinIdTable TABLE(RowNumber INT, BinId INT)
INSERT #BinIdTable(RowNumber, BinId)
SELECT ROW_NUMBER() OVER(ORDER BY ID), ParamValues.ID.value('.','INT')
FROM
#binIds.nodes('/Bins/id') AS ParamValues(ID)
DECLARE MyCursor CURSOR FOR
SELECT id FROM WebServiceBoxDetails WHERE BoxNumber = 12 ORDER BY id
OPEN MyCursor
FETCH NEXT FROM MyCursor
INTO #id
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #newid = B.BinId
FROM #BinIdTable B
WHERE RowNumber = #count
UPDATE WebServiceBoxDetails
SET BinId = #newid
WHERE Id = #id
SET #count = #count + 1
FETCH NEXT FROM MyCursor
INTO #id
END
CLOSE MyCursor
DEALLOCATE MyCursor
Let me know how it goes.

Related

How to reuse a cursor in tsql from the begining

How to reuse a cursor in tsql from the beginning without changing its definition.
I saw this topic link but I dont know what to do after this
fetch first from c;
I used after this code :
WHILE ##FETCH_STATUS = 0
BEGIN
...
FETCH NEXT FROM c INTO #myVariable;
END;
CLOSE c;
DEALLOCATE c;
but it didnt work.
You can create a scroll cursor with read only and reset to the first record from the cursor when needed.
For example:
DECLARE #T TABLE (Id int, Num int);
insert into #T values (1,0),(2,0),(3,0);
DECLARE #ID INT;
DECLARE #Num INT;
DECLARE #Counter INT = 0;
DECLARE c SCROLL CURSOR FOR (SELECT Id, Num FROM #T) FOR READ ONLY;
OPEN c;
FETCH NEXT FROM c INTO #ID, #Num;
WHILE ##FETCH_STATUS = 0 AND #Counter <= 3
BEGIN
SET #Counter = #Counter + 1;
UPDATE #T SET num = #Counter WHERE Id = #ID;
IF #ID = 2
BEGIN FETCH FIRST FROM c INTO #ID, #Num; END;
ELSE
BEGIN FETCH NEXT FROM c INTO #ID, #Num; END;
END;
CLOSE c;
select * from #T;
Gets:
Id Num
1 3
2 4
3 0
Just be aware that the values you get from the cursor don't change.
Since there's only 1 open and close.
Try this:
IF OBJECT_ID('tempdb..#tmp') IS NOT NULL
BEGIN
DROP TABLE #tmp;
END;
CREATE TABLE #tmp (num INT IDENTITY(10,10))
-- **** execute separately ****
INSERT INTO #tmp DEFAULT VALUES
GO 10
-- ****************************
DECLARE c scroll cursor for select num FROM #tmp;
OPEN c;
DECLARE #i int = 1,
#cursor_value INT;
WHILE #i < 10
BEGIN
IF ##FETCH_STATUS = 0 AND #i = 5
BEGIN
FETCH FIRST FROM c INTO #cursor_value;
END
ELSE
FETCH NEXT FROM c INTO #cursor_value
SELECT #i AS cntr, #cursor_value AS crsr
SET #i +=1
END
close c
DEALLOCATE c

Calling a stored procedure for each row in a query

I am trying to call a stored proc in a loop for each record in a query.
Here's what I have now:
WITH X AS (
SELECT customerid FROM DBcustomers
)
WHILE(EXISTS (SELECT * FROM X))
BEGIN
SELECT TOP 1 #Id = customerid FROM X
EXEC [dbo].[AnotherSP] #Id , 1
DELETE FROM X WHERE customerid = #Id
END
, but this does not compile.
I guess you want to do UPDATE like this:
UPDATE ProductTable SET IsActive = 1
WHERE customerid IN (SELECT customerid FROM DBcustomers)
or for your edited question use cursors:
DECLARE #id BIG
DECLARE Customers CURSOR
FOR SELECT customerid FROM DBcustomers
OPEN Customers FETCH NEXT FROM Customers INTO #id
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC [dbo].[AnotherSP] #id , 1
FETCH NEXT FROM Customers INTO #id
END
CLOSE Customers
DEALLOCATE Customers
UPDATE productTable
SET isActive = 1
WHERE customerId IN
(
SELECT customerId
FROM dbCustomers
)
Update:
DECLARE cr_customer CURSOR
FOR
SELECT customerId
FROM dbCustomers
DECLARE #id INT
FETCH NEXT
FROM cr_customer
INTO #id
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC anotherSP #id , 1
FETCH NEXT
FROM cr_customer
INTO #id
END
CLOSE cr_customer
DEALLOCATE
cr_customer
Something like this might do it:
DECLARE #Id INT;
SELECT TOP 1 #Id = customerid FROM DBcustomers ORDER BY customerid;
WHILE #Id IS NOT NULL
BEGIN
EXEC [dbo].[AnotherSP] #Id, 1;
DECLARE #LastId INT;
SELECT #LastId = #Id;
SELECT #Id = NULL;
SELECT #Id = customerid FROM DBcustomers WHERE customerId > #LastId ORDER BY customerid;
END;

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 one table from cursor

Please see the code below:
DECLARE #ID int
DECLARE #errorflag int
DECLARE Warning_Cursor CURSOR FOR
SELECT TOP 3 ID FROM Warnings
SET #errorflag = #errorflag + ##Error
OPEN Warning_cursor
SET #errorflag = #errorflag + ##Error
FETCH NEXT FROM Warning_cursor INTO #ID
WHILE ##FETCH_STATUS = 0
begin
SELECT #ID
FETCH NEXT FROM Warning_cursor INTO #ID
END
CLOSE Warning_cursor
DEALLOCATE Warning_cursor
The cursor returns three tables with one row each. How can I return one table with three rows?
Why don't you just do,
SELECT TOP 3 ID FROM Warnings
More generally, if you are using a cursor, you are probably doing it wrong.
If you really have to use a cursor for some reason that is not part of the question. You could do
DECLARE #Id int;
DECLARE #Ids TABLE (Id Int);
DECLARE Warning_Cursor CURSOR FOR SELECT TOP 3 ID FROM Warnings;
OPEN Warning_cursor;
FETCH NEXT FROM Warning_cursor INTO #Id;
WHILE ##FETCH_STATUS = 0 BEGIN
INSERT #Ids SELECT #Id;
FETCH NEXT FROM Warning_cursor INTO #Id;
END
CLOSE Warning_cursor;
DEALLOCATE Warning_cursor;
SELECT Id FROM #Ids;
The answer was to create a temporary table as follows:
DECLARE #ID int
DECLARE #errorflag int
DECLARE #CONCATRESULT TABLE (ID INT)
DECLARE Warning_Cursor CURSOR FOR
SELECT TOP 3 ID FROM Warnings
SET #errorflag = #errorflag + ##Error
OPEN Warning_cursor
SET #errorflag = #errorflag + ##Error
FETCH NEXT FROM Warning_cursor INTO #ID
WHILE ##FETCH_STATUS = 0
begin
INSERT into #CONCATRESULT (ID) VALUES (#ID)
FETCH NEXT FROM Warning_cursor INTO #ID
END
CLOSE Warning_cursor
DEALLOCATE Warning_cursor
select id from #CONCATRESULT

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