SQL Server avoiding Cursors - sql

I am just reading about SQL Server cursors, that I should avoid them as much as I can :)
Is there ALWAYS a way to write a query/function/procedure without cursors?
I have found some examples on the Net, but they are usually rather simple.
My example - can I avoid cursors?:
Let's have an update procedure X taking an account id and a transaction id - this is the unique key of the row I want to update
But there are more transactions for an account
SELECT accID, transID from table
Now I use a cursor to loop on the table, always taking the accID and transID to get the row and update it
Can I do it a non-cursor way?
Thanks

I was asked for more info about what I want to do, it is a bit too long, so I will add it here as a new comment.. I can not copy the code here, so I am trying to capture at least the base.
My code looked like:
DECLARE #headAcc varchar(20)
set #headAcc='ACC111'
declare #id varchar(20), #clId varchar(20)
declare cur CURSOR for
select addID, transID from Table1
where accID like #headAcc+'%' and...
order by 1 desc
OPEN cur
FETCH NEXT from cur into #accID, #transID
WHILE ##FETCH_STATUS = 0
BEGIN
-- 1
update Table2
set colX = ...
where accID=#accID and transID=#transID and...
-- 2
update Table3
set colY = ...
where accID=#accID and transID=#transID and...
FETCH NEXT from cur into #accID, #transID
END
CLOSE cur
DEALLOCATE cur
Thanks for the help in answers and link in comments, I was not very familiar with UPDATE JOINs, and that should be the answer.
After reading the articles I came up with 2 updates in form like:
DECLARE #headACC varchar(20)
set #headACC='ACC111'
update t2
set t2.colX = ...
from Table2 t2
join Table1 t1
on t1.accID=t2.accID
and t1.transID=t2.transID
where t1.accID like #headAcc+'%'
and...
It seems to work. Any other comments appreciated eg. if there is a more effective way.

See this example from MSDN on how to update one table based on data in other tables:
USE AdventureWorks2012;
GO
UPDATE Sales.SalesPerson
SET SalesYTD = SalesYTD + SubTotal
FROM Sales.SalesPerson AS sp
JOIN Sales.SalesOrderHeader AS so
ON sp.BusinessEntityID = so.SalesPersonID
AND so.OrderDate = (SELECT MAX(OrderDate)
FROM Sales.SalesOrderHeader
WHERE SalesPersonID = sp.BusinessEntityID);
GO

Related

Batch-wise fetch records from cursor

