Selecting specific rows from DELETED table within a trigger - sql

I have an intermediate table that defines many-to-many relationship between, for instance, Customer and Orders, like this:
USE [master]
GO
CREATE DATABASE Example
GO
USE [Example]
GO
CREATE TABLE [dbo].[CustomerOrders](
[CustomerId] [int],
[OrderId] [int]
)
GO
INSERT INTO CustomerOrders (CustomerId, OrderId) VALUES (1, 1)
INSERT INTO CustomerOrders (CustomerId, OrderId) VALUES (1, 2)
INSERT INTO CustomerOrders (CustomerId, OrderId) VALUES (2, 1)
GO
CREATE TRIGGER [dbo].[CustomerOrdersRemoved]
ON [dbo].[CustomerOrders]
FOR DELETE
AS
BEGIN
SET NOCOUNT ON;
--IF NOT EXISTS (SELECT CustomerId FROM CustomerOrders
INNER JOIN deleted ON CustomerOrders.CustomerId=deleted.CustomerId)
--this wont work
END
GO
DELETE CustomerOrders WHERE OrderId=1
GO
Now, I need to have an ON DELETE trigger on this table, which would need to update another table based on customers who don't have any orders left in the table. In this case, after the DELETE operation the customer with CustomerId=1 will have 1 order with OrderId=2 left and customer with CustomerId=2 will have no orders left. So I need to get only customer with CustomerId=2 from the deleted vtable within the trigger.
How can I accomplish this?

The problem is that you're using deleted.CustomerId on the right side of a comparison.
I was doing something similar to what you're doing.
and the solution is to define a #variable to hold deleted.CustomerId (After your SET NO COUNT line), then compare CustomerOrders.CustomerId to the variable.
EDIT:
Here's the code which works just fine, It is a trigger to update movie rating by the average rating it got from tblRating(MovieID, UserID, rating) which also represents a many-to-many relationship between tblMovies and tblUsers, this one uses inserted but it is the same with deleted:
CREATE TRIGGER trUpdateRating
ON [dbo].[tblRating]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
DECLARE #rate float, #mid int
SET #rate = 0;
-- here is the assignment I was talking about and it is valid
SELECT #mid=MovieID from inserted;
SELECT #rate=AVG(isnull(Rating, 0)) FROM tblRating WHERE MovieID=#mid;
-- and here is the comparison
UPDATE tblMovies
SET avg_rating = #rate
WHERE ID=#mid;
END

My hands are faster than my head :-/
The answer is
SELECT CustomerId FROM deleted WHERE CustomerId NOT IN (SELECT CustomerId FROM CustomerOrders)

Related

How to update a temporary table with a value from a select statement?

I have created a temporary table to hold some values like this:
CREATE TABLE #TempCount (PendingOrders INT, OpenOrders INT, ClosedOrders INT);
I want to update the columns with a value from a SELECT statement as such:
UPDATE #TempCount
SET PendingOrders =
(
SELECT
COUNT(OrderID) AS 'PendingOrders'
FROM
dbo.Products
WHERE
OrderStatus = 1
)
However nothing seems to get updated. I expected PendingOrders to show a number but it doesn't get anything entered in. The column is empty. The same applies to the other columns - nothing gets updated.
How could I solve this?
You need a row in order to update it. Do one "initializing" INSERT first:
INSERT INTO #TempCount VALUES(0, 0, 0)
Seems to me like you need an insert statement first to generate the row.
INSERT #TempCount (PendingOrders)
SELECT COUNT(OrderID) AS 'PendingOrders'
FROM dbo.Products
WHERE OrderStatus = 1
If you just want to hold single values then consider variables
declare #PendingOrders int
set #PendingOrders = ( SELECT COUNT(OrderID)
FROM dbo.Products
WHERE OrderStatus = 1 )
select #PendingOrders, #OpenOrders, #ClosedOrders
Use this syntax:
UPDATE t
SET t.xxx=p.xxx
FROM #TempCount t, Products p
WHERE ......
Subselect is not always necessary.

SQL Server 2012 Trigger

