I have a doubt regarding the variable declaration in a nested cursors scenario.
This is an small nested cursor sample that i found. In other samples I've seen I also find DECLARE clauses inside the first cursor.
DECLARE #ClientID int;
DECLARE Cur1 CURSOR FOR SELECT ClientID From Folder;
OPEN Cur1
FETCH NEXT FROM Cur1 INTO #ClientID;
SELECT #FETCH_Cur1 = ##FETCH_STATUS
WHILE #FETCH_Cur1 = 0
BEGIN
DECLARE #UID int;
DECLARE Cur2 CURSOR FOR SELECT UID FROM Attend Where ClientID=#ClientID;
OPEN Cur2;
FETCH NEXT FROM Cur2 INTO #UID;
SELECT #FETCH_Cur2 = ##FETCH_STATUS
WHILE #FETCH_Cur2 = 0
BEGIN
PRINT 'Found UID: ' + Cast(#UID as Varchar);
FETCH NEXT FROM Cur2 INTO #UID;
SELECT #FETCH_Cur2 = ##FETCH_STATUS
END;
CLOSE Cur2;
DEALLOCATE Cur2;
FETCH NEXT FROM Cur1 INTO #ClientID;
SELECT #FETCH_Cur1 = ##FETCH_STATUS
END;
PRINT 'DONE';
CLOSE Cur1;
DEALLOCATE Cur1;
The code works, but my doubt is if it's correct the DECLARATIONS inside the first cursor.
DECLARE #UID int;
Shouldn't Declarations be placed at the beginning of code, as is normally done for other programming languages?
You can DECLARE a variable inside a WHILE, yes; the latter DECLAREs will simply be ignored. If you declared the variable and assigned it a value at the time (for example DECLARE #UID int = 1; it would be assigned 1 in each iteration:
DECLARE #I int = 1;
WHILE #i < 10 BEGIN
DECLARE #W int;
SET #W = ISNULL(#W,1) + 1;
SET #I = #I + 1
END
SELECT #W; --10
GO
DECLARE #I int = 1;
WHILE #i < 10 BEGIN
DECLARE #W int = 0;
SET #W = ISNULL(#W,1) + 1;
SET #I = #I + 1
END
SELECT #W; -- 1
DB<>fiddle
Of course, I personally prefer to DECLARE the variables outside of the WHILE as I feel the code is "cleaner", but that doesn't mean you have to.
I have the following script which works fine:
DECLARE db_cursor1 CURSOR LOCAL FOR
SELECT ID, Name table_1
OPEN db_cursor1
FETCH NEXT FROM db_cursor1 INTO #ID, #Name
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRANSACTION
BEGIN TRY
<insert into table values>
COMMIT TRANSACTION
END TRY
BEGIN CATCH
PRINT ERROR_MESSAGE();
ROLLBACK TRANSACTION
END CATCH
FETCH NEXT FROM db_cursor1 INTO #ID, #Name
END
CLOSE db_cursor1
DEALLOCATE db_cursor1
The above script works fine in that it rolls back what is in the current iteration of db_cursor1 and then proceeds to the next iteration in case there is an error.
The problem arises when I have a nested cursor. It rolls back what is in the current iteration but DOES NOT proceed to the next iteration of cursor1.
DECLARE db_cursor1 CURSOR LOCAL FOR
SELECT ID, Name table_1
OPEN db_cursor1
FETCH NEXT FROM db_cursor1 INTO #ID, #Name
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRANSACTION
BEGIN TRY
<insert into table values>
--- inner cursor
DECLARE db_cursor2 CURSOR LOCAL FOR
SELECT ID, Name table_2
OPEN db_cursor2
FETCH NEXT FROM db_cursor2 INTO #ID, #Name
WHILE ##FETCH_STATUS = 0
BEGIN
<insert into table values>
FETCH NEXT FROM db_cursor2 INTO #ID, #Name
END
CLOSE db_cursor2
DEALLOCATE db_cursor2
COMMIT TRANSACTION
END TRY
BEGIN CATCH
PRINT ERROR_MESSAGE();
ROLLBACK TRANSACTION
END CATCH
FETCH NEXT FROM db_cursor1 INTO #ID, #Name
END
CLOSE db_cursor1
DEALLOCATE db_cursor1
I was able to create it on my end with basic values, please see SCRIPT #1 - ERROR if you'd like to test it. The issue that you're running into is that when you have an error in db_cursor2, you exit the loop without closing or deallocating the cursor. Then when the code goes to next iteration, it fails with this error A cursor with the name 'db_cursor2' already exists. Please see SCRIPT #2 - SUCCESS for correct results. To give it more color, you needed to add CLOSE db_cursor2; DEALLOCATE db_cursoe2; in your BEGIN CATCH.
SETUP, Designed for SQL Server 2016+
DROP TABLE IF EXISTS #table_1, #table_2
CREATE TABLE #table_1
(
[ID] INT,
[Name] VARCHAR(5)
);
CREATE TABLE #table_2
(
[ID] INT,
[NAME] VARCHAR(5)
);
INSERT INTO #table_1 SELECT 1, 'j';
INSERT INTO #table_1 SELECT 2, 'j';
INSERT INTO #table_2 SELECT 1, 'j';
INSERT INTO #table_2 SELECT 2, 'j';
SCRIPT #1 - ERROR
DECLARE #ID INT;
DECLARE #name VARCHAR(5);
DECLARE db_cursor1 CURSOR LOCAL FOR SELECT [ID], [Name] FROM #table_1;
OPEN db_cursor1;
FETCH NEXT FROM db_cursor1
INTO #ID, #name;
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRANSACTION;
BEGIN TRY
PRINT('trying 1')
--- inner cursor
DECLARE db_cursor2 CURSOR LOCAL FOR SELECT [ID], [Name] FROM #table_2;
OPEN db_cursor2;
FETCH NEXT FROM db_cursor2
INTO #ID, #name;
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT('trying 2')
SELECT 1/0
FETCH NEXT FROM db_cursor2
INTO #ID, #name;
END;
CLOSE db_cursor2;
DEALLOCATE db_cursor2;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
PRINT ERROR_MESSAGE();
ROLLBACK TRANSACTION;
END CATCH;
FETCH NEXT FROM db_cursor1
INTO #ID, #name;
END;
CLOSE db_cursor1;
DEALLOCATE db_cursor1;
SCRIPT #2 - SUCCESS
DECLARE #ID INT;
DECLARE #name VARCHAR(5);
DECLARE db_cursor1 CURSOR LOCAL FOR SELECT [ID], [Name] FROM #table_1;
OPEN db_cursor1;
FETCH NEXT FROM db_cursor1
INTO #ID, #name;
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRANSACTION;
BEGIN TRY
PRINT('trying 1')
--- inner cursor
DECLARE db_cursor2 CURSOR LOCAL FOR SELECT [ID], [Name] FROM #table_2;
OPEN db_cursor2;
FETCH NEXT FROM db_cursor2
INTO #ID, #name;
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT('trying 2')
SELECT 1/0
FETCH NEXT FROM db_cursor2
INTO #ID, #name;
END;
CLOSE db_cursor2;
DEALLOCATE db_cursor2;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
PRINT ERROR_MESSAGE();
-- was missing in above script
CLOSE db_cursor2
DEALLOCATE db_cursor2
ROLLBACK TRANSACTION;
END CATCH;
FETCH NEXT FROM db_cursor1
INTO #ID, #name;
END;
CLOSE db_cursor1;
DEALLOCATE db_cursor1;
Because you are using a TRY-CATCH, if there is an error within the TRY, code execution will begin in the CATCH.
Inside your catch you would have to handle the error, and depending on the error, close and deallocate db_cursor2. Or if the error is benign, perhaps return execution back to the TRY with a GOTO. GOTO statements cannot be used to enter a TRY or CATCH block. GOTO statements can be used to jump to a label inside the same TRY or CATCH block or to leave a TRY or CATCH block.
As #paul-wehland stated, it's because your Try-Catch isn't disposing the nested cursor. As such, your next iteration is going to initialize a cursor by name that already exists. I've provided an example of code that will run your basic scenario with an intended failure condition at iteration 11 of each cursor.
In the example, I've commented out the part of the code that addresses the issue. Where you choose to place that block is totally up to you, but it would make sense to check either before the nested cursor declaration, or inside the Catch block.
declare
#id tinyint,
#parent_id tinyint,
#name varchar(255),
#parent_name varchar(255);
declare
#table
table
(
id tinyint not null primary key,
[name] varchar(255) not null
);
declare
#target
table
(
parent_id tinyint not null,
child_id tinyint not null,
parent_name varchar(10) not null,
child_name varchar(10) not null,
primary key(parent_id, child_id)
);
with cteNumber
as (
select top 11
[id] = row_number() over (order by [object_id])
from
sys.objects
)
insert into
#table
select
id,
[name] = replicate('a', id)
from
cteNumber;
declare
db_cursor1
cursor
local keyset read_only forward_only
for
select
0,
id,
'Initial',
[name]
from
#table;
open
db_cursor1;
fetch
next
from
db_cursor1
into
#id,
#parent_id,
#name,
#parent_name;
while ##FETCH_STATUS = 0
begin
begin transaction;
begin try
insert into #target
(parent_id, child_id, parent_name, [child_name])
values
(#parent_id, #id, #parent_name, #name);
--- inner cursor
/*
if CURSOR_STATUS('local', 'db_cursor2') = 1
begin
close
db_cursor2;
deallocate
db_cursor2;
end;
-- */
declare
db_cursor2
cursor
local keyset read_only forward_only
for
select
id,
[name]
from
#table;
open
db_cursor2;
fetch
next
from
db_cursor2
into
#id,
#name;
while ##FETCH_STATUS = 0
begin
insert into #target
(parent_id, child_id, parent_name, [child_name])
values
(#parent_id, #id, #parent_name, #name);
fetch
next
from
db_cursor2
into
#id,
#name;
end;
close
db_cursor2;
deallocate
db_cursor2;
commit transaction
end try
begin catch
print ERROR_MESSAGE();
rollback transaction;
end catch;
fetch
next
from
db_cursor1
into
#id,
#parent_id,
#name,
#parent_name;
end;
close
db_cursor1;
deallocate
db_cursor1;
select
[Last #id] = #id,
[Last #name] = #name,
[Last #parent_id] = #parent_id,
[Last #parent_name] = #parent_name;
select
*
from
#table;
select
*
from
#target;
EDITED
You could also use the creation of a Cursor Variable, and assign the nested cursor declaration to it, which would negate the problem of dealing with the duplicate names. See below:
declare
#id tinyint,
#parent_id tinyint,
#name varchar(255),
#parent_name varchar(255);
declare
#table
table
(
id tinyint not null primary key,
[name] varchar(255) not null
);
declare
#target
table
(
parent_id tinyint not null,
child_id tinyint not null,
parent_name varchar(10) not null,
child_name varchar(10) not null,
primary key(parent_id, child_id)
);
with cteNumber
as (
select top 11
[id] = row_number() over (order by [object_id])
from
sys.objects
)
insert into
#table
select
id,
[name] = replicate('a', id)
from
cteNumber;
declare
#db_cursor2 cursor;
declare
db_cursor1
cursor
local keyset read_only forward_only
for
select
0,
id,
'Initial',
[name]
from
#table;
open
db_cursor1;
fetch
next
from
db_cursor1
into
#id,
#parent_id,
#name,
#parent_name;
while ##FETCH_STATUS = 0
begin
begin transaction;
begin try
insert into #target
(parent_id, child_id, parent_name, [child_name])
values
(#parent_id, #id, #parent_name, #name);
--- inner cursor
set #db_cursor2 = cursor
local keyset read_only forward_only
for
select
id,
[name]
from
#table;
open
#db_cursor2;
fetch
next
from
#db_cursor2
into
#id,
#name;
while ##FETCH_STATUS = 0
begin
insert into #target
(parent_id, child_id, parent_name, [child_name])
values
(#parent_id, #id, #parent_name, #name);
fetch
next
from
#db_cursor2
into
#id,
#name;
end;
close
#db_cursor2;
deallocate
#db_cursor2;
commit transaction
end try
begin catch
print ERROR_MESSAGE();
rollback transaction;
end catch;
fetch
next
from
db_cursor1
into
#id,
#parent_id,
#name,
#parent_name;
end;
close
db_cursor1;
deallocate
db_cursor1;
select
[Last #id] = #id,
[Last #name] = #name,
[Last #parent_id] = #parent_id,
[Last #parent_name] = #parent_name;
select
*
from
#table;
select
*
from
#target;
I have a query which processes XML data and I use a while(##FETCH_STATUS = 0) loop for data returned from the cursor.
When I run the query using Management Studio, ##FETCH_STATUS equals -1 and the code inside my loop is omitted. If I run the query using the debugger and press continue, it runs just fine and the ##FETCH_STATUS equals 0. When I run the query again, after running it in debug ##FETCH_STATUS equals 0 and changes to -1.
To sum up:
I run with SSMS - ##FETCH_STATUS = -1
I run with debugger - ##FETCH_STATUS = 0 (I want this value)
I run with SSMS once after running with debugger ##FETCH_STATUS still equals 0 but then changes to -1.
I use OPEN cursor, CLOSE cursor and DEALLOCATE cursor. Why does it work this way?
EDIT: Code you asked for:
IF (OBJECT_ID('dbo.XmlOrderResponses') IS NOT NULL)
DROP TABLE XmlOrderResponses;
CREATE TABLE XmlOrderResponses (
OrderResponseType INT
,OrderResponseNumber NVARCHAR(40)
,OrderResponseDate DATETIME
,DocumentFunctionCode NVARCHAR(40)
,Remarks INT
);
DECLARE CUR CURSOR
FOR
SELECT Subdirectory
FROM XMLFiles;
OPEN CUR
WHILE (##FETCH_STATUS = 0)
BEGIN
DECLARE #DocHandle AS INT;
DECLARE #TMP AS NVARCHAR(512);
FETCH NEXT
FROM Cur
INTO #TMP
DECLARE #XmlDocument AS NVARCHAR(MAX);
SET #XmlDocument = (
SELECT CAST(XMLSource AS NVARCHAR(max))
FROM XMLFiles
WHERE subdirectory = #TMP
);
EXEC sys.sp_xml_preparedocument #DocHandle OUTPUT
,#XmlDocument;
INSERT INTO XmlOrderResponses (
OrderResponseType
,OrderResponseNumber
,OrderResponseDate
,DocumentFunctionCode
,Remarks
)
SELECT *
FROM OPENXML(#DocHandle, '/Document-OrderResponse/*', 11) WITH (
OrderResponseType INT
,OrderResponseNumber NVARCHAR(40)
,OrderResponseDate DATETIME
,DocumentFunctionCode NVARCHAR(40)
,Remarks INT
);
EXEC sys.sp_xml_removedocument #DocHandle;
END
CLOSE CUR;
DEALLOCATE CUR;
--I know I shouldn't be doing that but I can't get rid of NULL records the other way.
DELETE
FROM XmlOrderResponses
WHERE OrderResponseType IS NULL
AND OrderResponseNumber IS NULL
AND OrderResponseDate IS NULL
AND DocumentFunctionCode IS NULL
AND Remarks IS NULL;
SELECT *
FROM XmlOrderResponses
SELECT ##FETCH_STATUS
The problem is that the first time you refer to ##FETCH_STATUS, you have not done a fetch with your cursor, so it is referring to the last cursor used. Imagine this simple example:
DECLARE C1 CURSOR
FOR
SELECT TOP 3 ID
FROM (VALUES ('1'), ('2'), ('3')) t (ID);
OPEN C1;
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #c1 CHAR(1);
FETCH NEXT FROM C1 INTO #c1;
PRINT #c1;
END
CLOSE C1;
DEALLOCATE C1;
DECLARE C2 CURSOR
FOR
SELECT TOP 3 ID
FROM (VALUES ('1'), ('2'), ('3')) t (ID);
OPEN C2;
-- HERE ##FETCH_STATUS REFERS TO THE LAST FETCH FOR CURSOR `C1` NOT `C2`
SELECT ##FETCH_STATUS;
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #c2 CHAR(1);
FETCH NEXT FROM C2 INTO #c2;
PRINT #c2;
END;
CLOSE C2;
DEALLOCATE C2;
At the commented line, even though you have closed, and deallocated C1, ##FETCH_STATUS is still referring to this cursor (since no other FETCH has been performed since), so you never enter your loop for C2
You should perform the Fetch before the loop, then at the end of each loop, rather than at the beginning.
DECLARE #TMP AS NVARCHAR(512);
OPEN CUR
-- DO FETCH FIRST
FETCH NEXT FROM Cur INTO #TMP
WHILE (##FETCH_STATUS = 0)
BEGIN
DECLARE #DocHandle AS INT;
-- DO ALL YOUR WORK WITH #TMP
--PERFORM THE FETCH AGAIN AT THE END OF THE LOOP
FETCH NEXT FROM Cur INTO #TMP
END
The other problem you have with doing FETCH at the start of each loop, is that the last item will be processed twice. Again a simple example (and assuming you enter the loop with ##FETCH_STATUS = 0)
DECLARE C1 CURSOR
FOR
SELECT ID = '1';
OPEN C1;
DECLARE #c CHAR(1);
WHILE (##FETCH_STATUS = 0)
BEGIN
DECLARE #c1 CHAR(1);
FETCH NEXT FROM C1 INTO #c1;
PRINT #c1;
END
This will print
1
1
Because, when ##FETCH_STATUS is -1, FETCH will just return the item at the current position.
I want to use cursor to delete record from table. How can I do it?
I use MSSQL 2008 Express this code does not delete anything from #temp. I also tried where current of cursor_name did not work.
Here is my sample code:
use AdventureWorks
drop table #temp
select * into #temp from HumanResources.Employee;
declare #eid as int;
declare #nid as varchar(15);
DECLARE Employee_Cursor CURSOR FOR
SELECT A.EmployeeID, A.NationalIDNumber FROM #temp AS A
OPEN Employee_Cursor;
FETCH NEXT FROM Employee_Cursor INTO #eid , #nid ;
WHILE ##FETCH_STATUS = 0
BEGIN
IF (#eid > 10)
BEGIN
delete from #temp where #temp.EmployeeID = #eid;
END
FETCH NEXT FROM Employee_Cursor;
END;
CLOSE Employee_Cursor;
DEALLOCATE Employee_Cursor;
GO
select * from #temp
thanks in advance
use AdventureWorks
select * into #temp from HumanResources.Employee;
declare #eid as int;
declare #nid as varchar(15);
DECLARE Employee_Cursor CURSOR FOR
SELECT A.EmployeeID, A.NationalIDNumber FROM #temp AS A
OPEN Employee_Cursor;
FETCH NEXT FROM Employee_Cursor INTO #eid , #nid ;
WHILE ##FETCH_STATUS = 0
BEGIN
IF (#eid > 10)
BEGIN
delete from #temp where current of Employee_Cursor
END
FETCH NEXT FROM Employee_Cursor INTO #eid , #nid ;
END;
CLOSE Employee_Cursor;
DEALLOCATE Employee_Cursor;
select * from #temp
drop table #temp
this works for me
There is a much simpler answer - use this command:
delete from HumanResources.Employee where current of Employee_Cursor
It's called 'Positioned delete' and described at MSDN.
Could you please try in below ways, thanks for your time.
You have fetched data from cursor but didn't push into your variables missed in WHILE loop, please have a look on below code, thanks.
drop table #temp
select * into #temp from Employee;
declare #eid as int;
declare #nid as varchar(15);
DECLARE Employee_Cursor CURSOR FOR
SELECT A.EmployeeID, A.NationalIDNumber FROM #temp AS A
OPEN Employee_Cursor;
FETCH NEXT FROM Employee_Cursor INTO #eid , #nid ;
WHILE ##FETCH_STATUS = 0
BEGIN
IF (#eid > 10)
BEGIN
delete from #temp where #temp.EmployeeID = #eid;
END
FETCH NEXT FROM Employee_Cursor INTO #eid , #nid ;
END;
CLOSE Employee_Cursor;
DEALLOCATE Employee_Cursor;
GO
select * from #temp
Update:
I've changes in 2nd FETCH statement, just have added below highlighted part, thanks
FETCH NEXT FROM Employee_Cursor INTO #eid , #nid ;
I have an select statement that returns a single row.
After that I have written a cursor as for me ##fetch_status is -1 it does not go inside the cursor only now
open cur_mkt
print #init
While (#init = 0)
Begin
fetch next from cur_mkt into
#Desc,
#Divisions
print ##fetch_status
if (##fetch_status =-1)
BREAK
Is there any way I can go inside the cursor,
please help me out.
It doesn't sound like you need a cursor (which you should try to avoid anyway). If you're determining the presence of a result you could do:
SELECT #Desc = Desc, #Divisions = Divisions
FROM YourTable
WHERE ID = 1
IF ( ##ROWCOUNT > 0 )
BEGIN
-- Row was found
END
So I would recommend not using cursors.
To directly answer the question, the way you use cursors/iterate round the results is as follows:
DECLARE #A INTEGER
DECLARE cur_mkt CURSOR FOR
SELECT 1 AS A
UNION ALL
SELECT 2 AS A
OPEN cur_mkt
FETCH NEXT FROM cur_mkt INTO #A
WHILE (##FETCH_STATUS = 0)
BEGIN
PRINT #A
FETCH NEXT FROM cur_mkt INTO #A
END
CLOSE cur_mkt
DEALLOCATE cur_mkt