DECLARE table_cursor CURSOR LOCAL FAST_FORWARD READ_ONLY FOR
SELECT column5
FROM testdb.tableA
OPEN table_cursor;
FETCH NEXT FROM table_cursor INTO #m_column5
WHILE ##FETCH_STATUS = 0
BEGIN
//two update queries are here
END
CLOSE table_cursor;
DEALLOCATE table_cursor;
Let's say this returns 10 million records. Can I fetch the results batch-wise in SQL Server 2014?
Is there a better approach I can go for? (considering the performance fact)
It is very hard to figure out what you are trying to do with that cursor.
Based on whatever little you've shared, it seems that what you really want to do is an UPDATE based on a TOP clause:
UPDATE TOP (10) Sales.Store
SET SalesPersonID = 276
WHERE SalesPersonID = 275;
or an UPDATE FROM SELECT if you need a join:
UPDATE [dbo].[Events]
SET [datetime]=SYSDATETIME()
FROM (SELECT TOP 1000 [id] FROM [dbo].[Events] WHERE [datetime]<CONVERT(DATE,SYSDATETIME()) [e2]
INNER JOIN [dbo].[Events] ON [Events].[id] = [e2].[id];
If you find that you really do need a cursor to support your process, you can probably then go with:
WHILE EXISTS (SELECT 1 FROM [dbo].[Events] WHERE [datetime]<CONVERT(DATE,SYSDATETIME())
BEGIN
-- Your logic here
END;

Update From a Loop (Table Locking Issues + CLR Memory Issues)

I have a table with a geography column. I want to update this value, for a subset of rows (~1,000), based on the results of a query.
I have created a view that will return the geography column I want + the id of the row in the table to be updated.
If I run the query
UPDATE A
SET A.GeogCol = B.GeogCol
FROM TableToUpdate A
INNER JOIN UpdatedFrom_View B
ON A.ID = B.ID
I will receive a system out of memory error. This occurs because it is 32 bit sql server and the VAS reservation runs out of space because of the CLR functions I call to create the geography column. I am not the sever administrator, so I cannot temporarily allocate more space to the VAS reservation.
If I reduce this to WHERE A.ID BETWEEN 0 AND 100 and attempt to manually itterate, I still have this problem sometimes. I have found a few of the troublesome rows (larger geography objects than usual), and I can update those if I say WHERE A.ID = #ID.
My thought was then to update based on a loop.
DECLARE #ID INT
DECLARE #g GEOGRAPHY
DECLARE IDCursor CURSOR FOR SELECT ID FROM TableToUpdate
OPEN IDCursor
FETCH NEXT FROM IDCursor INTO #ID
WHILE ##FETCH_STATUS = 0
BEGIN
SET #g = (SELECT GeogCol FROM UpdatedFrom_View WHERE ID = #ID)
UPDATE TableToUpdate SET GeogCol = #g WHERE ID = #ID
FETCH NEXT FROM IDCursor INTO #ID
END
CLOSE IDCursor
DEALLOCATE IDCursor
However this appears to have issues with table locking (I think). It has now run for just under 2 days and has updated less than 150 records.
For reference, when done manually, UPDATE TableToUpdate SET GeogCol = #g WHERE ID = #ID takes less than 10 seconds.
Is there a better way to do this?
--EDIT
So I decided to test something.
I wrote this query which takes approximately 1 second.
UPDATE A
SET A.GeogCol = B.GeogCol
FROM TableToUpdate A
INNER JOIN UpdatedFrom_View B
ON A.ID = B.ID
WHERE A.ID BETWEEN 1 AND 1
Then I wrote a stored procedure that does the EXACT same thing.
CREATE PROCEDURE TestUpdate #IDLow INT, #IDHigh INT
AS
BEGIN
UPDATE A
SET A.GeogCol = B.GeogCol
FROM TableToUpdate A
INNER JOIN UpdatedFrom_View B
ON A.ID = B.ID
WHERE A.ID BETWEEN #IDLow AND #IDHigh
END
GO
EXEC TestUpdate #IDLow = 1, #IDHigh = 1
This query takes over half an hour. What is happening?

How can I improve this query for performance, provide status, and run as SQL job?

I have two tables, in two different databases. I am using one of the tables to update values in the other database table.
There are over 200,000 rows to iterate through, and it is taking several hours to run, on an Amazon c3.xlarge instance.
Below is the query I am running, and I am wondering three things:
Can this query be optimized to perform faster?
I would like to add a count to get the number of actual records
updated.How?
How can I turn this into a SQL job?
DECLARE #id VARCHAR(12) -- unique id
DECLARE #currentval VARCHAR(64) -- current value
DECLARE #newval VARCHAR(64) -- updated value
DECLARE db_cursor1 CURSOR FOR
SELECT b.[id], a.status, b.[New Status]
FROM db1.dbo.['account'] as b inner join db2.dbo.accounttemp as a on a.ACCOUNTID = b.[ID]
OPEN db_cursor1
FETCH NEXT FROM db_cursor1
INTO #id,
#currentval,
#newval
WHILE ##FETCH_STATUS = 0
BEGIN
UPDATE db2.dbo.accounttemp
SET status = #newval
WHERE ACCOUNTID = #id
AND STATUS = #currentval
FETCH NEXT FROM db_cursor1
INTO #id,
#currentval,
#newval
END
CLOSE db_cursor1
DEALLOCATE db_cursor1
By reviewing the procedure, you will see that you can completely remove the cursor using the following SQL
UPDATE db2.dbo.accounttemp
SET status = a.Status
FROM db2.dbo.accounttemp at
INNER JOIN db1.dbo.['account'] AS a ON a.Id = at.[ACCOUNTID]
WHERE a.Status = at.Status
call the following line to return the rows affected by the update
RETURN ##ROWCOUNT
You can create an SQL maintenance plan to run this on scheduled basis

SQL Server 2000 - Breaking out of a loop

I am not good at SQL Server 2000. I have a comma-delimited list of ids. I need to see if that ID exists in a table. If it does, I want to break out of the loop with that ID saved in a variable that I can use in my stored procedure. This is what I am trying right now:
DECLARE #coreID INT
SET #coreID=NULL
DECLARE #itemID NVARCHAR(50)
DECLARE itemCursor CURSOR LOCAL FAST_FORWARD FOR
SELECT [String] AS 'itemID' FROM dbo.SplitListIntoTable(#myIDs)
OPEN itemCursor
FETCH NEXT FROM itemCursor INTO #itemID
WHILE ##FETCH_STATUS = 0 BEGIN
-- If #itemID EXISTS IN MyTable set #coreID=#itemID and Break. How do I do this?
FETCH NEXT FROM itemCursor INTO #itemID
END
CLOSE itemCursor
DEALLOCATE itemCursor
Thank you!
Ideally, you shouldn't use a cursor as performance won't be great. If you can do it as a set-based statement, do that instead, maybe like this:
SELECT TOP 1 #CoreID = [String]
FROM dbo.SplitListIntoTable(#myIDs) x
JOIN MyTable t ON x.[String] = t.ID
However, if you have a real reason to use a cursor, you can use the BREAK statement to break out of a WHILE loop
e.g.
WHILE ##FETCH_STATUS = 0
BEGIN
IF EXISTS(SELECT * FROM MyTable WHERE Id = #ItemID)
BEGIN
SET #CoreId = #ItemId
BREAK
END
FETCH NEXT FROM itemCursor INTO #itemID
END
I don't know how to do this using a cursor, but I supect you can do this much better (faster) with a a join. If the output of dbo.SplitListIntoTable(#myIDs) is actually an odered table, then you can output a table with another column what is say the string numer, 1, 2, 3, etc...
(I don't have sql in front of me to test this but something like)
create table t(itemNum int identity, itemId nvarchar(max))
insert into t (item id) select 1 from dbo.SplitListIntoTable(#myIDs)
Then join the two and take the top one
set #coreID =
select top 1 #itemID
from MyTable m
inner join t t.itemid = m.itemid
order by m.itemNum asc
of course you could use a CTE, table var or temp table too.

How to update a column fetched by a cursor in TSQL

Before I go any further: Yes, I know that cursors perform poorly compared with set-based operations. In this particular case I'm running a cursor on a temporary table of 100 or so records, and that temporary table will always be fairly small, so performance is less crucial than flexibility.
My difficulty is that I'm having trouble finding an example of how to update a column fetched by a cursor. Previously when I've used cursors I've retrieved values into variables, then run an update query at each step based upon these values. On this occasion I want to update a field in the temporary table, yet I can't figure out how to do it.
In the example below, I'm trying to update the field CurrentPOs in temporary table #t1, based upon a query that uses #t1.Product_ID to look up the required value. You will see in the code that I have attempted to use the notation curPO.Product_ID to reference this, but it doesn't work. I have also attempted to use an update statement against curPO, also unsuccessfully.
I can make the code work by fetching to variables, but I'd like to know how to update the field directly.
I think I'm probably missing something obvious, but can anyone help?
declare curPO cursor
for select Product_ID, CurrentPOs from #t1
for update of CurrentPOs
open curPO
fetch next from curPO
while ##fetch_status = 0
begin
select OrderQuantity = <calculation>,
ReceiveQuantity = <calculation>
into #POs
from PurchaseOrderLine POL
inner join SupplierAddress SA ON POL.Supplier_ID = SA.Supplier_ID
inner join PurchaseOrderHeader POH ON POH.PurchaseOrder_ID = POL.PurchaseOrder_ID
where Product_ID = curPO.Product_ID
and SA.AddressType = '1801'
update curPO set CurrentPOs = (select sum(OrderQuantity) - sum(ReceiveQuantity) from #POs)
drop table #POs
fetch next from curPO
end
close curPO
deallocate curPO
After doing a bit more googling, I found a partial solution. The update code is as follows:
UPDATE #T1
SET CURRENTPOS = (SELECT SUM(ORDERQUANTITY) - SUM(RECEIVEQUANTITY)
FROM #POS)
WHERE CURRENT OF CURPO
I still had to use FETCH INTO, however, to retrieve #t1.Product_ID and run the query that produces #POs, so I'd still like to know if it's possible to use FETCH on it's own.
Is this what you want?
declare curPO cursor
for select Product_ID, CurrentPOs from #t1
for update of CurrentPOs
open curPO
fetch next from curPO
while ##fetch_status = 0
begin
update curPO set CurrentPOs =
(select sum(<OrderQuantityCalculation>)
from PurchaseOrderLine POL
inner join SupplierAddress SA ON POL.Supplier_ID = SA.Supplier_ID
inner join PurchaseOrderHeader POH ON POH.PurchaseOrder_ID = POL.PurchaseOrder_ID
where Product_ID = curPO.Product_ID
and SA.AddressType = '1801') -
(select sum(<ReceiveQuantityCalculation>)
from PurchaseOrderLine POL
inner join SupplierAddress SA ON POL.Supplier_ID = SA.Supplier_ID
inner join PurchaseOrderHeader POH ON POH.PurchaseOrder_ID = POL.PurchaseOrder_ID
where Product_ID = curPO.Product_ID
and SA.AddressType = '1801')
fetch next from curPO
end
close curPO
deallocate curPO
Maybe you need something like that:
update DataBaseName..TableName
set ColumnName = value
where current of your_cursor_name;
Here's an example to calculate one column based upon values from two others (note, this could be done during the original table select). This example can be copy / pasted into an SSMS query window to be run without the need for any editing.
DECLARE #cust_id INT = 2, #dynamic_val NVARCHAR(40), #val_a INT, #val_b INT
DECLARE #tbl_invoice table(Cust_ID INT, Cust_Fees INT, Cust_Tax INT)
INSERT #tbl_invoice ( Cust_ID, Cust_Fees, Cust_Tax ) SELECT 1, 111, 11
INSERT #tbl_invoice ( Cust_ID, Cust_Fees, Cust_Tax ) SELECT 2, 222, 22
INSERT #tbl_invoice ( Cust_ID, Cust_Fees, Cust_Tax ) SELECT 3, 333, 33
DECLARE #TblCust TABLE
(
Rec_ID INT
, Val_A INT
, Val_B INT
, Dynamic_Val NVARCHAR(40)
, PRIMARY KEY NONCLUSTERED (Rec_ID)
)
INSERT #TblCust(Rec_ID, Val_A, Val_B, Dynamic_Val)
SELECT Rec_ID = Cust_ID, Val_A = Cust_Fees, Val_B = Cust_Tax, NULL
FROM #tbl_invoice
DECLARE cursor_cust CURSOR FOR
SELECT Rec_ID, Val_A, Val_B, Dynamic_Val
FROM #TblCust
WHERE Rec_ID <> #cust_id
FOR UPDATE OF Dynamic_Val;
OPEN cursor_cust;
FETCH NEXT FROM cursor_cust INTO #cust_id, #val_a, #val_b, #dynamic_val;
WHILE ##FETCH_STATUS = 0
BEGIN
UPDATE #TblCust
SET Dynamic_Val = N'#c = "' + LTRIM(STR((#val_a + #val_b), 40)) + N'"'
WHERE CURRENT OF cursor_cust
FETCH NEXT FROM cursor_cust INTO #cust_id, #val_a, #val_b, #dynamic_val;
END
CLOSE cursor_cust
DEALLOCATE cursor_cust
SELECT * FROM #TblCust