SQL: Statement Triggers VS For Each Row - sql

EDIT: I don't know what distro it is, it's in an exam paper.
I'm just not getting this, sadly. I'm quite happy with Row level triggers but could someone explain to me how the results would differ if the trigger was statement level instead?
Relation/Statement Trigger/Row Level Trigger
Employee(ID VARCHAR2(30), Salary NUMBER)
Create Trigger AutoRaise
After insert on Employee
Referencing new table as NT
update Employee
Set salary = salary + (select avg(salary) from NT)
Create trigger AutoRaise
After insert on Employee
Referencing new table as NT
For each Row
Update employee
Set salary = salary + (select avg(salary) from NT)
I understand that in the for each row trigger it'll fire for each row affected by the triggering statement. Now would the statement level trigger modify the results differently? Say if I inserted five tuples in one statement, would it set the salary etc for them all? If so, what's the benefit of the row level trigger here?
I've tried searching but I just can't get my head around it.
Thanks,
EDIT: Now, I'm just being dense but would either trigger produce different outputs? For the statement level trigger if I used the example values:
In table before trigger's creation:
(A,50)
Added in ONE statement after trigger is created:
(B,70), (C,30)
The first trigger would set the salary for each tuple being inserted, surely? So the first would become 120 (as the average is 50, 70 + 50 = 120) and the second would become 80. If this is true, how does the second trigger differ in results?

The difference is that in case of statement level trigger SELECT avg(salary) FROM NT will return average of salary for inserted rows, but in case of row level, avg(salary) always equals to salary of new record (trigger executed for each row individually). Also, statement level trigger will be executed if no records affected. In case of row level trigger most RDMS don't fire it when 0 records affected.
Side note. I believe the trigger bodies in the question are given for example only; otherwise, I'd recommend not using recursion in triggers even if particular RDMS has such an option.

Related

How to create a sql trigger to set a table column equal to a value after insert?

new to oracle and sql but trying to learn triggers. I think I'm having some syntax errors here, but let me explain what I am trying to do.
I have two tables: 1. group_membership with the columns
user_internal_id | group_internal_id (FK) | joined_time
and 2. group_details with the columns
group_internal_id (PK) | group_name | group_owner | created_time | movie_cnt | member_cnt|
(PK and FK stand for Primary Key and Foreign Key that relates to that Primary Key respectively.)
What I want to do:
After a new row is inserted into the group_membership table, I want to
update the value of member_cnt in the group_details table with the amount of times a particular group_internal_id appears in the group_membership table.
--
Now, my DBA for the app we are working on has created a trigger that simply updates the member_cnt of a particular group by reading the group_internal_id of the row inserted to group_membership, then adding 1 to the member_cnt. Which works better probably, but I want to figure out how come my trigger is having errors. Here is the code below
CREATE OR REPLACE TRIGGER set_group_size
AFTER INSERT ON group_membership
FOR EACH ROW
DECLARE g_count NUMBER;
BEGIN
SELECT COUNT(group_internal_id)
INTO g_count
FROM group_membership
GROUP BY group_internal_id;
UPDATE group_details
SET member_cnt = g_count
WHERE group_details.group_internal_id = group_membership.group_internal_id;
END;
The errors I'm receiving are:
Error(7,5): PL/SQL: SQL Statement ignored
Error(9,45): PL/SQL: ORA-00904: "GROUP_MEMBERSHIP"."GROUP_INTERNAL_ID": invalid identifier
I came here because my efforts have bene futile in troubleshooting. Hope to hear some feedback. Thanks!
The immeidate issue with your code is the update query of your trigger:
UPDATE group_details
SET member_cnt = g_count
WHERE group_details.group_internal_id = group_membership.group_internal_id;
group_membership is not defined in that scope. To refer to the value on the rows that is being inserted, use pseudo-table :new instead.
WHERE group_details.group_internal_id = :new.group_internal_id;
Another problem is the select query, that might return multiple rows. It would need a where clause that filters on the newly inserted group_internal_id:
SELECT COUNT(*)
INTO g_count
FROM group_membership
WHERE group_internal_id = :new.group_internal_id;
But these obvious fixes are not sufficient. Oracle won't let you select from the table that the trigger fired upon. On execution, you would meet error:
ORA-04091: table GROUP_MEMBERSHIP is mutating, trigger/function may not see it
There is no easy way around this. Let me suggest that this whole design is broken; the count of members per group is derived information, that can easily be computed on the fly whenever needed. Instead of trying to store it, you could, for example, use a view:
create view view_group_details as
select group_internal_id, group_name,
(
select count(*)
from group_membership gm
where gm.group_internal_id = gd.group_internal_id
) member_cnt
from group_details gd
Agree with #GMB that your design is fundamentally flawed, but if you insist on keeping a running count there is a easy solution to mutating they point out. The entire process is predicated on maintaining the count in group_details.member_count column. Therefore since that column has the previous count you do not need to count them - so eliminate the select. Your trigger becomes:
create or replace trigger set_group_size
after insert on group_membership
for each row
begin
update group_details
set member_cnt = member_cnt + 1
where group_details.group_internal_id = :new.group_internal_id;
end;
Of course then you need to handle group_membership Deletes and Updates of group_internal_id. Also, what happens when 2 users process the same group_membership simultaneously? Maintaining a running total for a derivable column is just not worth the effort. Best option just create the view as GMB suggested.