I have a small little thing with SQL that's been bothering me now for a while, let's say I have two tables (Customer and Loan). However, I want a trigger that's checking based on the Borrowertype attribute. I suppose with the second query after AND I need something to check whether the userID in Loans are the same as the one in Customer, but must be messing it up or I'm completely thinking this the wrong way.
CREATE TABLE Customer
(
userID int identity primary key,
Name varchar(20),
Borrowertype varchar(20)
);
CREATE TABLE Loan
(
Id int identity primary key,
userID int,
FOREIGN KEY (userID) REFERENCES Customer(userID)
);
IF OBJECT_ID ('Customer.maximum_books_per_user','TR') IS NOT NULL
DROP TRIGGER Customer.maximum_books_per_user;
GO
CREATE TRIGGER maximum_books_per_user ON Customer
AFTER INSERT
AS
IF (SELECT Borrowertype FROM Customer) = 'diffborrowertypehere'
AND (SELECT COUNT(*) FROM inserted AS i JOIN Customer AS c
ON ??? WHERE ???
) > 5
BEGIN
ROLLBACK TRANSACTION
RAISERROR('You have reached maximum allowed loans.', 16, 1)
END
GO
Your trigger needs to be on the Loan table, as that's where a row would be being inserted that could be rejected. Something like this:
EDIT: rewritten to handle inserts for multiple Customers at once
CREATE TRIGGER maximum_books_per_user ON Loan
FOR INSERT
AS
-- Fail if there are any customers that will have more than the maximum number of loans
IF EXISTS (
SELECT i.userID, COUNT(*)
FROM inserted i
JOIN Loan l
ON i.userID = l.userID
GROUP BY i.userID
HAVING COUNT(*) >= 5
)
BEGIN
ROLLBACK TRANSACTION
RAISERROR('You have reached maximum allowed loans.', 16, 1)
END

For each inserted row create row in other table with foreign key constrain

