Using fetch next with where in cursor - sql-server

Is there any option to search inside cursor?
For example: I have a table(MyTable) with row number and value,
that I want to copy to another table(TestTable),
but let's say that if there was a value >= 5 then the next value,
that I want to copy should be <= 3.
I can use something like this:
create table TestTable
(row tinyint,
value tinyint)
declare #row tinyint, #value tinyint, #trigger bit
declare test_cursor cursor fast_forward for
select row,value from MyTable order by row
open test_cursor
fetch next from test_cursor into #row,#value
set #trigger = 0
while ##FETCH_STATUS = 0
begin
if #trigger = 0
begin
insert into TestTable values (#row,#value)
if #value >= 5 set #trigger = 1
end
else if #value <= 3
begin
insert into TestTable values (#row,#value)
set #trigger = 0
end
fetch next from test_cursor into #row,#value
end
close test_cursor
deallocate test_cursor
That will work, but my question is: is there an any way to search inside cursor
for the next falue that <= 3 once trigger = 1,
instead of fetching next row over and over every time?

No, cursors don't support the kind of querying that you're after. You will have to visit each value and check it in the loop.

Related

T-SQL While Infinite Loop

The goal of the below script is to delete all records in a table for all the distinct users on it except the two first records for each user.
The thing is that the script goes into an infinite loop between these two lines
WHILE ##FETCH_STATUS = 0
SET #Event = 0;
The complete script is
DECLARE #Event int, #User int;
DECLARE cUsers CURSOR STATIC LOCAL FOR SELECT DISTINCT(UserID) FROM Identifications;
OPEN cUsers
FETCH NEXT FROM cUsers INTO #User;
WHILE ##FETCH_STATUS = 0
SET #Event = 0;
BEGIN
DECLARE cRows CURSOR STATIC LOCAL FOR
SELECT EventIdentificacionId FROM Identifications WHERE UserId = #User AND EventIdentificacionId NOT IN
(SELECT TOP 2 EventIdentificacionId FROM Identifications WHERE UserId = #User ORDER BY EventIdentificacionId);
OPEN cRows
FETCH NEXT FROM cRows INTO #Event;
WHILE ##FETCH_STATUS = 0
BEGIN
DELETE FROM Identifications WHERE EventIdentificacionId = #Event;
FETCH NEXT FROM cRows INTO #Event;
END
CLOSE cRows;
DEALLOCATE cRows;
FETCH NEXT FROM cUsers INTO #User;
END
CLOSE cUsers;
DEALLOCATE cUsers;
Can anybody give me some solution/explanation please?
As I wrote in my comment, There are far better ways to do such a thing than using a cursor, let alone a couple of nested cursors.
One such better option is to use a common table expression and row_number, and then delete the rows directly from the common table expression.
I'm not entirely sure this code is correct because I have no real way to test it as you didn't provide sample data or desired results, but I came up with that based on the code in the question:
;WITH CTE AS
(
SELECT UserId,
EventIdentificacionId,
ROW_NUMBER() OVER(PARTITION BY UserId ORDER BY EventIdentificacionId) As Rn
FROM Identifications
)
DELETE
FROM CTE
WHERE Rn > 2 -- Delete all but the first two rows
Change this line as shown:
DECLARE #Event int = 0, #User int = 0;
And remove this line
SET #Event = 0;
The reason you have an infinite loop is that this code:
WHILE ##FETCH_STATUS = 0
SET #Event = 0;
BEGIN
Is actually this:
-- A loop of a single instruction, with no exit criteria
WHILE ##FETCH_STATUS = 0 SET #Event = 0;
-- begin a new code block, with no condition or loop
BEGIN

FETCH NOT FINISHED BLANK

I have a problem with FETCH I am using this:
DECLARE Something CURSOR FOR
SELECT * FROM tblSomething
OPEN Something
WHILE ##FETCH_STATUS = 0
BEGIN
--CALL ANOTHER PROCEDURE
FETCH NEXT FROM Something
END
CLOSE Something
DEALLOCATE Something
The problem is 10 rows are in the table and 11 rows come out, the 11th row is blank data. I think it goes to fetch the next row and realises there isnt a next row. I need some sort of:
IF FETCH_ROWS = MAX then STOP
or something along those lines.
Try adding a FETCH NEXT FROM between OPEN and WHILE:
DECLARE Something CURSOR FOR
SELECT * FROM tblSomething
OPEN Something
FETCH NEXT FROM Something -- <== add this line
WHILE ##FETCH_STATUS = 0
BEGIN
--CALL ANOTHER PROCEDURE
FETCH NEXT FROM Something
END
CLOSE Something
DEALLOCATE Something
Here is a simple demonstration with a 10 rows table and a cursor that reads each row and prints the corresponding ID:
declare #tmp table (row_id int)
insert into #tmp values (1), (2), (3), (4), (5), (6), (7), (8), (9), (10)
DECLARE #myID as int;
DECLARE #wf_Cursor as CURSOR;
SET #wf_Cursor = CURSOR FOR SELECT row_id FROM #tmp
OPEN #wf_Cursor;
FETCH NEXT FROM #wf_Cursor INTO #myID;
WHILE ##FETCH_STATUS = 0
BEGIN
print('Reading row: ' + cast(#myID as nvarchar(10)));
FETCH NEXT FROM #wf_Cursor INTO #myID;
END
CLOSE #wf_Cursor;
DEALLOCATE #wf_Cursor;
Output:

How to return value 1 stored procedure

I have written a stored procedure for inserting name and email address:
CREATE PROCEDURE [dbo].[usp_Referral_Email]
#user_key varchar(36),
#name nvarchar(100),
#email nvarchar(500),
#result int output
AS
BEGIN
DECLARE #username Nvarchar(500)
DECLARE #useremail Nvarchar(500)
DECLARE #CusrsorID CURSOR
SET #CusrsorID = CURSOR FOR
SELECT Value,Value1
FROM ufn_split_string(#name,#email)
OPEN #CusrsorID
FETCH NEXT FROM #CusrsorID INTO #username, #useremail
WHILE ##FETCH_STATUS = 0
BEGIN
declare #user nvarchar(36)
begin try
begin transaction trans_Referral_Email
IF NOT EXISTS (SELECT 1 FROM dbo.C_User_Credentials
WHERE email = #useremail)
BEGIN
IF NOT EXISTS (SELECT 1 FROM dbo.Referral_Email
WHERE R_Email = #useremail)
BEGIN
INSERT INTO dbo.Referral_Email (CFK_C_UP_key, R_Name, R_Email)
VALUES (#user_key, #username, #useremail)
SET #result = 1
END
ELSE
BEGIN
SET #result = 0
END
END
ELSE
BEGIN
SET #result = 0
END
COMMIT transaction trans_Referral_Email
end try
begin catch
rollback transaction trans_Referral_Email
set #result=ERROR_MESSAGE()
end catch
FETCH NEXT FROM #CusrsorID INTO #username, #useremail
END
CLOSE #CusrsorID
DEALLOCATE #CusrsorID
END
As a example I will pass value
#name = ramesh,suresh,rahul
#email = ramesh#gmail.com,suresh#gmail.com,rahul#gmail.com
before inserting we checking condition email address are exists or not, suppose email address is exist it will not insert into the table.
Now my problem is i will explain through example. ramesh#gmail.com and suresh#gmail.com are new email address both email address are will insert into the table so return value is 1 rahul#gmail.com already exist in table so it will insert into the table so return value will be 0 and output #return value will be 0 but we have inserted 2 email address so i need #return value should be 1 as out put.
So my question is if at any place of email address is insert into the table if one email address also insert output should be #return=1
What you need is known as a "latch" (archaic) or as a flag variable, and is pretty common.
A flag variable (in this case, #result) should be initialized outside the loop and then set when a condition arises (in this case, a record is inserted). The variable should not be touched when any subsequent records are skipped. That way it acts as a sort of an OR gate.
Like this:
CREATE PROCEDURE [dbo].[usp_Referral_Email]
#user_key varchar(36),
#name nvarchar(100),
#email nvarchar(500),
#result int output
AS
BEGIN
DECLARE #username Nvarchar(500)
DECLARE #useremail Nvarchar(500)
DECLARE #CusrsorID CURSOR
SET #CusrsorID = CURSOR FOR
SELECT Value,Value1
FROM ufn_split_string(#name,#email)
OPEN #CusrsorID
FETCH NEXT FROM #CusrsorID INTO #username, #useremail
SET #result = 0 --<--- Will stay 0 until one or more rows are inserted
WHILE ##FETCH_STATUS = 0
BEGIN
declare #user nvarchar(36)
begin try
begin transaction trans_Referral_Email
IF NOT EXISTS (SELECT 1 FROM dbo.C_User_Credentials
WHERE email = #useremail)
BEGIN
IF NOT EXISTS (SELECT 1 FROM dbo.Referral_Email
WHERE R_Email = #useremail)
BEGIN
INSERT INTO dbo.Referral_Email (CFK_C_UP_key, R_Name, R_Email)
VALUES (#user_key, #username, #useremail)
SET #result = 1 --<--- Will stay 1 for the rest of its lifespan, even if other rows are not inserted
END
END
COMMIT transaction trans_Referral_Email
end try
begin catch
rollback transaction trans_Referral_Email
set #result=ERROR_MESSAGE()
end catch
FETCH NEXT FROM #CusrsorID INTO #username, #useremail
END
CLOSE #CusrsorID
DEALLOCATE #CusrsorID
END
Notice I've removed a bunch of the ELSE conditions, since you don't need to do anything when a record is skipped.
you are trying to process/insert several users with 1 stored procedure call and you can't use a single output INT field to return back insert status for several users.
Better to split #name and #email parameters at application level and pass to your (modified) stored procedure only a SINGLE pair of name and email. You will then have to call the spT from application level several times for each name/email pair.
If you still want to use a single spT for batch user insert, you will have to record each insert status into a temp table or table variable and then at the spT end you will have to SELECT from that temp table or table variable.
This way at application level you will have a row with status returned for each name/email input pair.
But I personally suggest you actually change your spT to be called once per each name/email pair. It's a better and cleaner approach.
HTH

SQL server Select variables showing NULL

in the code below when I run it in Degug mode I can see the variables contain values, however when I select them they show NULL, any ideas? I need to eventually do an Update back to the table [dbo].[HistData]
with the values where RecordID = some number. Any ideas welcome.
-- Declare the variables to store the values returned by FETCH.
DECLARE #HD_TckrPercent decimal(6,3) -- H2 in above formula
DECLARE #HD_CloseLater decimal(9,2) -- F2 in above formula
DECLARE #HD_CloseEarlier decimal(9,2) -- F3 in above formula
DECLARE #RowsNeeded INT
DECLARE #RecordCOUNT INT
SET #RowsNeeded = 2
set #RecordCOUNT = 0 -- to initialize it
DECLARE stocks_cursor CURSOR FOR
SELECT top (#RowsNeeded) [TCKR%], [Stock_Close] FROM [dbo].[HistData]
ORDER BY [RecordID]
OPEN stocks_cursor
-- Perform the first fetch and store the values in variables.
-- Note: The variables are in the same order as the columns
-- in the SELECT statement.
-- Check ##FETCH_STATUS to see if there are any more rows to fetch.
WHILE ##FETCH_STATUS = 0
BEGIN
-- Concatenate and display the current values in the variables.
-- This is executed as long as the previous fetch succeeds.
set #RecordCOUNT = (#RecordCOUNT + 1)
Print #HD_CloseLater
IF #RecordCOUNT = 1
BEGIN
FETCH NEXT FROM stocks_cursor
INTO #HD_TckrPercent, #HD_CloseLater
END
ELSE
BEGIN
FETCH NEXT FROM stocks_cursor
INTO #HD_TckrPercent, #HD_CloseEarlier
END
Select #HD_TckrPercent
Select #HD_CloseLater
Select #HD_CloseEarlier
END
CLOSE stocks_cursor
DEALLOCATE stocks_cursor
GO

SQL Server A trigger to work on multiple row inserts

I am maintaining some code that has a trigger on a table to increment a column. That column is then used by a 3rd party application A. Lets say that the table is called test with two columns num1 and num2. The trigger runs on each insert of num1 in test. Following is the trigger:
USE [db1]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[TEST_MYTRIG] ON [dbo].[test]
FOR INSERT AS
begin
SET NOCOUNT ON
DECLARE #PROC_NEWNUM1 VARCHAR (10)
DECLARE #NEWNUM2 numeric(20)
SELECT #PROC_NEWNUM1 = num1 FROM INSERTED
select #NEWNUM2 = MAX(num2) from TEST
if #NEWNUM2 is null
Begin
set #NEWNUM2 = 0
end
set #NEWNUM2 = #NEWNUM2 + 1
UPDATE TEST SET num2 = #NEWNUM2 WHERE num1 = #PROC_NEWNUM1
SET NOCOUNT OFF
End
This works fine in simple row based inserts, but there is another 3rd party app B (sigh) that sometimes does multiple inserts on this table something like this but not exactly:
INSERT INTO [db1].[dbo].[test]
([num1])
Select db1.dbo.test.num1 from [db1].[dbo].[test]
GO
This causes the trigger to behave erratically...
Now I don't have access to the source of app A or B and only control the database and the trigger. Is there anything that can be done with the trigger so that the updates done to num2 are correct in case of multiple inserts?
Solution:
Following is the solution based on affan's code:
DECLARE #PROC_NEWNUM1 VARCHAR (10)
DECLARE #NEWNUM2 numeric(20)
DECLARE my_Cursor CURSOR FAST_FORWARD FOR SELECT num1 FROM INSERTED;
OPEN my_Cursor
FETCH NEXT FROM my_Cursor into #PROC_NEWNUM1
WHILE ##FETCH_STATUS = 0
BEGIN
select #NEWNUM2 = MAX(num2) from TEST
if #NEWNUM2 is null
Begin
set #NEWNUM2 = 0
End
set #NEWNUM2 = #NEWNUM2 + 1
UPDATE TEST SET num2 = #NEWNUM2 WHERE num1 = #PROC_NEWNUM1
FETCH NEXT FROM my_Cursor into #PROC_NEWNUM1
END
CLOSE my_Cursor
DEALLOCATE my_Cursor
Check here for a set based approach:
SQL Server - Rewrite trigger to avoid cursor based approach
You just have to open a cursor on INSERTED and iterate it for #PROC_NEWNUM1 and put your rest of code that loop. e.g
DECLARE #PROC_NEWNUM1 VARCHAR (10)
DECLARE #NEWNUM2 numeric(20)
DECLARE my_Cursor CURSOR FOR SELECT num1 FROM INSERTED;
OPEN my_Cursor;
FETCH NEXT FROM #PROC_NEWNUM1;
WHILE ##FETCH_STATUS = 0
BEGIN FETCH NEXT FROM my_Cursor
select #NEWNUM2 = MAX(num2) from TEST
if #NEWNUM2 is null
Begin
set #NEWNUM2 = 0
end
set #NEWNUM2 = #NEWNUM2 + 1
UPDATE TEST SET num2 = #NEWNUM2 WHERE num1 = #PROC_NEWNUM1
END;
CLOSE my_Cursor; DEALLOCATE my_Cursor;
Take a look at inserted pseudo table in your trigger as it will contain multiple rows during these operations. You should always handle multiple rows in your triggers anyway.
See here for more info:
How to test for multiple row actions in a SQL Server trigger?
Trigger needs to be rewriteen to handle multiple row inserts. Never write a trigger like that using variables. All triggers must alawys consider that someday someone is going to do a multi-row insert/update/delete.
You shouldn't be incrementing columns that way in a trigger either, if you need incremented column numbers why aren't you using an identity column?
As already pointed out, cursors can be problematic and it's best to use joins between the triggered table and the inserted and deleted tables.
Here's an example of how to do that:
ALTER TRIGGER [dbo].[TR_assign_uuid_to_some_varchar_column]
ON [dbo].[myTable]
AFTER INSERT, UPDATE
AS
BEGIN
/********************************************
APPROACH
* we only care about update and insert in this case
* for every row in the "inserted" table, assign a new uuid for blanks
*********************************************/
update t
set uuid_as_varchar = lower(newid())
from myTable t
-- inserted table is populated for row updates and new row inserts
inner join inserted i on i.myPrimaryKey = t.myPrimarykey
-- deleted table is populated for row updates and row deletes
left join deleted d on d.myPrimaryKey = i.myPrimaryKey
-- only update the triggered table for rows applicable to the trigger and
-- the condition of currently having a blank or null stored for the id
where
coalesce(i.uuid_as_varchar,'') = ''
-- you can also check the row being replaced as use that as part of the conditions, e.g.
or ( coalesce(i.uuid_as_varchar,'') <> coalesce(d.uuid_as_varchar,'') );
END

Resources