SQL Server slow stored procedure that deletes - sql

I have written a stored procedure.
Now I see, that this is very poor performance.
I think this is because of the while loop.
ALTER PROCEDURE [dbo].[DeleteEmptyCatalogNodes]
#CatalogId UNIQUEIDENTIFIER,
#CatalogNodeType int = null
AS
BEGIN
SET NOCOUNT ON;
DECLARE #CID UNIQUEIDENTIFIER
DECLARE #CNT int
SET #CID = #CatalogId
SET #CNT = #CatalogNodeType
DELETE cn FROM CatalogNodes cn
LEFT JOIN CatalogNodes as cnj on cn.CatalogNodeId = cnj.ParentId
LEFT JOIN CatalogArticles as ca on cn.CatalogNodeId = ca.CatalogNodeId
WHERE cn.CatalogId = #CID
AND cnj.CatalogNodeId IS NULL
AND ca.ArticleId IS NULL
AND (cn.CatalogNodeType = #CNT OR #CNT IS NULL)
WHILE (##ROWCOUNT > 0)
BEGIN
DELETE cn FROM CatalogNodes cn
LEFT JOIN CatalogNodes as cnj on cn.CatalogNodeId = cnj.ParentId
LEFT JOIN CatalogArticles as ca on cn.CatalogNodeId = ca.CatalogNodeId
WHERE cn.CatalogId = #CID
AND cnj.CatalogNodeId IS NULL
AND ca.ArticleId IS NULL
AND (cn.CatalogNodeType = #CNT OR #CNT IS NULL)
END
END
Do anyone of you can give me a hint on how to do it more 'set' like?
Thanks a lot!
EDIT for comments and answers:
The tables are build like this:
CatalogNodes:
CatalogNodeId|ParentId|Name
1|NULL|Root
2|1|Node1
3|1|Node2
4|2|Node1.1
CatalogArticles:
CatalogNodeId|Name
3|Article1
3|Article2
3|Article3
After my SP was called, Node1 and Node1.1 have to be deleted.
In the first delete statement, Node1.1 will be deleted.
In the While loop, Node1 will be deleted.
I hope my problem is now easier to understand, it is a tree structure.

You just do not need WHILE part as all matched rows will get deleted from the first DELETE statement

your loop doesn't do anything ... the first delete statement will delete a number of records if there are any that comply to your where condition ... so ##rowcount will be greater than 0 but there won't be any records left to be deleted in your second delete statement inside the loop. or did I miss something?
anyway I don't think this executing delete two times in a row has a big influence on the performance ... you should see it if you look at the query plan ...

One way to do this in my point is to create a table variable and put all elements that you have to delete there and use i with join to make delete in one single statement.

CatalogNodes is what you want to delete. Create a select that pulls out all the CatalogNodes you want to get rid of. If there are things tied by foreign key constraints go and delete them first and finally once they are all gotten rid of Delete the CatalogNodes. Temporary tables could be of benefit here as they are held in memory.

Related

Trigger running every time despite conditional statement

I am trying to write an update trigger on a table which would cause it to run an additional update statement only if a certain column has been changed, so far the trigger runs the update no matter what, hoping maybe someone can see what I am doing wrong here.
Here is the trigger.
ALTER TRIGGER [dbo].[StatusChangedUpdateTrigger]
ON [dbo].[Trans_Order]
AFTER UPDATE
AS
DECLARE #OldOrderStatusId INT, #NewStatusOrderId INT, #ERRNUM INT;
BEGIN
SET #OldOrderStatusId = (SELECT OrderStatusId FROM deleted);
SET #NewStatusOrderId = (SELECT OrderStatusId FROM inserted);
IF (#OldOrderStatusId != #NewStatusOrderId)
SET NOCOUNT ON;
UPDATE Trans_Order
SET StatusChanged = 1
WHERE Id = (SELECT ID FROM inserted)
END
For some reason this is running no matter what, I can never set StatusChanged to 0 as it will automatically flip it back to 1 even if the OrderStatusId hasn't changed. So my update statement is running no matter what, so I am guessing I am doing something wrong in the if statement.
Hmmmm . . . Your logic seems strange. I would expect:
UPDATE t
SET StatusChanged = 1
FROM Trans_Order t JOIN
Inserted i
ON t.id = i.id JOIN
Deleted d
ON t.id = d.id
WHERE i.OrderStatusId <> d.OrderStatusId;
You might need to take NULL values into account -- although your code does not.
Note that your code is just a bug waiting to happen, because it assumes that inserted and deleted have only one row.
The specific problem with your code is that it is really:
IF (#OldOrderStatusId != #NewStatusOrderId)
BEGIN
SET NOCOUNT ON;
END;
UPDATE Trans_Order
SET StatusChanged = 1
WHERE Id = (SELECT ID FROM inserted);
Your indentation has confused the logic. However, you should still use the set-based version so the trigger does not fail.
The correct way to approach your trigger is as follows:
create or alter trigger [dbo].[StatusChangedUpdateTrigger] on [dbo].[Trans_Order]
after update
as
set nocount on
if ##RowCount=0 return
if Update(OrderStatusId)
begin
update t
set statusChanged=1
from inserted i join deleted d on d.id=i.id and d.OrderStatusId != i.OrderStatusId
join Trans_Order t on t.id=i.id
end
Always test ##rowcount and return if no rows updated.
Always put set options before DML
As you are only looking to update if a specific column is updated you can test specifically for that and if the update statement that's run doesn't touch that column the trigger will not run.
This will correctly account for multiple rows being updated and only update those where the new value is different to the old value.

SQL trigger for update

I was just trying to figure out how to do a basic trigger when I updated a row
Heres the setup
CREATE TABLE marriage(
personid int
married varchar(20)
);
INSERT INTO marriage
values (1, unmarried);
What im trying to do is create a sql trigger that will make it so that when I update a person can only go from married to divorced but not unmarried to divorced.
If anyone can help me with structuring this that would be great
This is what I was looking for if someone was looking for something similar
alter trigger
trigtest3
on married
for update
as
begin
declare #old varchar(20)
declare #new varchar(20)
select #old = married from deleted
select #new = married from inserted
if(#old like 'Unmarried' AND #new like 'Divorced')
rollback
end
SQL Server doesn't provide per-row triggers unfortunately, but only triggers for a complete command. And one single update command can update several rows, so you must look whether at least one affected row has undergone a forbidden change. You do this by joining the deleted and inserted pseudo tables on a column or a combination of columns that uniquely identify a record (i.e. the primary key).
create trigger trg_upd_married on marriage for update as
begin
declare #error_count int
select #error_count = count(*)
from deleted d
join inserted i on i.id = d.id
where d.married = 'Unmarried'
where i.married = 'Divorced'
if #error_count > 0
begin
raiserror('Unmarried persons cannot get divorced.', 16, 121)
rollback transaction
end
end;
The above trigger may still have errors. I am not fluent with TSQL (and just notice that I find its triggers quite clumsy - at least compared to Oracle's triggers I am used to).
You need to use instead of triggers as you need to prevent update. For update triggers are run after the insert happens. Use the following code -
create trigger abc on marriage
for instead of update
as
begin
Begin transaction
if exists(select 1 from deleted as a
inner join inserted as b
on a.personid = b.personid
where a.married = 'unmarried' and b.married = 'Divorced')
begin
raiserror('Status can not be changed from unmarried to Divorced',16,1)
Rollback transaction
end
else
begin
update a
set a.married = b.married
from marriage as a
inner join inserted as b
on a.personid = b.personid
Commit transaction
end
end
Let me know if this helps

Multiple Row Update SQL Trigger from Single Update SQL Statement

Ok. I am quite new to SQL triggers and have had some issues with them. Insert trigger works just fine, and the Delete trigger also. At first, doing a delete on multiple rows would only delete one, but I managed to figure that one out for myself :)
However, even after extensive searching (here and on Google) I am unable to find a satisfactory answer to the UPDATE trigger that I have. If I do an update like
UPDATE Customers Set CustomerUser = 0 Where CustomerStatus = 3
Then unfortunately, only the one record would be updated, and the other would remain as they were. Obviously, this is no good.
The trigger I am using is:
ALTER TRIGGER [dbo].[TRG_TriggerName] ON [dbo].[USER_Customers]
FOR UPDATE
AS
declare #customerid int;
declare #customervenue int;
declare #customeruser int;
declare #customerarea int;
declare #customerevent int;
declare #customerproject int;
declare #customerstatus int;
select #customerid=i.CustomerID from inserted i;
select #customervenue=i.CustomerVenue from inserted i;
select #customerarea=i.CustomerArea from inserted i;
select #customerevent=i.CustomerEvent from inserted i;
select #customerproject=i.CustomerProject from inserted i;
select #customeruser=i.CustomerUser from inserted i;
select #customerstatus=i.CustomerStatus from inserted i;
Update USER_Instances Set InstanceArea = #customerarea, InstanceVenue = #customervenue, InstanceUser = #customeruser, InstanceStatus = #customerstatus, InstanceEvent = #customerevent, InstanceLastUpdate = GetDate() Where InstanceObject = 17 AND InstanceIdentity = #customerid
GO
As you will immediately realize, this trigger is great - if you want to update just one record. Otherwise, it fails. Now - the main question here would be - How do I catch all the records that need updating, and update them all in one trigger action.
The examples I have seen here on Stack Overflow confuse me somewhat, or seem ineffective - for instance, it seems most of them deal with updating just ONE value in a second/other table, and not a whole bunch like I am trying to do. The ones that appear to work on multiple values, I can not understand :(
So after about 2 hours of searches, I give up, and hope that you can help me :) I realize this is a trigger-newbie issue, and though I know my MS-SQL, triggers are something I have never used, until now. So any help is greatly welcome :)
W
It seems that you need something like this
ALTER TRIGGER [dbo].[TRG_TriggerName] ON [dbo].[USER_Customers]
FOR UPDATE
AS
UPDATE USER_Instances
SET InstanceArea = i.CustomerArea,
InstanceVenue = i.CustomerVenue,
InstanceUser = i.CustomerUser,
InstanceStatus = i.CustomerStatus,
InstanceEvent = i.CustomerEvent,
InstanceLastUpdate = GetDate()
FROM USER_Instances JOIN inserted i
ON InstanceIdentity = i.CustomerID AND InstanceObject = 17
Since inserted virtual table can contain multiple rows you need to JOIN it to correctly do your UPDATE.

How to delete all rows without deleting the last returned row in SQL Server?

I want to delete all the rows from a SELECT without deleting the last returned row by using a trigger when a delete query is executed.
This trigger doesn't work so any help is greatly appreciated.
CREATE TRIGGER TR_StergereOfertaSpeciala
ON OferteSpeciale
INSTEAD OF DELETE
AS
DECLARE #nr INTEGER;
IF (EXISTS(SELECT * FROM DELETED))
BEGIN
SET #nr = (SELECT COUNT(*) FROM DELETED);
DELETE FROM (
SELECT TOP(#nr - 1)* FROM OferteSpeciale
INNER JOIN DELETED ON OferteSpeciale.codP = Deleted.codP
AND OferteSpeciale.codM = Deleted.codM
AND OferteSpeciale.dela = Deleted.dela)
END
Here is an example of getting your concept to work properly:
CREATE TRIGGER TR_StergereOfertaSpeciala
ON OferteSpeciale
INSTEAD OF DELETE
AS BEGIN
DECLARE #nr INT
SET #nr = (SELECT COUNT(*) FROM DELETED)
IF (#nr > 1) BEGIN
DELETE o
FROM OferteSpeciale AS o
INNER JOIN (SELECT TOP (#nr - 1) * FROM DELETED /* ORDER BY ??? */) AS d
ON o.codP = d.codP
AND o.codM = d.codM
AND o.dela = d.dela
END
END
Note the syntax for a delete with a join. Also note that we're arbitrarily choosing the 1 row to keep. I would suggest, as #RBarryYoung has mentioned, specifically ordering the set by something to know which row we are keeping.
Another way of doing this which could avoid the somewhat dynamic TOP clause (clever, BTW) would be to specifically exclude the record you want to keep using NOT EXISTS/IN
Also, you probably want to avoid trigger recursion and nested triggers in this case.

Optimize for speed a simple stored procedure

In SQL 2008 I've this easy-but-bad-write sp that works:
ALTER PROCEDURE [dbo].[paActualizaCapacidadesDeZonas]
AS
BEGIN
SET NOCOUNT ON;
DECLARE #IdArticulo AS INT
DECLARE #ZonaAct AS INT
DECLARE #Suma AS INT
UPDATE CapacidadesZonas SET Ocupado=0
DECLARE csrSumas CURSOR FOR
SELECT AT.IdArticulo, T.NumZona, SUM(AT.Cantidad)
FROM ArticulosTickets AT
INNER JOIN Tickets T ON AT.IdTicket = T.IdTicket
GROUP BY AT.IdArticulo, T.NumZona
OPEN csrSumas
FETCH NEXT FROM csrSumas INTO #IdArticulo, #ZonaAct, #Suma
WHILE ##FETCH_STATUS = 0
BEGIN
UPDATE CapacidadesZonas SET Ocupado = #Suma
WHERE NumZona = #ZonaAct AND IdArticulo = #IdArticulo
FETCH NEXT FROM csrSumas INTO #IdArticulo, #ZonaAct, #Suma
END
CLOSE csrSumas
DEALLOCATE csrSumas
END
I know: I must avoid cursors, so I'm pretty sure that it can be done in a much proper way.
I've tried with a single Update query:
UPDATE CapacidadesZonas SET Ocupado =
(SELECT SUM(AT.Cantidad)
FROM ArticulosTickets AT
INNER JOIN Tickets T ON AT.IdTicket = T.IdTicket
GROUP BY AT.IdArticulo, T.NumZona)
But this is really wrong, because the select returns more than one row.
I'm feeling bad with this, because it is supposed must be easy for me, but I can't find the equivalent query.
Any suggestions?
Thanks in advance.
There are many different solutions to this problem-- see this article for a few options. Here's one way: use a derived table.
UPDATE CapacidadesZonas SET Ocupado=0 WHERE Ocupado <> 0;
UPDATE CapacidadesZonas
SET Ocupado = SUM(s.Cantidad)
FROM CapacidadesZonas C INNER JOIN
(
SELECT T.NumZona, AT.IdArticulo, SUM(AT.Cantidad) as Ocupado
FROM ArticulosTickets AT
INNER JOIN Tickets T ON AT.IdTicket = T.IdTicket
GROUP BY AT.IdArticulo, T.NumZona
) s ON s.NumZona = C.NumZona AND s.IdArticulo = C.IdArticulo;
Caveats:
are you expecting that the CapacidadesZonas table is available to a live application while the update is happening? If so you may have a locking or perf issue since SQL will may lock the whole table for the update of every row. If this is the case, consider doing your update in batches (e.g. of 1,000 rows each). UPDATE TOP makes batching easy.
sometimes SQL picks a suboptimal plan for queries like this. it may be faster to load a temp table (like in astander's solution above, but using a temp table instead of a table var) than to try to do the update as a single query. If you do this, remember to make sure there's an index on (IDArticulo, NumZona) on the the temp table before you do your update.
Try:
UPDATE cz
SET Ocupado = SUM(AT.Cantidad)
FROM CapacidadesZonas as cz
INNER JOIN ArticulosTickets AT ON cz.numZona = at.numZona and cz.IDArticulo = at.IDArticulo
INNER JOIN Tickets T ON AT.IdTicket = T.IdTicket
GROUP BY AT.IdArticulo, T.NumZona