I have 2 tables with foreign key constraint:
Table A:
[id] int identity(1, 1) PK,
[b_id] INT
and
Table B:
[id] int identity(1, 1) PK
where [b_id] refers to [id] column of Table B.
The task is:
On each insert into table A, and new record into table B and update [b_id].
Sql Server 2008 r2 is used.
Any help is appreciated.
Having misread this the first time, I am posting a totally different answer.
First if table B is the parent table, you insert into it first. Then you grab the id value and insert into table A.
It is best to do this is one transaction. Depending on what the other fields are, you can populate table A with a trigger from table B or you might need to write straight SQL code or a stored procedure to do the work.
It would be easier to describe what to do if you have a table schema for both tables. However, assuming table B only has one column and table A only has ID and B_id, this is the way the code could work (you would want to add explicit transactions for production code). The example is for a single record insert which would not happen from a trigger. Triggers should always handle multiple record inserts and it would have to be written differently then. But without knowing what the columns in the tables are it is hard to provide a good example of this.
create table #temp (id int identity)
create table #temp2 (Id int identity, b_id int)
declare #b_id int
insert into #temp default values
select #B_id = scope_identity()
insert into #temp2 (B_id)
values(#B_id)
select * from #temp2
Now the problem gets more complex if there are other columns, as you would have to provide values for them as well.
Without removing identity specification you can use the following option:
SET IDENTITY_INSERT B ON
Try this:
CREATE TRIGGER trgAfterInsert ON [dbo].[A]
FOR INSERT
AS
IF ##ROWCOUNT = 0 RETURN;
SET NOCOUNT ON;
SET IDENTITY_INSERT B ON
DECLARE #B_Id INT
SELECT #B_Id = ISNULL(MAX(Id), 0) FROM B;
WITH RES (ID, BIDS)
AS
(SELECT Id, #B_Id + ROW_NUMBER() OVER (ORDER BY Id) FROM INSERTED)
UPDATE A SET [b_Id] = BIDS
FROM A
INNER JOIN RES ON A.ID = RES.ID
INSERT INTO B (Id)
SELECT #B_Id + ROW_NUMBER() OVER (ORDER BY Id) FROM INSERTED
SET IDENTITY_INSERT B OFF
GO
Though Nadeem's answer is on the right track, his trigger for some reason takes max.id instead of NEW.id and doesn't update A accordingly.
For what you ask to be usable by trigger, you need the FK in table A to be nulleable, else you have a race condition between the tables.
EDIT: As SpectralGhost pointed out, my original code didn't support multiple rows, this one will do:
CREATE TRIGGER trgAfterInsertA ON TableA
FOR INSERT
AS
DECLARE db_cursor CURSOR FOR
SELECT id FROM INSERTED
DECLARE #an_id int
OPEN db_cursor
FETCH NEXT FROM db_cursor INTO #an_id
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO TableB VALUES(VALUE_PLACEHOLDER)
UPDATE TableA
SET b_id = SCOPE_IDENTITY()
WHERE id = #an_id
FETCH NEXT FROM db_cursor INTO #an_id
END
GO
The VALUE_PLACEHOLDER are the values you initialize TableB with.

How do I insert from a table variable to a table with an identity column, while updating the the identity on the table variable?

I'm writing a SQL script to generate test data for our database. I'm generating the data in table variables (so I can track it later) and then inserting it into the real tables. The problem is, I need to track which rows I've added to the parent table, so that I can generate its child data later on in the script. For example:
CREATE TABLE Customer (
CustomerId INT IDENTITY,
Name VARCHAR(50)
)
CREATE TABLE Order (
OrderId INT IDENTITY,
CustomerId INT,
Product VARCHAR(50)
)
So, in my script, I create equivalent table variables:
DECLARE #Customer TABLE (
CustomerId INT IDENTITY,
Name VARCHAR(50)
) -- populate customers
DECLARE #Order TABLE (
OrderId INT IDENTITY,
CustomerId INT,
Product VARCHAR(50)
) -- populate orders
And I generate and insert sample data into each table variable.
Now, when I go to insert customers from my table variable into the real table, the CustomerId column in the table variable will become meaningless, as the real table has its own identity seed for its CustomerId column.
Is there a way I can track the new identity of each row inserted into the real table, in my table variable, so I can use a proper CustomerId for the order records? Or, is there a better way I should be going about this?
(Note: I originally started with an application to generate the test data, but it ran too slow during insert as > 1,000,000 records need to be generated.)
WHy do you need identity values on the table variables? If you use just int, you can isnert the ids after the insert is done. Grab them using the output clause. YOu might need an input values and an output values table varaiable to get this just right like this:
DECLARE #CustomerInputs TABLE (Name VARCHAR(50) )
DECLARE #CustomerOutputs TABLE (CustomerId INT ,Name VARCHAR(50) )
INSERT INTO CUSTOMERS (name)
OUTPUT inserted.Customerid, inserted.Name INTO #CustomerOutputs
SELECT Name FROM #CustomerInputs
SELECT * from #CustomerOutputs
You can insert the data to the table with a cursor and use the built-in function SCOPE_IDENTITY() to get the last id which was inserted in the current scope (by your script).
See this MSDN article for more information on SCOPE_IDENTITY.
Here is one way of doing it. If you can use it depends on your situation. You should not do it in production environment when users use your db.
-- Get the next identity values for Customer and Order
declare #NextCustomerID int
declare #NextOrderID int
set #NextCustomerID = IDENT_CURRENT('Customer')+1
set #NextOrderID = IDENT_CURRENT('Order')+1
-- Create tmp tables
create table #Customer (CustomerID int identity, Name varchar(50))
create table #Order (OrderID int identity, CustomerID int, Product varchar(50))
-- Reseed the identity columns in temp tables
dbcc checkident(#Customer, reseed, #NextCustomerID)
dbcc checkident(#Order, reseed, #NextOrderID)
-- Populate #Customer
-- Populate #Order
-- Allow insert to identity column on Customer
set identity_insert Customer on
-- Add rows to Customer
insert into Customer(CustomerId, Name)
select CustomerID, Name
from #Customer
-- Restore identity functionality on Customer
set identity_insert Customer off
-- Add rows to Order
set identity_insert [Order] on
insert into [Order](OrderID, CustomerID, Product)
select OrderID, CustomerID, Product
from #Order
set identity_insert [Order] off
-- Drop temp tables
drop table #Customer
drop table #Order
-- Check result
select * from [Order]
select * from Customer
The way I'd do it its first obtain the MAX(CustomerId) from your Customer Table. Then I'd get rid of the IDENTITY column on your variable table and do my own CustomerId using ROW_NUMBER() and the MaxCustomerId. It should be something like this:
DECLARE #MaxCustomerId INT
SELECT #MaxCustomerId = ISNULL(MAX(CustomerId),0)
FROM Customer
DECLARE #Customer TABLE (
CustomerId INT,
Name VARCHAR(50)
)
INSERT INTO #Customer(CustomerId, Name)
SELECT #MaxCustomerId + ROW_NUMBER() OVER(ORDER BY SomeColumn), Name
FROM YourDataTable
Or insert the values on a temp table, so you can use the same ids to fill your Order table.

Using temporary table in where clause

I want to delete many rows with the same set of field values in some (6) tables. I could do this by deleting the result of one subquery in every table (Solution 1), which would be redundant, because the subquery would be the same every time; so I want to store the result of the subquery in a temporary table and delete the value of each row (of the temp table) in the tables (Solution 2). Which solution is the better one?
First solution:
DELETE FROM dbo.SubProtocols
WHERE ProtocolID IN (
SELECT ProtocolID
FROM dbo.Protocols
WHERE WorkplaceID = #WorkplaceID
)
DELETE FROM dbo.ProtocolHeaders
WHERE ProtocolID IN (
SELECT ProtocolID
FROM dbo.Protocols
WHERE WorkplaceID = #WorkplaceID
)
// ...
DELETE FROM dbo.Protocols
WHERE WorkplaceID = #WorkplaceID
Second Solution:
DECLARE #Protocols table(ProtocolID int NOT NULL)
INSERT INTO #Protocols
SELECT ProtocolID
FROM dbo.Protocols
WHERE WorkplaceID = #WorkplaceID
DELETE FROM dbo.SubProtocols
WHERE ProtocolID IN (
SELECT ProtocolID
FROM #Protocols
)
DELETE FROM dbo.ProtocolHeaders
WHERE ProtocolID IN (
SELECT ProtocolID
FROM #Protocols
)
// ...
DELETE FROM dbo.Protocols
WHERE WorkplaceID = #WorkplaceID
Is it possible to do solution 2 without the subquery? Say doing WHERE ProtocolID IN #Protocols (but syntactically correct)?
I am using Microsoft SQL Server 2005.
While you can avoid the subquery in SQL Server with a join, like so:
delete from sp
from subprotocols sp
inner join protocols p on
sp.protocolid = p.protocolid
and p.workspaceid = #workspaceid
You'll find that this doesn't gain you really any performance over either of your approaches. Generally, with your subquery, SQL Server 2005 optimizes that in into an inner join, since it doesn't rely on each row. Also, SQL Server will probably cache the subquery in your case, so shoving it into a temp table is most likely unnecessary.
The first way, though, would be susceptible to changes in Protocols during the transactions, where the second one wouldn't. Just something to think about.
Can try this
DELETE FROM dbo.ProtocolHeaders
FROM dbo.ProtocolHeaders INNER JOIN
dbo.Protocols ON ProtocolHeaders.ProtocolID = Protocols.ProtocolID
WHERE Protocols.WorkplaceID = #WorkplaceID
DELETE ... FROM is a T-SQL extension to the standard SQL DELETE that provides an alternative to using a subquery. From the help:
D. Using DELETE based on a subquery
and using the Transact-SQL extension
The following example shows the
Transact-SQL extension used to delete
records from a base table that is
based on a join or correlated
subquery. The first DELETE statement
shows the SQL-2003-compatible subquery
solution, and the second DELETE
statement shows the Transact-SQL
extension. Both queries remove rows
from the SalesPersonQuotaHistory table
based on the year-to-date sales stored
in the SalesPerson table.
-- SQL-2003 Standard subquery
USE AdventureWorks;
GO
DELETE FROM Sales.SalesPersonQuotaHistory
WHERE SalesPersonID IN
(SELECT SalesPersonID
FROM Sales.SalesPerson
WHERE SalesYTD > 2500000.00);
GO
-- Transact-SQL extension
USE AdventureWorks;
GO
DELETE FROM Sales.SalesPersonQuotaHistory
FROM Sales.SalesPersonQuotaHistory AS spqh
INNER JOIN Sales.SalesPerson AS sp
ON spqh.SalesPersonID = sp.SalesPersonID
WHERE sp.SalesYTD > 2500000.00;
GO
You would want, in your second solution, something like
-- untested!
DELETE FROM
dbo.SubProtocols -- ProtocolHeaders, etc
FROM
dbo.SubProtocols
INNER JOIN #Protocols ON SubProtocols.ProtocolID = #Protocols.ProtocolID
However!!
Is it not possible to alter your design so that all the susidiary protocol tables have a FOREIGN KEY with DELETE CASCADE to the main Protocols table? Then you could just DELETE from Protocols and the rest would be taken care of...
edit to add:
If you already have FOREIGN KEYs set up, you would need to use DDL to alter them (I think a drop and recreate is required) in order for them to have DELETE CASCADE turned on. Once that is in place, a DELETE from the main table will automatically DELETE related records from the child table.
Without the temp table you risk deleting different rows in the the second delete, but that takes three operations to do.
You could delete from the first table and use the OUTPUT INTO clause to insert into a temp table all the IDs, and then use that temp table to delete the second table. This will make sure you only delete the same keys with and with only two statements.
declare #x table(RowID int identity(1,1) primary key, ValueData varchar(3))
declare #y table(RowID int identity(1,1) primary key, ValueData varchar(3))
declare #temp table (RowID int)
insert into #x values ('aaa')
insert into #x values ('bab')
insert into #x values ('aac')
insert into #x values ('bad')
insert into #x values ('aae')
insert into #x values ('baf')
insert into #x values ('aag')
insert into #y values ('aaa')
insert into #y values ('bab')
insert into #y values ('aac')
insert into #y values ('bad')
insert into #y values ('aae')
insert into #y values ('baf')
insert into #y values ('aag')
DELETE #x
OUTPUT DELETED.RowID
INTO #temp
WHERE ValueData like 'a%'
DELETE y
FROM #y y
INNER JOIN #temp t ON y.RowID=t.RowID
select * from #x
select * from #y
SELECT OUTPUT:
RowID ValueData
----------- ---------
2 bab
4 bad
6 baf
(3 row(s) affected)
RowID ValueData
----------- ---------
2 bab
4 bad
6 baf
(3 row(s) affected)