So I understand recursive triggers. Got to be careful of deadlocks etc. However this is only after an insert not after insert and update. Also, I have an audit trigger table that I am updating to make sure all is well. And querying after to double check. All looks fine but no update happens.
if exists (select 'a' from sys.triggers where name = 'invoicememologic')
begin
drop trigger invoicememologic
end
go
create trigger invoicememologic
on transactiontable
after insert
as
begin
declare #inum varchar(1000)
select #inum = min(transactioninvnum)
from
(select transactioninvnum
from inserted i
inner join project p on left(i.projectid, charindex(':', i.projectid)) = p.projectid
where right(i.projectid, 1) <> ':'
and abs(p.UseProjectMemoOnInv) = 1
group by transactioninvnum) b
while #inum is not null
begin
declare #rCount int
select #rCount = count(*)
from transactiontable
where TransactionInvNum = #inum
if #rCount = 1
begin
declare #tid varchar(100)
select #tid = transactionid
from transactiontable
where TransactionInvNum = #inum
declare #pmemo varchar(MAX)
select #pmemo = p.projectMemo
from transactiontable tt
inner join project p on left(tt.projectid, charindex(':', tt.projectid)) = p.projectid
where transactionInvNum = #inum
insert into audittrigger
values (#pmemo, #tid)
update transactiontable
set transactionmemo2 = #pmemo
where ltrim(rtrim(transactionid)) = ltrim(rtrim(#tid))
end
select #inum = min(transactioninvnum)
from
(select transactioninvnum
from inserted i
inner join project p on left(i.projectid, charindex(':', i.projectid)) = p.projectid
where abs(transactionjointinv) = 1
and right(i.projectid, 1) <> ':'
and abs(p.UseProjectMemoOnInv) = 1
group by transactioninvnum ) a
where transactioninvnum > #inum
end
end
Reason for trigger. 1 Invoice can be multiple rows in the database. 3 rows. So it only should update any one of the 3 rows. Doesn't matter. And it must grab the memo from the parent project of the phases that are being inserted into the database. hence the inner join on the left with charindex.
So I check the audit table. All looks well there. MEMO is there and the transactionid is there. I query after the trigger fires. TransactionID exists in the transactiontable but the memo2 is not being updated.
TransactionMemo2 is type of ntext. I thought it might be varchar with a direct update command will fail. I tried to update manually through setting a var as the text string and call the update manually with the transactionid being required. all worked fine. I am lost
Related
I'm trying to update some values based on every Id in the list. The logic I have seems to be what I want.
I want to populate a temporary table of Ids. Then for every ID I want to apply this query and output the deleted date and the ID into a new table I've created.
I keep getting the error:
Msg 10716, Level 15, State 1, Line 25
A nested INSERT, UPDATE, DELETE, or MERGE statement must have an OUTPUT clause.
What does this mean? I thought I am OUTPUTTING into the new table I've created.
USE datatemp
GO
DECLARE #idlist TABLE (id INT)
INSERT INTO #idlist (id) VALUES (3009099)
DECLARE #EndDate DATETIME
SET #EndDate = '2099-12-12'
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'TEMP_TABLE')
BEGIN
CREATE TABLE [TEMP_TABLE] (
[id] INT,
[thedatetoend] DATETIME);
END
BEGIN TRY
SELECT *
FROM #idlist AS idlist
OUTER APPLY(
UPDATE [custprofile]
SET thedatetoend = #EndDate
OUTPUT idlist.id, DELETED.thedatetoend
INTO [TEMP_TABLE]
FROM [custprofile] as bc
INNER JOIN [custinformation] as cc
ON cc.custengageid = bc.custengageid
WHERE cc.id = idlist.id
AND bc.modifierid = 2
AND bc.thedatetoend > GETDATE()
AND cc.type = 1) o
I think you may have more success by using a CTE and avoiding the outer apply approach you are currently using. Updates made to the CTE cascade to the source table. It might look something like the following but as some columns don't reference the table aliases don't expect this to work "as is" (i.e. I'm not sure if you are outputting ccid or bcid and I don't know which table thedatetoend belongs to.)
WITH
CTE AS (
SELECT
cc.id AS ccid, bcid AS bcid, thedatetoend
FROM [custprofile] AS bc
INNER JOIN [custinformation] AS cc ON cc.custengageid = bc.custengageid
INNER JOIN #idlist AS idlist ON cc.id = idlist.id
WHERE bc.modifierid = 2
AND bc.thedatetoend > GETDATE()
AND cc.type = 1
)
UPDATE CTE
SET thedatetoend = #EndDate
OUTPUT ccid, DELETED.thedatetoend
INTO [TEMP_TABLE]
Is there a more efficient way to write this code? Or with less code?
SELECT *
INTO #Temp
FROM testtemplate
Declare #id INT
Declare #name VARCHAR(127)
WHILE (SELECT Count(*) FROM #Temp) > 0
BEGIN
SELECT TOP 1 #id = testtemplateid FROM #Temp
SELECT TOP 1 #name = name FROM #Temp
UPDATE testtemplate
SET testtemplate.vendortestcode = (SELECT test_code FROM test_code_lookup WHERE test_name = #name)
WHERE testtemplateid = #id
--finish processing
DELETE #Temp Where testtemplateid = #id
END
DROP TABLE #Temp
You can do this in a single UPDATE with no need to loop.
UPDATE tt
SET vendortestcode = tcl.test_code
FROM testtemplate tt
INNER JOIN test_code_lookup tcl
ON tt.name = tcl.test_name
You could try a single update like this:
UPDATE A
SET A.vendortestcode = B.test_code
FROM testtemplate A
INNER JOIN test_code_lookup B
ON A.name = B.test_name
Also, the way you are doing it now is wrong, since you are taking a TOP 1 Id and a TOP 1 name in two separate querys, without an ORDER BY, so its not sure that you are taking the right name for your ID.
You could write a function to update vendortestcode. Then your code reduces to one SQL statement:
update testtemplate set vendortestcode = dbo.get_test_code_from_name(name)
Can anyone explain why Sql Server is complaining about the syntax around the "WITH" clause?
Thanks for any help.
CREATE TABLE TestTable1 (
Id int not null,
Version int not null constraint d_Ver default (0),
[Name] nvarchar(50) not null,
CONSTRAINT pk_TestTable1 PRIMARY KEY (Id, Version)
);
GO
CREATE TRIGGER trg_iu_UniqueActiveName
ON [dbo].[TestTable1]
AFTER INSERT, UPDATE
AS
IF(UPDATE([Name]))
BEGIN
IF(
(
WITH MaxVers AS
(SELECT Id, Max(Version) AS MaxVersion
FROM [dbo].[TestTable1]
GROUP BY Id)
SELECT Count(1)
FROM [dbo].[TestTable1] t
INNER JOIN MaxVers ON t.Id = MaxVers.Id AND t.Version = MaxVers.MaxVersion
WHERE t.[Name] = inserted.[Name]
)
> 0
)
BEGIN
DECLARE #name nvarchar(50)
SELECT #name = [Name] FROM inserted;
RAISERROR('The name "%s" is already in use.', 16, 1, #name);
END
END;
GO
Edit 2:
For anyone who is curious, here is the CTE version that incorporates all of the great comments below. I think I will switch to the sub-query approach so that I can use the "EXISTS" as suggested.
CREATE TRIGGER trg_iu_UniqueActiveName
ON [dbo].[TestTable1]
AFTER INSERT, UPDATE
AS
IF(UPDATE([Name]))
BEGIN
DECLARE #cnt [int];
WITH MaxVers AS
(SELECT Id, Max(Version) AS MaxVersion
FROM [dbo].[TestTable1]
GROUP BY Id)
SELECT #cnt = COUNT(1)
FROM [dbo].[TestTable1] t
INNER JOIN MaxVers ON t.Id = MaxVers.Id AND t.Version = MaxVers.MaxVersion
INNER JOIN [inserted] i ON t.[Id] = MaxVers.[Id]
WHERE t.[Name] = i.[Name] AND NOT [t].[Id] = [i].[Id] ;
IF( #cnt > 0)
BEGIN
DECLARE #name nvarchar(50)
SELECT #name = [Name] FROM inserted;
RAISERROR('The name "%s" is already in use by an active entity.', 16, 1, #name);
ROLLBACK TRANSACTION;
END
END;
GO
Edit 3: Here is the "Exists" version (Note, I think that the select in the error handling part would not work correctly with more than one inserted record):
CREATE TRIGGER trg_iu_UniqueActiveName
ON [dbo].[TestTable1]
AFTER INSERT, UPDATE
AS
IF(UPDATE([Name]))
BEGIN
IF(EXISTS (
SELECT t.Id
FROM [dbo].[TestTable1] t
INNER JOIN (
SELECT Id, Max(Version) AS MaxVersion
FROM [dbo].[TestTable1]
GROUP BY Id) maxVer
ON t.[Id] = [maxVer].[Id] AND [t].[Version] = [maxVer].[MaxVersion]
INNER JOIN [inserted] i ON t.[Id] = MaxVer.[Id]
WHERE [t].[Name] = [i].[Name] AND NOT [t].[Id] = [i].[Id]
))
BEGIN
DECLARE #name nvarchar(50)
SELECT #name = [Name] FROM inserted;
RAISERROR('The name "%s" is already in use by an active entity.', 16, 1, #name);
ROLLBACK TRANSACTION;
END
END;
GO
The only thing I can figure is that the statement "When a CTE is used in a statement that is part of a batch, the statement before it must be followed by a semicolon." (Transact SQL Reference) means that a CTE can not be used within an IF statement.
BTW, you have two other errors: 1) inserted pseudo table is not included in the first sub-query, even though you reference it in the were clause. 2) Your trigger is assuming a single row is being inserted or updated. It is possible that there would be multiple duplicate names but the raiserror will only report one of them.
EDIT And avoid (select count(*) ...) > when exists (select * ....) will do The exists can stop at the first row.
EDIT 2 Crap. SQL Server trigges default to after triggers. So the row you are checking for existence on already exists in the table when the trigger fire:
CREATE TRIGGER trg_iu_UniqueActiveName
ON [dbo].[TestTable1]
AFTER INSERT, UPDATE
AS
IF(UPDATE([Name]))
BEGIN
IF EXISTS
(
SELECT *
FROM [dbo].[TestTable1] t
INNER JOIN inserted i on i.[NAME] = t.[NAME]
INNER JOIN (SELECT Id, Max(Version) AS MaxVersion
FROM [dbo].[TestTable1]
GROUP BY Id) MaxVers ON t.Id = MaxVers.Id AND t.Version = MaxVers.MaxVersion
)
BEGIN
DECLARE #name nvarchar(50)
SELECT #name = [Name] FROM inserted;
RAISERROR('The name "%s" is already in use.', 16, 1, #name);
END
END;
GO
insert into testTable1 (name) values ('Hello')
results in:
Msg 50000, Level 16, State 1, Procedure trg_iu_UniqueActiveName, Line 20
The name "Hello" is already in use.
(1 row(s) affected)
Plus, the raiserror does not perform a rollback, so the row is still there.
I don't think that you can use CTEs with inner queries.
Use this as workaround:
DECLARE #cnt int;
WITH MaxVers AS
(SELECT Id, Max(Version) AS MaxVersion
FROM [dbo].[TestTable1]
GROUP BY Id)
SELECT #cnt = Count(1)
FROM [dbo].[TestTable1] t
INNER JOIN MaxVers ON t.Id = MaxVers.Id AND t.Version = MaxVers.MaxVersion
WHERE t.[Name] = inserted.[Name];
IF #cnt > 0
BEGIN
DECLARE #name nvarchar(50)
SELECT #name = [Name] FROM inserted;
RAISERROR('The name "%s" is already in use.', 16, 1, #name);
END
Doesn't appear to like the WITH statement inside an IF does it.
Try the following SQL instead:
SELECT COUNT(1)
FROM TestTable1 t1
WHERE t.Name = (SELECT [Name] FROM inserted)
AND t.Version = (SELECT MAX(Version) FROM TestTable1 t2 WHERE t2.Id = t.Id)
Much simpler in my opinion. This doesn't account for multiple rows in the inserted table however. Change it to an IN rather than an = would probably do that.
As others have noted sometimes putting a semi-colon in from of the WITH statement works, but I couldn't get it to in this instance.
I have a simple query for update table (30 columns and about 150 000 rows).
For example:
UPDATE tblSomeTable set F3 = #F3 where F1 = #F1
This query will affected about 2500 rows.
The tblSomeTable has a trigger:
ALTER TRIGGER [dbo].[trg_tblSomeTable]
ON [dbo].[tblSomeTable]
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
declare #operationType nvarchar(1)
declare #createDate datetime
declare #UpdatedColumnsMask varbinary(500) = COLUMNS_UPDATED()
-- detect operation type
if not exists(select top 1 * from inserted)
begin
-- delete
SET #operationType = 'D'
SELECT #createDate = dbo.uf_DateWithCompTimeZone(CompanyId) FROM deleted
end
else if not exists(select top 1 * from deleted)
begin
-- insert
SET #operationType = 'I'
SELECT #createDate = dbo..uf_DateWithCompTimeZone(CompanyId) FROM inserted
end
else
begin
-- update
SET #operationType = 'U'
SELECT #createDate = dbo..uf_DateWithCompTimeZone(CompanyId) FROM inserted
end
-- log data to tmp table
INSERT INTO tbl1
SELECT
#createDate,
#operationType,
#status,
#updatedColumnsMask,
d.F1,
i.F1,
d.F2,
i.F2,
d.F3,
i.F3,
d.F4,
i.F4,
d.F5,
i.F5,
...
FROM (Select 1 as temp) t
LEFT JOIN inserted i on 1=1
LEFT JOIN deleted d on 1=1
END
And if I execute the update query I have a timeout.
How can I optimize a logic to avoid timeout?
Thank you.
This query:
SELECT *
FROM (
SELECT 1 AS temp
) t
LEFT JOIN
INSERTED i
ON 1 = 1
LEFT JOIN
DELETED d
ON 1 = 1
will yield 2500 ^ 2 = 6250000 records from a cartesian product of INSERTED and DELETED (that is all possible combinations of all records in both tables), which will be inserted into tbl1.
Is that what you wanted to do?
Most probably, you want to join the tables on their PRIMARY KEY:
INSERT
INTO tbl1
SELECT #createDate,
#operationType,
#status,
#updatedColumnsMask,
d.F1,
i.F1,
d.F2,
i.F2,
d.F3,
i.F3,
d.F4,
i.F4,
d.F5,
i.F5,
...
FROM INSERTED i
FULL JOIN
DELETED d
ON i.id = d.id
This will treat update to the PK as deleting a record and inserting another, with a new PK.
Thanks Quassnoi, It's a good idea with "FULL JOIN". It is helped me.
Also I try to update table in portions (1000 items in one time) to make my code works faster because for some companyId I need to update more than 160 000 rows.
Instead of old code:
UPDATE tblSomeTable set someVal = #someVal where companyId = #companyId
I use below one:
declare #rc integer = 0
declare #parts integer = 0
declare #index integer = 0
declare #portionSize int = 1000
-- select Ids for update
declare #tempIds table (id int)
insert into #tempIds
select id from tblSomeTable where companyId = #companyId
-- calculate amount of iterations
set #rc=##rowcount
set #parts = #rc / #portionSize + 1
-- update table in portions
WHILE (#parts > #index)
begin
UPDATE TOP (#portionSize) t
SET someVal = #someVal
FROM tblSomeTable t
JOIN #tempIds t1 on t1.id = t.id
WHERE companyId = #companyId
delete top (#portionSize) from #tempIds
set #index += 1
end
What do you think about this? Does it make sense? If yes, how to choose correct portion size?
Or simple update also good solution? I just want to avoid locks in the future.
Thanks
Below is a SQL Server 2005 update trigger. For every update on the email_subscriberList table where the isActive flag changes we insert an audit record into the email_Events table. This works fine for single updates but for bulk updates only the last updated row is recorded. How do I convert the below code to perform an insert for every row updated?
CREATE TRIGGER [dbo].[Email_SubscriberList_UpdateEmailEventsForUpdate_TRG]
ON [dbo].[Email_subscriberList]
FOR UPDATE
AS
DECLARE #CustomerId int
DECLARE #internalId int
DECLARE #oldIsActive bit
DECLARE #newIsActive bit
DECLARE #email_address varchar(255)
DECLARE #mailinglist_name varchar(255)
DECLARE #email_event_type varchar(1)
SELECT #oldIsActive = isActive from Deleted
SELECT #newIsActive = isActive from Inserted
IF #oldIsActive <> #newIsActive
BEGIN
IF #newIsActive = 1
BEGIN
SELECT #email_event_type = 'S'
END
ELSE
BEGIN
SELECT #email_event_type = 'U'
END
SELECT #CustomerId = customerid from Inserted
SELECT #internalId = internalId from Inserted
SELECT #email_address = (select email from customer where customerid = #CustomerId)
SELECT #mailinglist_name = (select listDescription from Email_lists where internalId = #internalId)
INSERT INTO Email_Events
(mailshot_id, date, email_address, email_event_type, mailinglist_name)
VALUES
(#internalId, getDate(), #email_address, #email_event_type,#mailinglist_name)
END
example
untested
CREATE TRIGGER [dbo].[Email_SubscriberList_UpdateEmailEventsForUpdate_TRG]
ON [dbo].[Email_subscriberList]
FOR UPDATE
AS
INSERT INTO Email_Events
(mailshot_id, date, email_address, email_event_type, mailinglist_name)
SELECT i.internalId,getDate(),c.email,
case i.isActive when 1 then 'S' else 'U' end,e.listDescription
from Inserted i
join deleted d on i.customerid = d.customerid
and i.isActive <> d.isActive
join customer c on i.customerid = c.customerid
join Email_lists e on e.internalId = i.internalId
Left outer joins, for in case there are no related entries in customer or email_Lists (as is possible in the current code) -- make them inner joins if you know there will be data present (i.e. foreign keys are in place).
CREATE TRIGGER [dbo].[Email_SubscriberList_UpdateEmailEventsForUpdate_TRG]
ON [dbo].[Email_subscriberList]
FOR UPDATE
AS
INSERT INTO Email_Events
(mailshot_id, date, email_address, email_event_type, mailinglist_name)
select
i.InternalId
,getdate()
,cu.Email
,case i.IsaActive
when 1 then 'S'
else 'U'
end
,el.ListDescription
from inserted i
inner join deleted d
on i.CustomerId = d.CustomerId
and i.IsActive <> d.IsActive
left outer join Customer cu
on cu.CustomerId = i.CustomerId
left outer join Email_Lists el
on el.InternalId = i.InternalId
Test it well, especially for concurrency issues. Those joins within the trigger make me nervous.