I am using sql-server 2012 and i wrote a trigger like this:
ALTER TRIGGER [dbo].[ModValue]
ON [dbo].[Table1]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
declare #ocid bigint
declare #ncid bigint,#pid bigint
set #ocid=(select CategoryId from deleted)
select #ncid=CategoryId,#pid=Id from inserted
if(#ocid<>#ncid)
begin
delete from [Table2] where ProductId=#pid
delete from [Table3] where ProductId=#pid
delete from [Table4] where ProductId=#pid
end
END
When i want to update my table(Table1) i got this error:
Msg 512, Level 16, State 1, Procedure ModValue, Line 15
Subquery returned more than 1 value. This is not permitted when the subquery
follows =, !=, <, <= , >, >= or when the subquery is used as an
expression. The statement has been terminated.
Update query:
update Table1
set sizing = 0
where categoryid = 238
What is wrong with this scripts?
The DELETED and INSERTED tables may contain more than one row.
DELETED - contains data that existed before modifications (old)
INSERTED - modified data (new).
So you have to find out where CategoryID was changed:
...
from
Inserted new
inner join
Deleted old on old.id = new.id
where
new.CategoryID <> old.CategoryID
To do something with that while keeping in mind that there could be many rows, for example:
delete from [Table2] t2
where exists (select 1
from Inserted new
inner join Deleted old on old.id = new.id
where new.CategoryID <> old.CategoryID
and t2.ProductId = d.ProductID)
Note that ProductID could be changed by UPDATE statement too.
I think the following line is the problem:
set #ocid=(select CategoryId from deleted)
SET command expects at most 1 line from the SELECT statement. deleted table may contain more than 1 line, as trigger will fire at batch level, not record level.
Related
I am hoping someone can help me out of this tedium...!?
As the title suggests I have a Temp Table (create dynamically in a select statement):
SELECT *
INTO #results
FROM Table_1
CROSS APPLY ( SELECT TOP 1 *
FROM Table_2
WHERE (Table_1.ItemId = Table_2.ItemId)
ORDER BY CreatedDt DESC
)
... which as you can see uses a Sub-Query in a cross join.
Next I am trying to use this temp table #results to update a related table with its values. have tried using an update:
UPDATE a
SET a.StatusId = b.StatusId
FROM Table_1 a
INNER JOIN #results b on (a.ItemId = b.ItemId)
and with a Merge:
MERGE INTO Table_1 a
USING #results b
ON (a.ItemId = b.temId)
WHEN MATCHED THEN UPDATE SET a.StatusId = b.StatusId;
but I seem to always get a response:
Msg 512, Level 16, State 1, Procedure trg_dbo_PBITree_TreeModel_HierarchicalEscalationHistory_InsertNode,
Line 7 [Batch Start Line 11] Subquery returned more than 1 value. This
is not permitted when the subquery follows =, !=, <, <= , >, >= or
when the subquery is used as an expression.
When I query the two tables in question (#results & Table_1) they both have 1 to 1 relationships and cannot see at all where it could be hiding some kind of Subquery!?
Can anyone help quickly on this at all please? This seems to be 1-0-1 stuff and its baking my burger!
-- Edit --
I have taken a look at the Trigger mentioned in the error message as it was suggested it could be trying to handle a single row update instead of a multiple row update which is what I am doing. Nothing looking too unusual to me...?
ALTER TRIGGER [dbo].[trg_dbo_PBITree_TreeModel_HierarchicalEscalationHistory_InsertNode]
ON [dbo].[Table_1]
AFTER UPDATE
AS
BEGIN
-- NodeModelInsertOrUpdateTPH
IF ((select [Item] from inserted) = 'X')
BEGIN
UPDATE tx
SET
tx.LastUpdatedBy = i.LastUpdatedBy,
tx.LastUpdatedAt = i.LastUpdatedAt
FROM
[dbo].[Table_X] tx,
inserted i
WHERE
tx.OtherItemId = i.OtherItemId
END
END
Anyone have any ideas?
Your trigger is the issue here. Your IF statement has a query which would return more than 1 row and that exact message would be the result. You should make your trigger tolerant of multiple row operations. Here is the same logic but it can handle any number of rows being updated.
ALTER TRIGGER [dbo].[trg_dbo_PBITree_TreeModel_HierarchicalEscalationHistory_InsertNode]
ON [dbo].[Table_1]
AFTER UPDATE
AS
BEGIN
UPDATE tx
SET
tx.LastUpdatedBy = i.LastUpdatedBy,
tx.LastUpdatedAt = i.LastUpdatedAt
FROM
[dbo].[Table_X] tx
join inserted i ON tx.OtherItemId = i.OtherItemId
where i.Item = 'X'
END
Sorry for my english.
I have 2 tables:
Table1
id
table2_id
num
modification_date
and
Table2
id
table2num
I want to make a trigger which after insert or delete in Table1 updates the last value num in Table2.table1lastnum.
My trigger:
CREATE OR REPLACE TRIGGER TABLE1_NUM_TRG
AFTER INSERT OR DELETE ON table1
FOR EACH ROW
BEGIN
IF INSERTING then
UPDATE table2
SET table2num = :new.num
WHERE table2.id = :new.table2_id;
ELSE
UPDATE table2
SET table2num = (SELECT num FROM (SELECT num FROM table1 WHERE table2_id = :old.table2_id ORDER BY modification_date DESC) WHERE ROWNUM <= 1)
WHERE table2.id = :old.table2_id;
END IF;
END TABLE1_NUM_TRG;
But after delete in Table1 I have error:
ORA-04091: table BD.TABLE1 is mutating, trigger/function may not see it
ORA-06512: at "BD.TABLE1_NUM_TRG", line 11
ORA-04088: error during execution of trigger 'BD.TABLE1_NUM_TRG'
What am I doing wrong?
What you've run into is the classic "mutating table" exception. In a ROW trigger Oracle does not allow you to run a query against the table which the trigger is defined on - so it's the SELECT against TABLE1 in the DELETING part of the trigger that's causing this issue.
There are a couple of ways to work around this. Perhaps the best in this situation is to use a compound trigger, which would look something like:
CREATE OR REPLACE TRIGGER TABLE1_NUM_TRG
FOR INSERT OR DELETE ON TABLE1
COMPOUND TRIGGER
TYPE NUMBER_TABLE IS TABLE OF NUMBER;
tblTABLE2_IDS NUMBER_TABLE;
BEFORE STATEMENT IS
BEGIN
tblTABLE2_IDS := NUMBER_TABLE();
END BEFORE STATEMENT;
AFTER EACH ROW IS
BEGIN
IF INSERTING THEN
UPDATE TABLE2 t2
SET t2.TABLE2NUM = :new.NUM
WHERE t2.ID = :new.TABLE2_ID;
ELSIF DELETING THEN
tblTABLE2_IDS.EXTEND;
tblTABLE2_IDS(tblTABLE2_IDS.LAST) := :new.TABLE2_ID;
END IF;
END AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
IF tblTABLE2_IDS.COUNT > 0 THEN
FOR i IN tblTABLE2_IDS.FIRST..tblTABLE2_IDS.LAST LOOP
UPDATE TABLE2 t2
SET t2.TABLE2NUM = (SELECT NUM
FROM (SELECT t1.NUM
FROM TABLE1 t1
WHERE t1.TABLE2_ID = tblTABLE2_IDS(i)
ORDER BY modification_date DESC)
WHERE ROWNUM = 1)
WHERE t2.ID = tblTABLE2_IDS(i);
END LOOP;
END IF;
END AFTER STATEMENT;
END TABLE1_NUM_TRG;
A compound trigger allows each timing point (BEFORE STATEMENT, BEFORE ROW, AFTER ROW, and AFTER STATEMENT) to be handled. Note that the timing points are always invoked in the order given. When an appropriate SQL statement (i.e. INSERT INTO TABLE1 or DELETE FROM TABLE1) is executed and this trigger is fired the first timing point to be invoked will be BEFORE STATEMENT, and the code in the BEFORE STATEMENT handler will allocate a PL/SQL table to hold a bunch of numbers. In this case the numbers to be stored in the PL/SQL table will be the TABLE2_ID values from TABLE1. (A PL/SQL table is used instead of, for example, an array because a table can hold a varying number of values, while if we used an array we'd have to know in advance how many numbers we would need to store. We can't know in advance how many rows will be affected by a particular statement, so we use a PL/SQL table).
When the AFTER EACH ROW timing point is reached and we find that the statement being processed is an INSERT, the trigger just goes ahead and performs the necessary UPDATE to TABLE2 as this won't cause a problem. However, if a DELETE is being performed the trigger saves the TABLE1.TABLE2_ID into the PL/SQL table allocated earlier. When the AFTER STATEMENT timing point is finally reached, the PL/SQL table allocated earlier is iterated through, and for each TABLE2_ID found the appropriate update is performed.
Documentation here.
You have to define a before trigger for delete.Try using two triggers
CREATE OR REPLACE TRIGGER INS_TABLE1_NUM_TRG
AFTER INSERT ON table1
FOR EACH ROW
BEGIN
UPDATE table2
SET table2num = :new.num
WHERE table2.id = :new.table2_id;
END INS_TABLE1_NUM_TRG;
CREATE OR REPLACE TRIGGER DEL_TABLE1_NUM_TRG
BEFORE DELETE ON table1
FOR EACH ROW
BEGIN
UPDATE table2
SET table2num = (SELECT num FROM
(SELECT num FROM table1 WHERE table2_id = :old.table2_id
ORDER BY modification_date DESC)
WHERE ROWNUM <= 1)
WHERE table2.id = :old.table2_id;
END DEL_TABLE1_NUM_TRG;
#psaraj12 answer is the best IMHO, but in the DELETE trigger I would use the :OLD notation as the inside query is unnecessary and will slow trigger significantly:
...
BEFORE DELETE ON table1
FOR EACH ROW
UPDATE table2
SET table2num = :OLD.num
WHERE table2.id = :OLD.table2_id;
...
I am trying to update multiple rows in one table, based on a select statement on another table.
This is my query:
UPDATE dbo.[user_message_content]
SET [status] = 1
WHERE [message_id] = (SELECT [message_id] FROM dbo.[user_message] WHERE [receiver_id] = #userID)
AND [status] = 0
This select statement may return multiple rows, which leads me to this error:
Msg 512, Level 16, State 1, Procedure usp_profileUserMessageMarkAsRead, Line 11
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
What is the correct way to achieve what I need?
Thanks very much
If you want to update all those records, change the = to IN:
UPDATE dbo.[user_message_content]
SET [status] = 1
WHERE [message_id] IN
( SELECT [message_id] FROM dbo.[user_message] WHERE [receiver_id] = #userID )
AND [status] = 0
You can also use UPDATE with FROM clause http://msdn.microsoft.com/en-us/library/aa260662(SQL.80).aspx.
USE tempdb;
GO
create table #user_message_content([status] int, message_id int)
create table #user_message (message_id int,receiver_id int)
declare #UserID int
UPDATE mc
SET mc.[status] = 1
FROM #user_message_content mc join #user_message m on mc.message_id = m.message_id
WHERE m.receiver_id = #userID
AND mc.[status]=0;
drop table #user_message_content
drop table #user_message
I think you need to use a join to do this
USE dbo; // Sets the current database to dbo, I did this for readability
UPDATE user_message_content join user_message on user_message_content.message_id = user_message.message_id
SET user_message_content.status = 1
WHERE user_message.receiver_id = #userID;
I write a insert trigger which work is: when a row inserted into the table then it update a particular field by a 20 char unique number which is generated using time and random number. It working fine when I insert a single row. But problem happens when I insert multiple rows using a single insert statement.
My trigger & insert query and error are below:
Trigger:
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
go
ALTER TRIGGER [unique_outmsg_id]
ON [dbo].[tbl_subscription]
FOR INSERT
AS
BEGIN
DECLARE #random_number varchar(6);
DECLARE #time_value varchar(23);
DECLARE #unique_id varchar(20);
SELECT #random_number = convert(varchar,(SELECT CAST(RAND() * 999 AS INT)));
SELECT #time_value = convert(varchar,(select replace(replace(replace(replace(convert(varchar(23), getdate(), 121),'-',''),'.',''),' ',''),':','')));
SELECT #unique_id=(SELECT(#time_value+#random_number));
UPDATE dbo.tbl_subscription SET outmsg_id=#unique_id WHERE outbox_id=(SELECT outbox_id FROM inserted)
END
Output of the trigger: 20110724093323697833
Multiple row insert query:
USE [test_abc]
INSERT INTO [test_abc].[dbo].[tbl_subscription] (inbox_id,inmsg_id,enabled)
SELECT s.inbox_id,s.enabled
FROM [test_def].[dbo].[tbl_subscriberlist] s,[test_def].[dbo].infoservice i
WHERE s.mo_key = 'ABC' AND i.subscribtionKey='ABC'
Functionality of this query:
To collect the information from another table and insert into tbl_subscription multiple rows.
But the error is when I run this query:
Msg 512, Level 16, State 1, Procedure unique_outmsg_id, Line 13
Subquery returned more than 1 value. This is not permitted when the
subquery follows =, !=, <, <= , >, >= or when the subquery is used as
an expression. The statement has been terminated.
So, why this error is happen when I am inserting multiple rows into the table?? Any ideas? Please help.
Triggers must be written to handle multiple rows.
The problem is in this update statement:
UPDATE dbo.tbl_subscription
SET outmsg_id=#unique_id
WHERE outbox_id = (SELECT outbox_id FROM inserted)
It expects the subselect (SELECT outbox_id FROM inserted) to return only a single row (which of course it won't when the trigger is fired for a multi-row batch).
You need to rewrite your UPDATE statement as a join to the inserted table
Multirow Considerations for DML Triggers
Implementing DML Triggers
This is returning all rows. You can only select 1 values into a field
WHERE outbox_id = (SELECT outbox_id FROM inserted)
Instead try (I can't test it though)
UPDATE s
SET outmsg_id=#unique_id
dbo.tbl_subscription s
JOIN inserted i
ON s.outbox_id=i.outbox_id
Furthermore this will always return the same number, which i imagine is not indended
SELECT #random_number = convert(varchar,(SELECT CAST(RAND() * 999 AS INT)));
Instead try:
SELECT #random_number = convert(varchar,(CAST(RAND(ABS(CHECKSUM(NEWID())))*999 AS INT) AS INT)));
(I recognize some of Mikael Eirikson's code in your statement)
I've done this before somewhere I'm sure of it!
I have a SQL Server 2000 table that I need to log changes to fields on updates and inserts into a second Logging table. A simplified version of the structure I'm using is below:
MainTable
ID varchar(10) PRIMARY KEY
DESCRIPTION varchar(50)
LogTable
OLDID varchar(10)
NEWID varchar(10)
For any other field something like this would work great:
Select i.DESCRIPTION As New, d.DESCRIPTION As Old
From Inserted i
LEFT JOIN Deleted d On i.ID=d.ID
...But obviously the join would fail if ID was changed.
I cannot modify the Tables in way, the only power I have in this database is to create a trigger.
Alternatively is there someone who can teach me time travelling and I'll go back into the past and ask myself back then how I did this? Cheers :)
Edit:
I think I need to clarify a few things here. This is not actually my database, it is a pre-existing system that I have almost no control of, other than writing this trigger.
My question is how can I retrieve the old primary key if said primary key was changed. I don't need to be told that I shouldn't change the primary key or about chasing up foreign keys etc. That's not my problem :)
DECLARE #OldKey int, #NewKey int;
SELECT #Oldkey = [ID] FROM DELETED;
SELECT #NewKey = [ID] FROM INSERTED;
This only works if you have a single row. Otherwise you have no "anchor" to link old and new rows. So check in your trigger for > 1 in INSERTED.
Is it possible to assume that the INSERTED and DELETED tables presented to you in a trigger are guaranteed to be in the same order?
I don't think it's possible. Imagine if you have 4 rows in the table:
1 Val1
2 Val2
3 Val3
4 Val4
Now issue the following update:
UPDATE MainTable SET
ID = CASE ID WHEN 1 THEN 2 WHEN 2 THEN 1 ELSE ID END
Description = CASE ID WHEN 3 THEN 'Val4' WHEN 4 THEN 'Val3' ELSE Description END
Now, how are you going to distinguish between what happened to rows 1 & 2 and what happened to rows 3 & 4. And more importantly, can you describe what's different between them? All of the stuff that tells you which columns have been updated won't help you.
If it's possible in this case that there's an additional key on the table (e.g. Description is UNIQUE), and your update rules allow it, you could write the trigger to prevent simultaneous updates to both keys, and then you can use whichever key hasn't been updated to correlate the two tables.
If you must handle multiple-row inserts/updates, and there's no alternate key that's guaranteed not to change, the only way I can see to do this is to use an INSTEAD OF trigger. For example, in the trigger you could break the original insert/update command into one command per row, grabbing each old id before you insert/update.
Within triggers in SQL Server you have access to two tables: deleted and inserted. Both of these have already been mentioned. Here's how they function depending on what action the trigger is firing on:
INSERT OPERATION
deleted - not used
inserted - contains the new rows being added to the table
DELETE OPERATION
deleted - contains the rows being removed from the table
inserted - not used
UPDATE OPERATION
deleted - contains the rows as they would exist before the UPDATE operation
inserted - contains the rows as they would exist after the UPDATE operation
These function in every way like tables. Therefore, it is entirely possible to use a row based operation such as something like the following (Operation exists only on the audit table, as does DateChanged):
INSERT INTO MyAuditTable
(ID, FirstColumn, SecondColumn, ThirdColumn, Operation, DateChanged)
VALUES
SELECT ID, FirstColumn, SecondColumn, ThirdColumn, 'Update-Before', GETDATE()
FROM deleted
UNION ALL
SELECT ID, FirstColumn, SecondColumn, ThirdColumn, 'Update-After', GETDATE()
FROM inserted
----new----
add an identity column to the table that the application can not change, you can then use that new column to join the inserted to the deleted tables within the trigger:
ALTER TABLE YourTableName ADD
PrivateID int NOT NULL IDENTITY (1, 1)
GO
----old----
Don't ever update/change key values. How can you do this and fix all of your foreign keys?
I wouldn't recommend ever using a trigger that can't handle a set of rows.
If you must change the key, insert a new row with the proper new key and values, use SCOPE_IDENTITY() if that is what your are doing. Delete the old row. Log for the old row that it was changed to the new row's key, which you should now have. I hope there is no foreign key on the changed key in your log...
You can create a new identity column on table MainTable (named for example correlationid) and correlate inserted and deleted tables using this column.
This new column should be transparent for existing code.
INSERT INTO LOG(OLDID, NEWID)
SELECT deleted.id AS OLDID, inserted.id AS NEWID
FROM inserted
INNER JOIN deleted
ON inserted.correlationid = deleted.correlationid
Pay attention, you could insert duplicate records in the log table.
Of course nobody should be changing the primary key on the table -- but that is exactly what triggers are supposed to be for (in part), is to keep people from doing things they shouldn't do. It's a trivial task in Oracle or MySQL to write a trigger that intercepts changes to primary keys and stops them, but not at all easy in SQL Server.
What you of course would love to be able to do would be to simply do something like this:
if exists
(
select *
from inserted changed
join deleted old
where changed.rowID = old.rowID
and changed.id != old.id
)
... [roll it all back]
Which is why people go out googling for the SQL Server equivalent of ROWID. Well, SQL Server doesn't have it; so you have to come up with another approach.
A fast, but sadly not bombproof, version is to write an instead of update trigger that looks to see whether any of the inserted rows have a primary key not found in the updated table or vice versa. This would catch MOST, but not all, of the errors:
if exists
(
select *
from inserted lost
left join updated match
on match.id = lost.id
where match.id is null
union
select *
from deleted new
left join inserted match
on match.id = new.id
where match.id is null
)
-- roll it all back
But this still doesn't catch an update like...
update myTable
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
Now, I've tried making the assumption that the inserted and deleted tables are ordered in such a way that cursoring through the inserted and deleted tables simultaneously will give you properly matching rows. And this APPEARS to work. In effect you turn the trigger into the equivalent of the for-each-row triggers available in Oracle and mandatory in MySQL...but I would imagine the performance will be bad on massive updates since this is not native behavior to SQL Server. Also it depends upon an assumption that I can't actually find documented anywhere and so am reluctant to depend on. But code structured that way APPEARS to work properly on my SQL Server 2008 R2 installation. The script at the end of this post highlights both the behavior of the fast-but-not-bombproof solution and the behavior of the second, pseudo-Oracle solution.
If anybody could point me to someplace where my assumption is documented and guaranteed by Microsoft I'd be a very grateful guy...
begin try
drop table kpTest;
end try
begin catch
end catch
go
create table kpTest( id int primary key, name nvarchar(10) )
go
begin try
drop trigger kpTest_ioU;
end try
begin catch
end catch
go
create trigger kpTest_ioU on kpTest
instead of update
as
begin
if exists
(
select *
from inserted lost
left join deleted match
on match.id = lost.id
where match.id is null
union
select *
from deleted new
left join inserted match
on match.id = new.id
where match.id is null
)
raisError( 'Changed primary key', 16, 1 )
else
update kpTest
set name = i.name
from kpTest
join inserted i
on i.id = kpTest.id
;
end
go
insert into kpTest( id, name ) values( 0, 'zero' );
insert into kpTest( id, name ) values( 1, 'one' );
insert into kpTest( id, name ) values( 2, 'two' );
insert into kpTest( id, name ) values( 3, 'three' );
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- This throws an error, appropriately
update kpTest set id = 5, name = 'FIVE' where id = 1
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- This allows the change, inappropriately
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
, name = UPPER( name )
go
select * from kpTest
/*
0 ZERO
1 TWO -- WRONG WRONG WRONG
2 ONE -- WRONG WRONG WRONG
3 THREE
*/
-- Put it back
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
, name = LOWER( name )
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
drop trigger kpTest_ioU
go
create trigger kpTest_ioU on kpTest
instead of update
as
begin
declare newIDs cursor for select id, name from inserted;
declare oldIDs cursor for select id from deleted;
declare #thisOldID int;
declare #thisNewID int;
declare #thisNewName nvarchar(10);
declare #errorFound int;
set #errorFound = 0;
open newIDs;
open oldIDs;
fetch newIDs into #thisNewID, #thisNewName;
fetch oldIDs into #thisOldID;
while ##FETCH_STATUS = 0 and #errorFound = 0
begin
if #thisNewID != #thisOldID
begin
set #errorFound = 1;
close newIDs;
deallocate newIDs;
close oldIDs;
deallocate oldIDs;
raisError( 'Primary key changed', 16, 1 );
end
else
begin
update kpTest
set name = #thisNewName
where id = #thisNewID
;
fetch newIDs into #thisNewID, #thisNewName;
fetch oldIDs into #thisOldID;
end
end;
if #errorFound = 0
begin
close newIDs;
deallocate newIDs;
close oldIDs;
deallocate oldIDs;
end
end
go
-- Succeeds, appropriately
update kpTest
set name = UPPER( name )
go
select * from kpTest;
/*
0 ZERO
1 ONE
2 TWO
3 THREE
*/
-- Succeeds, appropriately
update kpTest
set name = LOWER( name )
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Fails, appropriately
update kpTest
set id = case
when id = 1 then 2
when id = 2 then 1
else id
end
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Fails, appropriately
update kpTest
set id = id + 1
go
select * from kpTest;
/*
0 zero
1 one
2 two
3 three
*/
-- Succeeds, appropriately
update kpTest
set id = id, name = UPPER( name )
go
select * from kpTest;
/*
0 ZERO
1 ONE
2 TWO
3 THREE
*/
drop table kpTest
go