How do I count the number of distinct rows minus a join over those same distinct rows?
I'm writing after triggers where I need to raise an error if the user does not have rights to the rows submitted. I can do this in two statements but this seems inefficient.
DECLARE #AccessibleCount INT =
(
SELECT
COUNT(DISTINCT i.[ParentId])
FROM
inserted i
INNER JOIN [SuperSecret].[Parent] AS p ON
p.[Id] = i.[ParentId] AND
p.[LockedBy] = #UserId
);
DECLARE #ActualCount INT = (SELECT COUNT(DISTINCT [ParentId]) FROM inserted);
IF (#AccessibleCount <> #ActualCount)
BEGIN
RAISERROR(...);
ROLLBACK TRANSACTION;
END
For performance sake, it seems like I should use a subquery over the distinct inserted.ParentId for both counts. I tried the following but it resulted in "Invalid object name 'i'."
DECLARE #ActualMinusAccessible INT =
(
SELECT
COUNT(*)
-
(
SELECT
COUNT(*)
FROM
i
INNER JOIN [SuperSecret].[Parent] AS p ON
p.[Id] = i.[ParentId] AND
p.[LockedBy] = #UserId
)
FROM
(
SELECT DISTINCT [ParentId] FROM inserted
) AS i
);
IF (#ActualMinusAccessible <> 0)
BEGIN
RAISERROR (...);
ROLLBACK TRANSACTION;
END
If am not wrong you want to Raise Error if a [ParentId] is inserted which is not present in [SuperSecret].[Parent] table. Try changing your SQL query like this.
IF EXISTS (SELECT 1
FROM inserted i
WHERE NOT EXISTS (SELECT 1
FROM [SuperSecret].[Parent] a
WHERE i.[ParentId] = a.[ParentId] AND a.[LockedBy] = #UserId))
BEGIN
RAISERROR (...);
ROLLBACK TRANSACTION;
END
OR
IF (SELECT Count(DISTINCT [ParentId]) - (SELECT Count(DISTINCT i.[ParentId])
FROM inserted i
INNER JOIN [SuperSecret].[Parent] AS p
ON p.[Id] = i.[ParentId]
AND p.[LockedBy] = #UserId)
FROM inserted) <> 0
BEGIN
RAISERROR (...);
ROLLBACK TRANSACTION;
END
Related
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
I have a table Addresses with columns ID (PK), EmpID (int), Address (nvarchar(100), IsDefault (bit).
Now I have to ensure that only one record with same EmpID have IsDefault set to 1.
I have done for now following with triggers.
First one is an insert trigger. First it's checks if there is entered value 1 in IsDefault. If yes, then it checks if there are more then one record with same EmpID and IsDefault value set on 1. If that is also true, then it sets all other IsDefault values for the same EmpID to 0. :
create trigger [dbo].[TRG_dbo_Addresses_IsDefault_OnlyOneRecord_insert]
on [dbo].[lAddressesOrganisations]
after insert
as
begin
set nocount on;
begin try
if exists (
select *
from inserted as i
where i.IsDefault = 1)
begin
if (
select count(*)
from dbo.Addresses as lao
inner join inserted as i on i.ID=lao.ID
where lao.IsDefault = 1
and lao.EmpID = i.EmpID
) > 1
begin
update lao
set lao.IsDefault = 0
from dbo.Addresses as lao
where (
select row_number () over (partition by EmpID order by ID desc) as rn
from dbo.Addresses as lao
) > 1
end
end
end try
begin catch
if ##trancount > 0
rollback tran;
end catch
end
This other one is update trigger that I don't have a clue how to write it. First it does the same as insert trigger, checking the inserted value is 1 where value was 0. If yes, if there is more then one record with same EmpID and IsDefault set to 1. If yes,
how to write that all other records are set to 0 for same EmpID, while the one that is being updated remain 1?
create trigger [dbo].[TRG_dbo_Addresses_IsDefault_OnlyOneRecord_update]
on [dbo].[Addresses]
after update
as
begin
set nocount on;
begin try
if exists (
select *
from inserted as i
inner join deleted as d on d.ID=i.ID
where i.IsDefault = 1
and d.IsDefault = 0)
begin
if (
select count(*)
from dbo.Addresses as lao
inner join inserted as i on i.ID=lao.ID
where lao.IsDefault = 1
and lao.EmpID = i.EmpID
) > 1
begin
update lao
set lao.IsDefault = 0
from dbo.Addresses as lao
inner join inserted as i on i.ID=lao.ID
where (I don't have an idea what to put here)
and lao.OrganisationID = i.OrganisationID
end
end
end try
begin catch
if ##trancount > 0
rollback tran;
end catch
end
I'm working on ms sql 2016.
I agree with Matt: if possible, avoid trigger.
Anyway, I think in your UPDATE trigger you should change to:
if (
select count(*)
from dbo.Addresses as lao
inner join inserted as i on lao.EmpID = i.EmpID
where lao.IsDefault = 1
) > 1
begin
update lao
set lao.IsDefault = 0
from dbo.Addresses as lao
inner join inserted as i on lao.EmpID = i.EmpID
where lao.IsDefault = 1
and lao.ID <> i.id
end
Morevore, you can rewrite it to:
IF EXISTS(SELECT 1 FROM
FROM dbo.Addresses as lao
INNER join inserted as i on lao.EmpID = i.EmpID
WHERE lao.IsDefault = 1 AND lao.ID <>i.ID)
BEGIN
UPDATE lao
SET lao.IsDefault = 0
FROM dbo.Addresses as lao
INNER JOIN inserted as i on lao.EmpID = i.EmpID
WHERE lao.IsDefault = 1
and lao.ID <> i.id
END
You should change your insert trigger too.
Updated: insert trigger.
As far I can see (but I can't do tests, so please do complete case tests), and if ID is always max value or if you want to preserve default of last id inserted, I think you could rewrite your insert trigger as following: (you could remove IF too, if you don't care about doing always an UPDATE for zero rows too)
as
begin
set nocount on;
begin try
IF EXISTS(SELECT 1
FROM dbo.Addresses as lao
INNER join inserted as i on lao.EmpID = i.EmpID
WHERE lao.IsDefault = 1
AND lao.ID <>i.ID
AND i.IsDefault=1)
BEGIN
UPDATE lao
SET lao.IsDefault = 0
FROM dbo.Addresses as lao
INNER JOIN inserted as i on lao.EmpID = i.EmpID
WHERE lao.IsDefault = 1
and lao.ID <> i.id
AND i.IsDefault=1
END
end try
begin catch
if ##trancount > 0
rollback tran;
end catch
This change (AND i.IsDefault=1) could be applied to UPDATE trigger too.
I might consider whether the problem that you are trying to solve here could be solved without a trigger. Triggers tend to carry significant performance penalties and can separate logic from the rest of your code, making it a headache to maintain in the future.
If you really want to go with a trigger solution on row insert/update and there is always exactly 1 row marked with the bit, don't try to calculate counts of rows with it. Simply set all rows to 0 for the relevant EmpID and then update the inserted row to 1 after that in the trigger.
i have two tables
PaymentData
Ser Customerid Totalpaid
1. AGP001 2400
2. AGP002 1000
3. AGP003 1500
Receipt
Receipt# Customerid Paid
1. AGP001 1200
2. AGP001 1200
I want to create a trigger on Receipt table, and trigger will fire on insert, update, and delete operations which updates the totalpaid field of PaymentData table. Everytime a new Receipt record is inserted or updated against some customerid, totalpaid field for that customer will updated as well.
Trigger should do following.
Update PaymentData.totalpaid = sum(Recipt.paid)
where Receipt.customerID = PaymentData.customerID
I guess you need some trigger like:
CREATE TRIGGER dbo.OnReceiptUpdate
ON dbo.Receipt
AFTER INSERT,DELETE,UPDATE --operations you want trigger to fire on
AS
BEGIN
SET NOCOUNT ON;
DECLARE #customer_id VARCHAR(10)
SET #customer_id= COALESCE
(
(SELECT customer_id FROM inserted), --table inserted contains inserted rows (or new updated rows)
(SELECT customer_id FROM deleted) --table deleted contains deleted rows
)
DECLARE #total_paid DECIMAL
SET #total_paid =
(
SELECT SUM(paid)
FROM Receipt
WHERE customer_id = #customer_id
)
UPDATE PaymentData
SET total_paid = #total_paid
WHERE customer_id = #customer_id
IF ##ROWCOUNT = 0 --if nothing was updated - you don't have record in PaymentData, so make it
INSERT INTO PaymentData (customer_id, total_paid)
VALUES (#customer_id, #total_paid)
END
GO
Keep in mind - it aint gonna work with multiply updates/deletes/inserts - This is just example of how you need to do it
Try this trigger with multiply updates, inserts or deletes.
CREATE TRIGGER [dbo].upd_PaymentData ON dbo.Receipt
FOR INSERT, UPDATE, DELETE
AS
IF ##ROWCOUNT = 0 return
SET NOCOUNT ON;
DECLARE #actionTable nvarchar( 10),
#insCount int = (SELECT COUNT(*) FROM inserted),
#delCount int = (SELECT COUNT(*) FROM deleted)
SELECT #actionTable = CASE WHEN #insCount > #delCount THEN 'inserted'
WHEN #insCount < #delCount THEN 'deleted' ELSE 'updated' END
IF #actionTable IN ('inserted', 'updated')
BEGIN
;WITH cte AS
(
SELECT r.Customerid, SUM(r.Paid) AS NewTotalPaid
FROM dbo.Receipt r
WHERE r.Customerid IN (SELECT i.Customerid FROM inserted i)
GROUP BY r.Customerid
)
UPDATE p
SET p.Totalpaid = c.NewTotalPaid
FROM dbo.PaymentData p JOIN cte c ON p.Customerid = c.Customerid
END
ELSE
BEGIN
;WITH cte AS
(
SELECT d.Customerid, SUM(ISNULL(r.Paid, 0)) AS NewTotalPaid
FROM deleted d LEFT JOIN dbo.Receipt r ON d.Customerid = r.Customerid
GROUP BY d.Customerid
)
UPDATE p
SET p.Totalpaid = c.NewTotalPaid
FROM dbo.PaymentData p JOIN cte c ON p.Customerid = c.Customerid
END
CREATE TRIGGER [dbo].upd_PaymentData ON dbo.Receipt
FOR INSERT, UPDATE, DELETE
AS
IF ##ROWCOUNT = 0 return
SET NOCOUNT ON;
DECLARE #actionTable nvarchar( 10),
#insCount int = (SELECT COUNT(*) FROM inserted),
#delCount int = (SELECT COUNT(*) FROM deleted)
SELECT #actionTable = CASE WHEN #insCount > #delCount THEN 'inserted'
WHEN #insCount < #delCount THEN 'deleted' ELSE 'updated' END
IF #actionTable IN ('inserted', 'updated')
BEGIN
;WITH cte AS
(
SELECT r.Customerid, SUM(r.Paid) AS NewTotalPaid,<strike> r.paymentDate</strike>
FROM dbo.Receipt r
WHERE r.Customerid IN (SELECT i.Customerid FROM inserted i)
GROUP BY r.Customerid
)
UPDATE p
SET p.Totalpaid = c.NewTotalPaid
<strike>SET p.lastpaymentDate = c.paymentDate</strike>
FROM dbo.PaymentData p JOIN cte c ON p.Customerid = c.Customerid
END
ELSE
BEGIN
;WITH cte AS
(
SELECT d.Customerid, SUM(ISNULL(r.Paid, 0)) AS NewTotalPaid
FROM deleted d LEFT JOIN dbo.Receipt r ON d.Customerid = r.Customerid
GROUP BY d.Customerid
)
UPDATE p
SET p.Totalpaid = c.NewTotalPaid
FROM dbo.PaymentData p JOIN cte c ON p.Customerid = c.Customerid
END
I tried to add more functionality as indicated inside tage but it gives error and is not working.
Simply added one field in both tables.
In paymentdata added lastpaymentdate,
And in receipt added paymentdate.
On inserting or updating receipt table, paymentdata.lastpaymentdate should also update to receipt.paymentdate.
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.
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.