How to reuse a cursor in tsql from the begining - sql

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

Related

Function with a cursor, cant find the error

I'm not quite sure what I typed in that was wrong. This is the error I receive from my SQL code:
Msg 102, Level 15, State 1, Procedure GradeAverage, Line 22
Incorrect syntax near 'myCursor'.
Function:
CREATE FUNCTION GradeAverage
(#StudentID as varchar(20))
RETURNS int
AS
BEGIN
DECLARE #Final int
DECLARE #Grade int
DECLARE #Total int= 0
DECLARE #Count int = 0
DECLARE myCursor CURSOR FOR
SELECT FinalGrade
FROM CourseEnrollment
WHERE StudentId = #StudentID AND FinalGrade IS NOT NULL
OPEN myCursor;
FETCH NEXT FROM myCursor INTO #Grade
WHILE ##FETCH_STATUS = 0
BEGIN
SET #Total = #Grade + #Total;
SET #Count = #Count +1;
FETCH NEXT FROM myCursor INTO #Grade
END
SET #Final = (#Total/#Count);
CLOSE myCursor
DEALLOCATE myCursor
RETURNS #Final
The first problem is that RETURNS declares the return type of the function as in RETURNS int, but to actually specify the return value, you need to use RETURN (no S):
The second problem is that each BEGIN in your function definition must be matched with an END, including the BEGIN/END which surrounds your entire function body.
Finally, for the sake of readability, I suggest you adopt some more standard formatting conventions. It should look like this:
CREATE FUNCTION GradeAverage (#StudentID as varchar(20)) RETURNS int
AS
BEGIN
DECLARE #Final int;
DECLARE #Grade int;
DECLARE #Total int= 0;
DECLARE #Count int = 0;
DECLARE myCursor CURSOR FOR
SELECT FinalGrade
FROM CourseEnrollment
WHERE StudentId = #StudentID AND FinalGrade IS NOT NULL;
OPEN myCursor;
FETCH NEXT FROM myCursor INTO #Grade;
WHILE ##FETCH_STATUS = 0
BEGIN
SET #Total = #Grade + #Total;
SET #Count = #Count +1;
FETCH NEXT FROM myCursor INTO #Grade;
END
SET #Final = (#Total/#Count);
CLOSE myCursor;
DEALLOCATE myCursor;
RETURN #Final;
END

Loop in stored procedure in SQL server

I need help with writing stored procedure that calls another stored procedure and passes values to it. So far this was done in C#, now I want to move it to stored procedure and make an SQL agent job that calls it at specific time. Any ideas? This is the case.
Table A:
PK_TableA_ID
Table B:
PK_TableB_ID
Stored procedure SP1:
#TableA_ID
#TableB_ID
I need this but in T-SQL
foreach(var TableAId in TableA)
{
foreach(var TableBId in TableB)
{
//call stored procedure
SP1(TableAId, TableBId);
}
}
Here's an example of how you can use cursors to do loops:
-- set up some test data
declare #table_a table (PK_TableA_ID int)
declare #table_b table (PK_TableB_ID int)
insert #table_a values (1),(2),(3)
insert #table_b values (4),(5),(6)
-- do the actual processing
SET NOCOUNT ON
DECLARE #TableA_ID int, #TableB_ID int
DECLARE TableA_cursor CURSOR FOR SELECT PK_TableA_ID FROM #table_a
OPEN TableA_cursor
FETCH NEXT FROM TableA_cursor INTO #TableA_ID
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE TableB_cursor CURSOR FOR SELECT PK_TableB_ID FROM #table_b
OPEN TableB_cursor
FETCH NEXT FROM TableB_cursor INTO #TableB_ID
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT CAST(#TableA_ID AS CHAR(1)) + ':' + CAST(#TableB_ID AS CHAR(1))
-- execute your stored procedure here:
-- EXEC Your_stored_procedure (#TableA_ID, #TableB_ID)
FETCH NEXT FROM TableB_cursor INTO #TableB_ID
END
CLOSE TableB_cursor
DEALLOCATE TableB_cursor
FETCH NEXT FROM TableA_cursor INTO #TableA_ID
END
CLOSE TableA_cursor
DEALLOCATE TableA_cursor
The cursor above (with the test data in the temporary tables) will generate this output:
1:4
1:5
1:6
2:4
2:5
2:6
3:4
3:5
3:6
Using cursors might not be the best way to solve your problem though.
I have a clean and clear option without cursors for this case using the table ids.
DECLARE
#Counter1 INT,#MaxId1 INT,
#Counter2 INT, #MaxId2 INT
SELECT #Counter1 = min(PK_TableA_ID) , #MaxId1 = max(PK_TableA_ID)
FROM TableA
SELECT #Counter2 = min(PK_TableB_ID) , #MaxId2 = max(PK_TableB_ID)
FROM TableB
WHILE(#Counter1 IS NOT NULL AND #Counter1 <= #MaxId1)
BEGIN
WHILE(#Counter2 IS NOT NULL AND #Counter2 <= #MaxId2)
BEGIN
//call stored procedure
SP1(#Counter1, #Counter2);
SET #Counter2 = #Counter2 + 1
END;
SELECT #Counter2 = min(PK_TableB_ID) , #MaxId2 = max(PK_TableB_ID)
FROM TableB
SET #Counter1 = #Counter1 + 1
END;

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

Update table via xml input

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.

Cursor inside cursor

Main problem is about changing the index of rows to 1,2,3.. where contact-id and type is the same. but all columns can contain exactly the same data because of some ex-employee messed up and update all rows by contact-id and type. somehow there are rows that aren't messed but index rows are same. It is total chaos.
I tried to use an inner cursor with the variables coming from the outer cursor.
But It seems that its stuck in the inner cursor.
A part of the query looks like this:
Fetch NEXT FROM OUTER_CURSOR INTO #CONTACT_ID, #TYPE
While (##FETCH_STATUS <> -1)
BEGIN
IF (##FETCH_STATUS <> -2)
DECLARE INNER_CURSOR Cursor
FOR
SELECT * FROM CONTACTS
where CONTACT_ID = #CONTACT_ID
and TYPE = #TYPE
Open INNER_CURSOR
Fetch NEXT FROM INNER_CURSOR
While (##FETCH_STATUS <> -1)
BEGIN
IF (##FETCH_STATUS <> -2)
What can be the problem? Is ##FETCH_STATUS ambiguous or something?
EDIT: everything looks fine if i don't use this code inside inner cursor:
UPDATE CONTACTS
SET INDEX_NO = #COUNTER
where current of INNER_CURSOR
EDIT: here is the big picture:
BEGIN TRAN
DECLARE #CONTACT_ID VARCHAR(15)
DECLARE #TYPE VARCHAR(15)
DECLARE #INDEX_NO SMALLINT
DECLARE #COUNTER SMALLINT
DECLARE #FETCH_STATUS INT
DECLARE OUTER_CURSOR CURSOR
FOR
SELECT CONTACT_ID, TYPE, INDEX_NO FROM CONTACTS
WHERE
CONTACT_ID IN (SELECT CONTACT_ID FROM dbo.CONTACTS
WHERE CONTACT_ID IN(...)
GROUP BY CONTACT_ID, TYPE, INDEX_NO
HAVING COUNT(*) > 1
OPEN OUTER_CURSOR
FETCH NEXT FROM OUTER_CURSOR INTO #CONTACT_ID, #TYPE, #INDEX_NO
WHILE (##FETCH_STATUS <> -1)
BEGIN
IF (##FETCH_STATUS <> -2)
SET #COUNTER = 1
DECLARE INNER_CURSOR CURSOR
FOR
SELECT * FROM CONTACTS
WHERE CONTACT_ID = #CONTACT_ID
AND TYPE = #TYPE
FOR UPDATE
OPEN INNER_CURSOR
FETCH NEXT FROM INNER_CURSOR
WHILE (##FETCH_STATUS <> -1)
BEGIN
IF (##FETCH_STATUS <> -2)
UPDATE CONTACTS
SET INDEX_NO = #COUNTER
WHERE CURRENT OF INNER_CURSOR
SET #COUNTER = #COUNTER + 1
FETCH NEXT FROM INNER_CURSOR
END
CLOSE INNER_CURSOR
DEALLOCATE INNER_CURSOR
FETCH NEXT FROM OUTER_CURSOR INTO #CONTACT_ID, #TYPE, #INDEX_NO
END
CLOSE OUTER_CURSOR
DEALLOCATE OUTER_CURSOR
COMMIT TRAN
You have a variety of problems. First, why are you using your specific ##FETCH_STATUS values? It should just be ##FETCH_STATUS = 0.
Second, you are not selecting your inner Cursor into anything. And I cannot think of any circumstance where you would select all fields in this way - spell them out!
Here's a sample to go by. Folder has a primary key of "ClientID" that is also a foreign key for Attend. I'm just printing all of the Attend UIDs, broken down by Folder ClientID:
Declare #ClientID int;
Declare #UID int;
DECLARE Cur1 CURSOR FOR
SELECT ClientID From Folder;
OPEN Cur1
FETCH NEXT FROM Cur1 INTO #ClientID;
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT 'Processing ClientID: ' + Cast(#ClientID as Varchar);
DECLARE Cur2 CURSOR FOR
SELECT UID FROM Attend Where ClientID=#ClientID;
OPEN Cur2;
FETCH NEXT FROM Cur2 INTO #UID;
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT 'Found UID: ' + Cast(#UID as Varchar);
FETCH NEXT FROM Cur2 INTO #UID;
END;
CLOSE Cur2;
DEALLOCATE Cur2;
FETCH NEXT FROM Cur1 INTO #ClientID;
END;
PRINT 'DONE';
CLOSE Cur1;
DEALLOCATE Cur1;
Finally, are you SURE you want to be doing something like this in a stored procedure? It is very easy to abuse stored procedures and often reflects problems in characterizing your problem. The sample I gave, for example, could be far more easily accomplished using standard select calls.
You could also sidestep nested cursor issues, general cursor issues, and global variable issues by avoiding the cursors entirely.
declare #rowid int
declare #rowid2 int
declare #id int
declare #type varchar(10)
declare #rows int
declare #rows2 int
declare #outer table (rowid int identity(1,1), id int, type varchar(100))
declare #inner table (rowid int identity(1,1), clientid int, whatever int)
insert into #outer (id, type)
Select id, type from sometable
select #rows = count(1) from #outer
while (#rows > 0)
Begin
select top 1 #rowid = rowid, #id = id, #type = type
from #outer
insert into #innner (clientid, whatever )
select clientid whatever from contacts where contactid = #id
select #rows2 = count(1) from #inner
while (#rows2 > 0)
Begin
select top 1 /* stuff you want into some variables */
/* Other statements you want to execute */
delete from #inner where rowid = #rowid2
select #rows2 = count(1) from #inner
End
delete from #outer where rowid = #rowid
select #rows = count(1) from #outer
End
Do you do any more fetches? You should show those as well. You're only showing us half the code.
It should look like:
FETCH NEXT FROM #Outer INTO ...
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #Inner...
OPEN #Inner
FETCH NEXT FROM #Inner INTO ...
WHILE ##FETCH_STATUS = 0
BEGIN
...
FETCH NEXT FROM #Inner INTO ...
END
CLOSE #Inner
DEALLOCATE #Inner
FETCH NEXT FROM #Outer INTO ...
END
CLOSE #Outer
DEALLOCATE #Outer
Also, make sure you do not name the cursors the same... and any code (check your triggers) that gets called does not use a cursor that is named the same. I've seen odd behavior from people using 'theCursor' in multiple layers of the stack.
This smells of something that should be done with a JOIN instead. Can you share the larger problem with us?
Hey, I should be able to get this down to a single statement, but I haven't had time to play with it further yet today and may not get to. In the mean-time, know that you should be able to edit the query for your inner cursor to create the row numbers as part of the query using the ROW_NUMBER() function. From there, you can fold the inner cursor into the outer by doing an INNER JOIN on it (you can join on a sub query). Finally, any SELECT statement can be converted to an UPDATE using this method:
UPDATE [YourTable/Alias]
SET [Column] = q.Value
FROM
(
... complicate select query here ...
) q
Where [YourTable/Alias] is a table or alias used in the select query.
I had the same problem,
what you have to do is declare the second cursor as:
DECLARE [second_cursor] Cursor LOCAL For
You see"CURSOR LOCAL FOR" instead of "CURSOR FOR"
I don't fully understand what was the problem with the "update current of cursor" but it is solved by using the fetch statement twice for the inner cursor:
FETCH NEXT FROM INNER_CURSOR
WHILE (##FETCH_STATUS <> -1)
BEGIN
UPDATE CONTACTS
SET INDEX_NO = #COUNTER
WHERE CURRENT OF INNER_CURSOR
SET #COUNTER = #COUNTER + 1
FETCH NEXT FROM INNER_CURSOR
FETCH NEXT FROM INNER_CURSOR
END