How to replace nested cursors? - sql-server

I'm in the middle of writing an achievement module for a website we run. This particular bit of code will run on SQL Server at the end of a given timeframe to award the achievements (which will be notified through the client the next time the site is visited).
So I need to look at all the teams and all the "end" achievements (which are independent of the teams). Each achievement has one or more rules that must be met. Once I've determined that the achievement is passed, I award the achievement to all of the participants of that team.
I was handling this through cursors, and I got an error, so when I went to google the problem, I got endless links on forums of "YOU DUMB $&#$ WHY ARE YOU USING CURSORS" (to paraphrase). I figured while I was solving my problem, I may as well replace what I have using a set-based approach (if possible).
The alternative examples I found were all basic update scenarios using single nested loops on tables that have keys back to each other, and honestly, I wouldn't have considered using cursors in those scenarios to begin with.
What I have below is not entirely syntactically correct, but I can figure that out on my own by playing around. This should give a clear idea of what I'm trying to accomplish, however:
declare #TimeframeID int;
declare #AchievementID int;
declare #q1 int;
declare #q2 int;
declare #TeamID int;
declare #TotalReg int;
declare #TotalMin int;
declare #AvgMin decimal(10, 2);
declare #AggType varchar(50);
declare #Pass bit = 1;
declare #Email varchar(50);
declare #ParticipantID int;
select #TimeframeID = MAX(TimeframeID) from Timeframes
where IsActive = 1;
declare team_cur CURSOR FOR
select
t.TeamID,
(select COUNT(1) from Registrations r
where r.TeamID = t.TeamID and r.TimeframeID = #TimeframeID) TotalReg,
(select SUM(Minutes) from Activities a
inner join Registrations r on r.RegistrationID = a.RegistrationID
where r.TeamID = t.TeamID and r.TimeframeID = #TimeframeID) TotalMin
from Teams t
where Active = 1
group by TeamID;
declare ach_cur CURSOR FOR
select AchievementID from luAchievements
where TimeframeID = #TimeframeID and AchievementType = 'End';
declare rule_cur CURSOR for
select Quantity1, Quantity2, AggregateType
from AchievementRule_Links arl
inner join luAchievementRules ar on ar.RuleID = arl.RuleID
where arl.AchievementID = #AchievementID;
open team_cur;
fetch next from team_cur
into #TeamID, #TotalReg, #TotalMin;
while ##FETCH_STATUS = 0
begin
open ach_cur;
fetch next from ach_cur
into #AchievementID;
while ##FETCH_STATUS = 0
begin
open rule_cur;
fetch next from rule_cur
into #q1, #q2, #AggType;
while ##FETCH_STATUS = 0
begin
if #AggType = 'Total'
begin
if #q1 > #TotalReg
begin
set #Pass = 0;
end
end
else if #AggType = 'Average'
begin
print 'do this later; need to get specs';
end
fetch next from rule_cur
into #q1, #q2, #AggType;
end
close rule_cur;
deallocate rule_cur;
-- if passed, award achievement to all team members
if #Pass = 1
begin
declare reg_cursor cursor for
select max(p.Email) from Participants p
inner join Registrations reg on reg.ParticipantID = p.ParticipantID
where reg.TeamID = #TeamID;
open reg_cursor;
fetch next from reg_cursor
into #Email;
while ##FETCH_STATUS = 0
begin
exec ProcessAchievement #AchievementID, #Email, 0;
fetch next from reg_cursor
into #Email;
end
close reg_cursor;
deallocate reg_cursor;
-- award achievement to team
exec ProcessTeamAchievement #AchievementID, #TeamID;
end
fetch next from ach_cur
into #AchievementID;
end
close ach_cur;
deallocate ach_cur;
fetch next from team_cur
into #TeamID, #TotalReg, #TotalMin;
end
close team_cur;
deallocate team_cur;
Is there a set-based alternative to what I'm doing here? I need to add that this is running against a small set of records, so performance isn't my concern; best practices for future gargantuan updates are.

To make this set-based you need to fix the procs you are calling so that they either have a table-valued parameter or the code in the proc joins to a table where you have the records you want to process. In the second case you would either mark them as processsed or delete them when you are finished.

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

how to use bulk collect in db2 cursor

Here is my procedure, I don't know how to use bulk collecton in cursor, that we can batch process the cursor data. Please help me, thanks!
CREATE PROCEDURE PROC_AUTOACTIVE
BEGIN ATOMIC
DECLARE v_sql VARCHAR(800);
DECLARE v_customer_id BIGINT;
DECLARE v_cardnum varchar(500);
DECLARE v_cardtype varchar(20);
DECLARE v_status varchar(10);
DECLARE v_lastname varchar(200);
DECLARE v_email varchar(150);
DECLARE v_mobile varchar(30);
DECLARE v_phone varchar(30);
DECLARE v_zipcode varchar(20);
DECLARE v_crm_mobile varchar(30);
DECLARE v_address varchar(500);
DECLARE v_order_count BIGINT;
DECLARE v_order_no varchar(500);
DECLARE not_found CONDITION FOR SQLSTATE '02000';
DECLARE at_end INT DEFAULT 0;
DECLARE c_customers CURSOR FOR s_cardsinfo;
DECLARE CONTINUE HANDLER FOR not_found SET at_end = 1;
SET v_sql = 'select t.customer_id, v.CUSTOMER_ID, v.CARD_TYPE, v.STATUS
from customer_tempcard t,
vip_fields v
where t.tempcard_num=v.CUSTOMER_ID
and t.status=1
and v.STATUS=1
and exists (select id
from orders o
where o.FK_CUSTOMER=t.CUSTOMER_ID
and o.FK_ORDER_STATUS in (3,4,6)) ';
PREPARE s_cardsinfo FROM v_sql;
OPEN c_customers;
--fetch card info
LOOP_CUSTOMER_INFO:
LOOP
FETCH c_customers INTO v_customer_id,v_cardnum,v_cardtype,v_status;
IF at_end <> 0 THEN
SET at_end = 0;
LEAVE LOOP_CUSTOMER_INFO;
END IF;
select c.LOGON_ID, o.DEV_CUSTOMER_NAME,
o.DEV_MOBILE, o.DEV_PHONE, o.DEV_ZIP, o.DEV_ADDRESS, o.ORDER_NO
into v_email, v_lastname,
v_mobile, v_phone, v_zipcode, v_address, v_order_no
from orders o,customer c
where o.FK_CUSTOMER=c.ID
and o.FK_CUSTOMER=v_customer_id
and o.FK_ORDER_STATUS in (3,4,6)
order by o.ID desc
fetch first 1 rows only;
IF v_mobile <> null THEN
SET v_crm_mobile = v_mobile;
ELSE
SET v_crm_mobile = v_phone;
END IF;
update customer_tempcard ct
set ct.STATUS='0',
ct.UPDATE_TIME=current_timestamp
where ct.CUSTOMER_ID=v_customer_id;
update card_store cs
set cs.STATUS='0',
cs.UPDATE_TIME=current_timestamp
where cs.CARD_NUM=v_cardnum;
update vip_fields v
set v.LAST_NAME=v_lastname,
v.EMAIL=v_email, v.MOBILE=v_crm_mobile,
v.CUSTOMER_UPDATE_TIME=current_timestamp,
v.UPDATE_TIME=current_timestamp,
v.OPERATION_TYPE='2',
v.CREATE_SOURCE='2',
v.STATUS='0',
v.ZIP_CODE=v_zipcode,
v.ADDRESS=v_address
where customer_id = v_cardnum;
update customer c
set c.VIP_CARD_NUMBER=v_cardnum,
c.VIP_CARD_NAME=v_lastname,
c.VIP_EMAIL=v_email,
c.VIP_CARD_TYPE=v_cardtype,
c.LEVEL=v_cardtype,
c.VIP_ZIP=v_zipcode,
c.VIP_MOBILE=v_crm_mobile,
c.VIP_ADDRESS=v_address,
c.FK_CUSTOMER_GRADE='1'
where c.id=v_customer_id;
insert into beactiveinfo
values (default,v_cardnum,v_order_no,current_timestamp);
END LOOP;
CLOSE c_customers;
END
BULK COLLECT is part of the Oracle compatibility feature in DB2, so, firstly, you cannot use it in the DB2 SQL PL native context, which you are using in your procedure. Secondly, you don't use BULK COLLECT in a cursor. You use SELECT ... BULK COLLECT INTO an_array_variable ... to populate a PL/SQL array. If you intend then to loop over that array, you won't get any performance benefit over the cursor, while incurring the memory overhead for storing the entire result set in the application memory.

Procedure takes long time to execute query

I have the following SP for SQL Server. Strangely the SP has weired behaviour when executing the query
Select #max_backup_session_time = Max(MachineStat.BackupSessionTime) from MachineStat where MachineStat.MachineID = #machine_id;
It takes 1 second if the MachineStat table has rows pertaining to #machine_id but if there are no rows for a #machine_id then it takes more than half a minute to execute. Can someone please help me understand this.
SET NOCOUNT ON;
DECLARE #MachineStatsMId TABLE (
MachineId INT NULL,
BackupSessiontime BIGINT NULL,
MachineGroupName NVARCHAR(128) NULL )
DECLARE #machine_id AS INT;
DECLARE #Machine_group_id AS INT;
DECLARE #machine_group_name AS NVARCHAR(128);
DECLARE #max_backup_session_time AS BIGINT;
SET #machine_id = 0;
SET #Machine_group_id = 0;
SET #machine_group_name = '';
DECLARE MachinesCursor CURSOR FOR
SELECT m.MachineId,
m.MachineGroupId,
mg.MachineGroupName
FROM Machines m,
MachineGroups mg
WHERE m.MachineGroupId = mg.MachineGroupId;
OPEN MachinesCursor;
FETCH NEXT FROM MachinesCursor INTO #machine_id, #machine_group_id, #machine_group_name;
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #max_backup_session_time = Max(MachineStat.BackupSessionTime)
FROM MachineStat
WHERE MachineStat.MachineID = #machine_id;
INSERT INTO #MachineStatsMId
VALUES (#machine_id,
#max_backup_session_time,
#machine_group_name);
FETCH NEXT FROM MachinesCursor INTO #machine_id, #machine_group_id, #machine_group_name;
END;
SELECT *
FROM #MachineStatsMId;
CLOSE MachinesCursor;
DEALLOCATE MachinesCursor;
GO
Here is an alternate version that avoids a cursor and table variable entirely, uses proper (modern) joins and schema prefixes, and should run a lot quicker than what you have. If it still runs slow in certain scenarios, please post the actual execution plan for that scenario as well as an actual execution plan for the fast scenario.
ALTER PROCEDURE dbo.procname
AS
BEGIN
SET NOCOUNT ON;
SELECT
m.MachineId,
BackupSessionTime = MAX(ms.BackupSessionTime),
mg.MachineGroupName
FROM dbo.Machines AS m
INNER JOIN dbo.MachineGroups AS mg
ON m.MachineGroupId = mg.MachineGroupId
INNER JOIN dbo.MachineStat AS ms -- you may want LEFT OUTER JOIN here, not sure
ON m.MachineId = ms.MachineID
GROUP BY m.MachineID, mg.MachineGroupName;
END
GO

Is a recursively called stored procedure possible in SQL Server?

Here is what I have as VBScript Subroutine:
sub buildChildAdminStringHierarchical(byval pAdminID, byref adminString)
set rsx = conn.execute ("select admin_id from administrator_owners where admin_id not in (" & adminString & ") and owner_id = " & pAdminID)
do while not rsx.eof
adminString = adminString & "," & rsx(0)
call buildChildAdminStringHierarchical(rsx(0),adminString)
rsx.movenext
loop
end sub
Is there anyway to turn this into a stored procedure since it's got the recursive call in the subroutine?
Here is what I've tried...
CREATE PROCEDURE usp_build_child_admin_string_hierarchically
#ID AS INT,
#ADMIN_STRING AS VARCHAR(8000),
#ID_STRING AS VARCHAR(8000) OUTPUT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #index int;
DECLARE #length int;
DECLARE #admin_id int;
DECLARE #new_string varchar(8000);
SET #index = 1;
SET #length = 0;
SET #new_string = #ADMIN_STRING;
CREATE TABLE #Temp (ID int)
WHILE #index <= LEN(#new_string)
BEGIN
IF CHARINDEX(',', #new_string, #index) = 0
SELECT #length = (LEN(#new_string) + 1) - #index;
ELSE
SELECT #length = (CHARINDEX(',', #new_string, #index) - #index);
SELECT #admin_id = CONVERT(INT,SUBSTRING(#new_string, #index, #length));
SET #index = #index + #length + 1;
INSERT INTO #temp VALUES(#admin_id);
END
DECLARE TableCursor CURSOR FOR
SELECT Admin_ID FROM Administrator_Owners WHERE Admin_ID NOT IN (SELECT ID FROM #temp) AND Owner_ID = #ID;
OPEN TableCursor;
FETCH NEXT FROM TableCursor INTO #admin_id;
WHILE ##FETCH_STATUS = 0
BEGIN
IF LEN(#ID_STRING) > 0
SET #ID_STRING = #ID_STRING + ',' + CONVERT(VARCHAR, #admin_id);
ELSE
SET #ID_STRING = CONVERT(VARCHAR, #admin_id);
EXEC usp_build_child_admin_string_hierarchically #admin_id, #ID_STRING, #ID_STRING;
FETCH NEXT FROM TableCursor INTO #admin_id;
END
CLOSE TableCursor;
DEALLOCATE TableCursor;
DROP TABLE #temp;
END
GO
But I get the following error when that stored procedure is called...
A cursor with the same name 'TableCursor' already exists.
You can specify a LOCAL cursor, like this:
DECLARE TableCursor CURSOR LOCAL FOR
SELECT ...
At least in SQL Server 2008 R2 (my machine), this allows you to recursively call the sproc without running into "Cursor already exists" errors.
The problem is that while your cursor isn't global, it is a session cursor. Since you're doing recursion, even though each iteration is creating a cursor in a new proc scope, they're all being created in the same PID (connection) at the same time, thus the collision.
You'll need to generate unique cursor names in each iteration of the procedure based on some criteria that won't be reproduced during the recursion.
Or, preferably, find a way to do what you need using set logic, and handle any necessary recursion using a recursive CTE.
You can, but it's usually not a good idea. SQL is made for set-based operations. Also, in MS SQL Server at least, the recursion is limited to the number of recursive calls that it can make. You can only nest up to 32 levels deep.
The problem in your case is that the CURSOR lasts through each call, so you end up creating it more than once.

performance problem sql server 2005 update sentence

I have a table "OFICIAL3" with 500k rows. and 30 columns. and table INSIS with 150k rows and 20 columns.
OFICIAL3.NUMERO_TITULO has an index.
INSIS.NumeroDocumento has an index too.
update sentence take long time. this process will take 9 hours in my machine
my machine is a core 2 duo 2.GHZ and 2GB RAM
ALTER PROCEDURE [dbo].[CompletarDatos] AS
declare #cantidad int;
declare #CONTADOR int;
declare #NRO_TITULO VARCHAR(600);
declare #POYECTO VARCHAR(200);
DECLARE #I_PROYECTO VARCHAR(500);
DECLARE #I_AREA_INT VARCHAR(500);
SET NOCOUNT ON
BEGIN
SET #cantidad =(select count(*) from OFICIAL3)
SET #CONTADOR=1
declare CURSORITO cursor for
select NUMERO_TITULO from OFICIAL3
open CURSORITO
fetch next from CURSORITO
into #NRO_TITULO
while ##fetch_status = 0
begin
SET #CONTADOR=#CONTADOR+1
PRINT 'ROW='+CONVERT(NVARCHAR(30),#CONTADOR)+' NRO TITULO='+#NRO_TITULO
SET #I_PROYECTO = (SELECT PROYECTO FROM INSIS WHERE NumeroDocumento=#NRO_TITULO)
SET #I_AREA_INT = (SELECT I_AREA_INTERVENCION FROM INSIS WHERE NumeroDocumento=#NRO_TITULO)
UPDATE OFICIAL3 SET PROYECT=#I_PROYECTO , COD_AREA=#I_AREA_INT WHERE NUMERO_TITULO=#NRO_TITULO
fetch next from CURSORITO into #NRO_TITULO
end
-- cerramos el cursor
close CURSORITO
deallocate CURSORITO
END
Assuming OFICIAL4 is a typo, this should work as a single update:
UPDATE o
SET PROYECT = i.PROYECTO,
COD_AREA = i.I_AREA_INTERVENCION
FROM OFICIAL3 o
INNER JOIN
INSIS i
ON o.NUMERO_TITULO = i.NumeroDocumento
As others have commented, an approach that avoids the CURSOR is vastly preferable from a performance point of view. Another thought is that a covering index on `INSIS (NumeroDocumento, PROYECTO, I_AREA_INTERVENCION) would speed things up further for this query.
Is there any way you can do this without a cursor? Removing the iteration should help it considerably.

Resources