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

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.

Related

Using IF UPDATE on SQL Trigger when handling multiple inserted/updated records

I use this SQL Server trigger to look for insert/update of multiple records from a specific table and put it into another queue table (for processing later).
ALTER TRIGGER [dbo].[IC_ProductUpdate] ON [dbo].[StockItem]
AFTER INSERT, UPDATE
AS
BEGIN
SELECT RowNum = ROW_NUMBER() OVER(ORDER BY ItemID) , ItemID
INTO #ProductUpdates
FROM INSERTED;
DECLARE #MaxRownum INT;
SET #MaxRownum = (SELECT MAX(RowNum) FROM #ProductUpdates);
DECLARE #Iter INT;
SET #Iter = (SELECT MIN(RowNum) FROM #ProductUpdates);
WHILE #Iter <= #MaxRownum
BEGIN
-- Get Product Id
DECLARE #StockItemID INT = (SELECT ItemID FROM #ProductUpdates WHERE RowNum = #Iter);
-- Proceed If This Product Is Sync-able
IF (dbo.IC_CanSyncProduct(#StockItemID) = 1)
BEGIN
-- Check If There Is A [ProductUpdate] Queue Entry Already Exist For This Product
IF ((SELECT COUNT(*) FROM IC_ProductUpdateQueue WHERE StockItemID = #StockItemID) > 0)
BEGIN
-- Reset [ProductUpdate] Queue Entry
UPDATE IC_ProductUpdateQueue
SET Synced = 0
WHERE StockItemID = #StockItemID
END
ELSE
BEGIN
-- Insert [ProductUpdate] Queue Entry
INSERT INTO IC_ProductUpdateQueue (StockItemID, Synced)
VALUES (#StockItemID, 0)
END
END
SET #Iter = #Iter + 1;
END
DROP TABLE #ProductUpdates;
END
This works fine, however I only want the above trigger to react if certain columns were updated.
The columns I am interested in are:
Name
Description
I know I can use the following T-SQL syntax to check if a column really updated (during update event) like this:
IF (UPDATE(Name) OR UPDATE(Description))
BEGIN
// do something...
END
But, I am not sure how to incorporate this into the above trigger, since my trigger handles multiple rows being updated at same time also.
Any ideas? At which point in the trigger could i use IF (UPDATE(colX))?
First, I would suggest to have one separate trigger for each operation - one for INSERT, and another for UPDATE. Keeps the code cleaner (less messy IF statements and so forth).
The INSERT trigger is pretty simple, since there's nothing to check for updating - and there's absolutely no need for a temporary table and a slow WHILE loop - just two simple, set-based statements and you're done:
CREATE TRIGGER [dbo].[IC_ProductInsert] ON [dbo].[StockItem]
AFTER INSERT
AS
BEGIN
-- update the queue for those entries that already exist
-- those rows that *DO NOT* exist yet are not being touched
UPDATE puq
SET Synced = 0
FROM dbo.IC_ProductUpdateQueue puq
INNER JOIN Inserted i ON puq.StockItemID = i.StockItemID
-- for those rows that don't exist yet - insert the values
INSERT INTO dbo.IC_ProductUpdateQueue (StockItemID, Synced)
SELECT
i.StockItemID, 0
FROM
Inserted i
WHERE
NOT EXISTS (SELECT * FROM dbo.IC_ProductUpdateQueue puq
WHERE puq.StockItemID = i.StockItemID)
END
The UPDATE trigger needs one extra check - to see whether or not one of the two columns of interest has changed. This can be handled quite easily by combining the Inserted pseudo table with the new values (after the UPDATE), and the Deleted pseudo table with the "old" values (before the UPDATE):
ALTER TRIGGER [dbo].[IC_ProductUpdate] ON [dbo].[StockItem]
AFTER UPDATE
AS
BEGIN
-- update the queue for those entries that already exist
-- those rows that *DO NOT* exist yet are not being touched
UPDATE puq
SET Synced = 0
FROM dbo.IC_ProductUpdateQueue puq
INNER JOIN Inserted i ON puq.StockItemID = i.StockItemID
INNER JOIN Deleted d ON d.StockItemID = i.StockItemID
WHERE
i.Name <> d.Name OR i.Description <> d.Description
-- for those rows that don't exist yet - insert the values
INSERT INTO dbo.IC_ProductUpdateQueue (StockItemID, Synced)
SELECT
i.StockItemID, 0
FROM
Inserted i
INNER JOIN
Deleted d ON d.StockItemID = i.StockItemID
WHERE
i.Name <> d.Name OR i.Description <> d.Description
AND NOT EXISTS (SELECT * FROM dbo.IC_ProductUpdateQueue puq
WHERE puq.StockItemID = i.StockItemID)
END
You can join to deleted and use where I.Name <> D.Name...
https://www.mssqltips.com/sqlservertip/2342/understanding-sql-server-inserted-and-deleted-tables-for-dml-triggers/

Create a function for generating random number in SQL Server trigger

I have to create a function in a SQL Server trigger for generating random numbers after insert. I want to update the column with that generated random number please help what I have missed in my code.
If you know other ways please suggest a way to complete my task.
This my SQL Server trigger:
ALTER TRIGGER [dbo].[trgEnquiryMaster]
ON [dbo].[enquiry_master]
AFTER INSERT
AS
declare #EnquiryId int;
declare #ReferenceNo varchar(50);
declare #GenReferenceNo NVARCHAR(MAX);
select #EnquiryId = i.enquiry_id from inserted i;
select #ReferenceNo = i.reference_no from inserted i;
BEGIN
SET #GenReferenceNo = 'CREATE FUNCTION functionRandom (#Reference VARCHAR(MAX) )
RETURNS VARCHAR(MAX)
As
Begin
DECLARE #r varchar(8);
SELECT #r = coalesce(#r, '') + n
FROM (SELECT top 8
CHAR(number) n FROM
master..spt_values
WHERE type = P AND
(number between ascii(0) and ascii(9)
or number between ascii(A) and ascii(Z)
or number between ascii(a) and ascii(z))
ORDER BY newid()) a
RETURNS #r
END
'
EXEC(#GenReferenceNo)
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON
-- update statements for trigger here
UPDATE enquiry_master
SET reference_no ='updated'
WHERE enquiry_id = #EnquiryId
END
To generate random numbers, just call CRYPT_GEN_RANDOM which was introduced in SQL Server 2008:
SELECT CRYPT_GEN_RANDOM(5) AS [Hex],
CONVERT(VARCHAR(20), CRYPT_GEN_RANDOM(5), 2) AS [HexStringWithout0x],
CONVERT(VARCHAR(20), CRYPT_GEN_RANDOM(10)) AS [Translated-ASCII],
CONVERT(NVARCHAR(20), CRYPT_GEN_RANDOM(20)) AS [Translated-UCS2orUTF16]
returns:
Hex HexStringWithout0x Translated-ASCII Translated-UCS2orUTF16
0x4F7D9ABBC4 0ECF378A7A ¿"bü<ݱØï 붻槬㟰添䛺⯣왚꒣찭퓚
If you are ok with just 0 - 9 and A - F, then the CONVERT(VARCHAR(20), CRYPT_GEN_RANDOM(5), 2) is all you need.
Please see my answer on DBA.StackExchange on a similar question for more details:
Password generator function
The UPDATE statement shown in the "Update" section of that linked answer is what you want, just remove the WHERE condition and add the JOIN to the Inserted pseudo-table.
The query should look something like the following:
DECLARE #Length INT = 10;
UPDATE em
SET em.[reference_no] = rnd.RandomValue
FROM dbo.enquiry_master em
INNER JOIN Inserted ins
ON ins.enquiry_id = em.enquiry_id
CROSS APPLY dbo.GenerateReferenceNo(CRYPT_GEN_RANDOM((em.[enquiry_id] % 1) + #Length)) rnd;
And since the function is slightly different, here is how it should be in order to get both upper-case and lower-case letters:
CREATE FUNCTION dbo.GenerateReferenceNo(#RandomValue VARBINARY(20))
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN
WITH base(item) AS
(
SELECT NULL UNION ALL SELECT NULL UNION ALL SELECT NULL UNION ALL
SELECT NULL UNION ALL SELECT NULL UNION ALL SELECT NULL
), items(item) AS
(
SELECT NULL
FROM base b1
CROSS JOIN base b2
)
SELECT (
SELECT TOP (LEN(#RandomValue))
SUBSTRING('1234567890QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm',
(CONVERT(TINYINT, SUBSTRING(#RandomValue, 1, 1)) % 62) + 1,
1) AS [text()]
FROM items
FOR XML PATH('')
) AS [RandomReferenceNo];
GO
And please follow the usage shown above, passing in CRYPT_GEN_RANDOM((em.[enquiry_id] % 1) + #Length), not: CRYPT_GEN_RANDOM(#RefferenceNOLength).
Other notes:
#marc_s already explained the one-row vs multiple-rows flaw and how to fix that.
not only is a trigger not the place to create a new object (i.e. the function), that function wouldn't have worked anyway since the call to newid() (in the ORDER BY) is not allowed in a function.
You don't need to issue two separate SELECTs to set two different variables. You could do the following:
SELECT #EnquiryId = i.enquiry_id,
#ReferenceNo = i.reference_no
FROM TableName i;
Passing strings into a function requires quoting those strings inside of single-quotes: ASCII('A') instead of ASCII(A).
UPDATE
The full Trigger definition should be something like the following:
ALTER TRIGGER [dbo].[trgEnquiryMaster]
ON [dbo].[enquiry_master]
AFTER INSERT
AS
BEGIN
DECLARE #Length INT = 10;
UPDATE em
SET em.[reference_no] = rnd.RandomValue
FROM dbo.enquiry_master em
INNER JOIN Inserted ins
ON ins.enquiry_id = em.enquiry_id
CROSS APPLY dbo.GenerateReferenceNo(
CRYPT_GEN_RANDOM((em.[enquiry_id] % 1) + #Length)
) rnd;
END;
A trigger should be very nimble and quick - it is no place to do heavy and time-intensive processing, and definitely no place to create new database objects since (a) the trigger is executed in the context of the code causing it to fire, and (b) you cannot control when and how often the trigger is fired.
You need to
define and create your function to generate that random value during database setup - once, before any operations are executed on the database
rewrite your trigger to take into account that multiple rows could be inserted at once, and in that case, the Inserted table will contain multiple rows which all have to be handled.
So your trigger will look something like this (with several assumptions by me - e.g. that enquiry_id is the primary key on your table - you need this to establish the INNER JOIN between your data table and the Inserted pseudo table:
ALTER TRIGGER [dbo].[trgEnquiryMaster]
ON [dbo].[enquiry_master]
AFTER INSERT
AS
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON
-- update statements for trigger here
UPDATE enq
SET reference_no = dbo.GenerateRandomValue(.....)
FROM enquiry_master enq
INNER JOIN inserted i ON enq.enquiry_id = i.enquiry_id

SQL Server slow stored procedure that deletes

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.

how to debug a recursive trigger

I have a recursive trigger, that seems to do exactly what I want it to with no recursion, however when I turn recursion on I get the error: "Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32)"
This should not happen, as I expect 2 or maybe 3 levels of nesting, so I need to debug the trigger and work out what exactly is going on. I added a print statement, but that does not work...
How do you go about debugging a recursive trigger?
ALTER TRIGGER [dbo].[DataInstance2_Trigger]
ON [dbo].[DataInstance]
AFTER UPDATE
AS
BEGIN
UPDATE DataInstance
SET
DataInstance.IsCurrent = i.IsCurrent
FROM DataInstance di
Inner join DataContainer dc on
di.DataContainerId = dc.DataContainerId
Inner join Inserted i on
dc.ParentDataContainerId = i.DataContainerId
WHERE di.IsCurrent != i.IsCurrent
declare #x int
SET #X = (select max(DataContainerId) from Inserted)
print #X
END
You can include a SELECT statement in your trigger (I've just tried this - try SELECT * FROM DataInstance prior to the UPDATE in the trigger).
I've repro'ed the problem - the UPDATE in the trigger is causing the trigger to fire again even if there are no rows updated. A suitable fix would be to wrap the UPDATE statement within the trigger in an IF (SELECT COUNT(*) FROM INSERTED) <> 0 BEGIN ... END block.

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