Sql trigger for update in specific column after insert

I made a trigger that is supposed to update another value in the same table a after I make an insert to the table. I do get the result I am looking for, but when I ask my teacher if it is correct he responded that this trigger updates "all" tables(?) and thus incorrect. He would not explain more than that (he is that kind of teacher...). Can anyone understand what he means? Not looking for the right code, just an explanation of what I might have misunderstood.
CREATE TRIGGER setDate
ON Loans
AFTER INSERT
AS
BEGIN
UPDATE Loans
set date = GETDATE()
END;
Your teacher intends to say that the query updates all rows in the table -- perhaps you misunderstood her or him.
The best way to do what you want is to use a default value:
alter table loans alter column date datetime default getdate();
That is, a trigger is not needed. If you did use a trigger, I'll give you two hints:
An instead of trigger.
inserted should be somewhere in the trigger.
Hello there. I upvoted your question because that might be the question of many beginners in SQL Server.
As I see you defined the trigger right! It is a correct way although it's not the best.
By the way we are not discussing all ways you could choose or not, I'm gonna correct your code and explain what your teacher meant.
Look at this UPDATE you wrote:
UPDATE Loans
SET date = GETDATE()
If you write a SELECT without a WHERE clause, what does it do? It would result all the rows in the table which was selected. right?
SELECT * FROM dbo.loans
So without a WHERE clause, your UPDATE will update all of the rows in the table.
OK now what to do to just update the only row ( or rows) which were recently inserted?
When you are writing a trigger, you are allowed to use this table: Inserted
which it has the rows were recently inserted. kinda these rows first come to this table and then go to the final destination table. So you can update them before they go the dbo.loans
it would be like this:
Update dbo.loans
SET date = GETDATE()
WHERE Id IN (SELECT * FROM Inserted)

How do I write a trigger that limits that a person from the bankcustomer table can only have 3 accounts in the account table?

I have a bankcustomer table which looks like this:
create table bankcustomer
(
cpr char(10) primary key,
name varchar(30) not null
)
And an account table which looks like this:
create table account
(
accountnr int identity(1001,1) primary key,
accountowner char(10) foreign key references bankcustomer,
created date not null,
balance decimal(14,2) not null
)
I want to write a trigger in SQL Server that limits a bankcustomer so that a bankcustomer can have no more than 3 accounts in the account table.
I create an account for a specific bank customer by inserting a value in the accountowner column in the account table that matches a cpr from the bankcustomer table (when inserting a record into the account table).
I have this code so far:
create trigger mytrigger4
on account
for insert
as
if exists (select count(*)
from inserted
join bankcustomer on inserted.accountowner = bankcustomer.cpr
where cpr = inserted.accountowner
having count(*) > 3)
begin
rollback tran
raiserror('A customer must have a maximum of 3 accounts', 16, 1)
end
go
The problem is that I can keep creating accounts (insert records in the account table) for a customer even though the customer already has 3 accounts. Which means the code in the trigger does not work at all.
Any help would be appreciated!
Let's think about your code. First, it is apparent that you test using single row inserts only. And that probably carries over into your sql code generally. That's bad, because an insert (or update or delete or merge) can affect any number of rows. While you can expect that the majority of inserts from an application are likely to be single rows, there are always situations that affect multiple rows. And that is an assumption that should always be in your mind when writing sql code generally - and triggers specifically.
Your test is based on exists. That is testing for the existence of rows generate by the query inside the exists clause. So - look carefully at your query. First, you count but do not group. Therefore, you are counting all the rows generated by the query. This is incorrect because of the assumption mentioned earlier. But let's ignore that for the moment and examine the select statement alone.
Your select statement joins inserted to the parent bankcustomer. The join is correct but why is there a where clause? And let's sidetrack into best practices. Always - ALWAYS - give each table a useful alias and use that alias when referencing columns. Why? Because this makes it easier for others to read and understand your query. BTW - a useful alias is not a single character. Yep - writing code can be a little work.
Let's continue. Your query counts all rows in the resultset. If you insert a single row, what is the result of your join? We know that an account is associated with a single bankcustomer. So when you insert a single row into account, the join will produce A SINGLE ROW. And counting that single row resultset will always produce a single row with the value of - tadah - 1. Now there is a way to cause your trigger generate an error. Insert 4 or more rows with a single statement. The error will probably not be accurate, but it will kill the transaction and display a message.
So you see that your logic is flawed. You need to count rows in the actual table (account), not inserted. But not all the rows - because that would be inefficient. You just need to consider all rows that "share" the accountowner values found in inserted. Note the plural "values". This is where your single row assumption fails. So, how to do that? Here is one way to write your trigger. Note that the first query is included to let you "see" what the count query is producing - this is for debugging only. Production triggers should never return a resultset in any fashion.
alter trigger mytrigger4
on account
for insert
as begin
select cust.cpr, count(*)
from bankcustomer as cust join account as acc on cust.cpr = acc.accountowner
where exists (select * from inserted as ins where ins.accountowner = cust.cpr)
group by cust.cpr;
if exists (select cust.cpr, count(*)
from bankcustomer as cust join account as acc on cust.cpr = acc.accountowner
where exists (select * from inserted as ins where ins.accountowner = cust.cpr)
group by cust.cpr
having count(*) > 3)
begin
rollback tran
raiserror('A customer must have a maximum of 3 accounts', 16, 1)
end
end;
go
I'll leave it to you to actually test is thoroughly - which includes the use of insert statements that insert multiple rows. And, of course, you will vary the test data to include customers that have no accounts, less than 3 accounts, exactly 3 accounts, and more than 3 accounts (because sometimes things happen and extra accounts get added despite your best efforts).

Simple trigger on salary

I am new to sql and i have an employee table. I want to create a trigger so that no one can insert a new employee record with salary less than 200. I tried this code but it doesn't accept any record anymore and i see this error "The transaction ended in the trigger. The batch has been aborted." whatever the salary is.
This is my code
Create trigger Trig on Employees after insert
As
Begin
if((Select count(EID) from inserted where salary<200)>0)
Rollback
else
Commit
End
Thanks in advance
For your scenario trigger is not a good solution. try to add check constraint on Salary column. I think Check constraint is a good solution for your problem
ALTER TABLE Employees
ADD CONSTRAINT chk_Salary CHECK (Salary>=200)
https://msdn.microsoft.com/en-us/library/ms187550.aspx
When using rollback, it will treat the entire transaction as one, so if one of the inserts within the trigger meets the condition, it will unfortunately rollback all of the inserts within the transaction.
So, basically if you want to have it work this way, you won't be able to use rollback as an option or it will delete all of the other salaries from this transaction.
Here's the workaround, when one of the items meets the condition that you don't want after the insert, you will need to do something like this:
BEGIN
IF ...
DELETE FROM Table WHERE …
END

T-SQL Trigger After Specific Insertion

I need to write a trigger that only runs when an insertion that has a certain field with a specific value is inserted. There is no updating.
For example, if I insert an entry that has column X with the value "MAC ID", I want it to run a trigger, but ONLY if the value is "MAC ID".
I know how to test for this normally, but not sure how to test this in a trigger. So far I've found that I want to use "FOR INSERT", but beyond that I don't know how to implement it.
Any help is much appreciated! Thanks!
create trigger MaxIdInsert
on YourTable
after insert
as
if exists
(
select *
from inserted
where ColumnX = 'MAX ID'
)
begin
-- do what you want here if ColumnX has value of 'MAX ID'
end
go
There's no way to only fire a trigger on certain DML specifications (besides insert, update, and/or delete). Your best bet is to test out the dynamic inserted table, which contains records that are inserted into YourTable. In this, you can test for inserted records that have a ColumnX value of "MAX ID".
Edit: In case you were wondering, I know you specified a for trigger in your question. A for trigger is equivalent to an after trigger in SQL Server.
You need to be aware that triggers run once for a batch, not once per row. As such, you may be running in a circumstance in which some of the rows match your criteria, and others do not.
As such, you'd do best to write your trigger logic to select the matching rows from inserted. If there were no matching rows, the rest of your logic will be working with an empty set, but that's no real problem - SQL Server is plenty fast enough at doing nothing, when required.
E.g. if you're inserting these rows into an audit table, something like:
create trigger MacId
on T
for insert
as
insert into Audit(Col1,Col2,Col3)
select i.Col1,i.Col2,'inserted'
from inserted i
where i.Col4 = 'MAC ID'