How to write triggers to enforce business rules? - oracle

I want to create triggers for practicing PL/SQL and I sorta got stuck with these two ones, which I'm sure they're simple, but I can't get a hold of this code.
The first trigger forbids an employee to have a salary higher than the 80% of their boss (The code is incomplete because I don't know how to continue):
CREATE OR REPLACE TRIGGER MAX_SALARY
BEFORE INSERT ON EMP
FOR EACH ROW
P.BOSS EMP.JOB%TYPE := 'BOSS'
P.SALARY EMP.SAL%TYPE
BEGIN
SELECT SAL FROM EMP
WHERE
JOB != P.BOSS
...
And the second one, there must not be less than two employees per department
CREATE TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE EMPNO
EMPLOYEES NUMBER(2,0);
BEGIN
SELECT COUNT(EMPNO)INTO EMPLOYEES FROM EMP
WHERE DEPTNO = DEPT.DEPTNO;
IF EMPLOYEES < 2 THEN
DBMS_OUTPUT.PUT_LINE('There cannot be less than two employees per department');
END IF;
END;
I really don't know If I'm actually getting closer or far from it altogether...

which I'm sure they're simple
Actually these tasks are not simple for triggers. The business logic is simple, and the SQL to execute the business logic is simple, but implementing it in triggers is hard. To understand why you need to understand how triggers work.
Triggers fire as part of a transaction, which means they are are applied to the outcome of a SQL statement such as an insert or an update. There are two types of triggers, row level and statement level triggers.
Row-level triggers fire once for every row in the result set we can reference values in the current row, which is useful for evaluating row-level rules.. But we cannot execute DML against the owning table: Oracle hurls ORA-04088 mutating table exception, because such actions violate transactional integrity.
Statement level triggers fire exactly once per statement. Consequently they are useful for enforcing table-level rules but crucially they have no access to the result set, which means they don’t know which records have been affected by the DML.
Both your business rules are table level rules, as they require the evaluation of more than one EMP record. So, can we enforce them through triggers? Let’s start with the second rule:
there must not be less than two employees per department
We could implement this with an trigger AFTER statement trigger like this:
CREATE or replace TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select * from dept) loop
SELECT COUNT(EMPNO) INTO EMPLOYEES
FROM EMP
where i.DEPTNO = EMP.DEPTNO;
IF EMPLOYEES < 2 THEN
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
END IF;
end loop;
END;
/
Note this trigger uses RAISE_APPLICATION_ERROR() instead of DBMS_OUTPUT.PUT_LINE(). Raising an actual exception is always the best approach: messages can be ignored but exceptions must be handled.
The problem with this approach is that it will fail any update or delete of any employee, because the classic SCOTT.DEPT table has a record DEPTNO=40 which has no child records in EMP. So maybe we can be cool with departments which have zero employees but not with those which have just one?
CREATE or replace TRIGGER MIN_LIMIT
AFTER DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select deptno, count(*) as emp_cnt
from emp
group by deptno having count(*) < 2
) loop
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
end loop;
END;
/
This will enforce the rule. Unless of course somebody tries to insert one employee into department 40:
insert into emp
values( 2323, 'APC', ‘DEVELOPER', 7839, sysdate, 4200, null, 40 )
/
We can commit this. It will succeed because our trigger doesn’t fire on insert. But some other user’s update will subsequently fail. Which is obviously bobbins. So we need to include INSERT in the trigger actions.
CREATE or replace TRIGGER MIN_LIMIT
AFTER INSERT or DELETE OR UPDATE on EMP
declare
EMPLOYEES pls_integer;
BEGIN
for i in ( select deptno, count(*) as emp_cnt
from emp
group by deptno having count(*) < 2
) loop
raise_application_error(-20042, 'problem with dept #' || i.DEPTNO || '. There cannot be less than two employees per department');
end loop;
END;
/
Unfortunately now we cannot insert one employee in department 40:
ORA-20042: problem with dept #40. There cannot be less than two employees per department
ORA-06512: at "APC.MIN_LIMIT", line 10
ORA-06512: at "SYS.DBMS_SQL", line 1721
We need to insert two employees in a single statement:
insert into emp
select 2323, 'APC', 'DEVELOPER', 7839, sysdate, 4200, null, 40 from dual union all
select 2324, 'ANGEL', 'DEVELOPER', 7839, sysdate, 4200, null, 40 from dual
/
Note that switching existing employees to a new department has the same limitation: we have to update at least two employees in the same statement.
The other problem is that the trigger may perform badly, because we have to query the whole table after every statement. Perhaps we can do better? Yes. A compound trigger (Oracle 11g and later) allows us to track the affected records for use in a statement level AFTER trigger. Let’s see how we can use one to implement the first rule
No employee can have a salary higher than the 80% of their boss
Compound triggers are highly neat. They allow us to share program constructs across all the events of the trigger. This means we can store the values from row-level events in a collection, which we can use to drive some SQL in an statement level AFTER code..
So this trigger fires on three events. Before a SQL statement is processed we initialise a collection which uses the projection of the EMP table. The code before row stashes the pertinent values from the current row, if the employee has a manager. (Obviously the rule cannot apply to President King who has no boss). The after code loops through the stashed values, looks up the salary of the pertinent manager and evaluates the employee's new salary against their boss's salary.
CREATE OR REPLACE TRIGGER MAX_SALARY
FOR INSERT OR UPDATE ON EMP
COMPOUND TRIGGER
type emp_array is table of emp%rowtype index by simple_integer;
emps_nt emp_array ;
v_idx simple_integer := 0;
BEFORE STATEMENT IS
BEGIN
emps_nt := new emp_array();
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
v_idx := v_idx + 1;
if :new.mgr is not null then
emps_nt(v_idx).empno := :new.empno;
emps_nt(v_idx).mgr := :new.mgr;
emps_nt(v_idx).sal := :new.sal;
end if;
END BEFORE EACH ROW;
AFTER EACH ROW IS
BEGIN
null;
END AFTER EACH ROW;
AFTER STATEMENT IS
mgr_sal emp.sal%type;
BEGIN
for i in emps_nt.first() .. emps_nt.last() loop
select sal into mgr_sal
from emp
where emp.empno = emps_nt(i).mgr;
if emps_nt(i).sal > (mgr_sal * 0.8) then
raise_application_error(-20024, 'salary of empno ' || emps_nt(i).empno || ' is too high!');
end if;
end loop;
END AFTER STATEMENT;
END;
/
This code will check every employee if the update is universal, for instance when everybody gets a 20% pay rise...
update emp
set sal = sal * 1.2
/
But if we only update a subset of the EMP table it only checks the boss records it needs to:
update emp set sal = sal * 1.2
where deptno = 20
/
This makes it more efficient than the previous trigger. We could re-write trigger MIN_LIMIT as a compound trigger; that is left as an exercise for the reader :)
Likewise, each trigger fails as soon as a single violating row is found:
ORA-20024: salary of empno 7902 is too high!
ORA-06512: at "APC.MAX_SALARY", line 36
It would be possible to evaluate all affected rows, stash the violating row(s) in another collection then display all the rows in the collection. Another exercise for the reader.
Finally, note that having two triggers fire on the same event on the same table is not good practice. It's generally better (more efficient, easier to debug) to have one trigger which does everything.
An after thought. What happens to Rule #1 if one session increases the salary of an employee whilst simultaneously another session decreases the salary of the boss? The trigger will pass both updates but we can end up with a violation of the rule. This is an inevitable consequence of the way triggers work with Oracle's read-commit transaction consistency. There is no way to avoid it except by employing a pessimistic locking strategy and pre-emptively locking all the rows which might be affected by a change. That may not scale and is definitely hard to implement using pure SQL: it needs stored procedures. This is another reason why triggers are not good for enforcing business rules.
I'm using Oracle10g
That is unfortunate. Oracle 10g has been obsolete for almost a decade now. Even 11g is deprecated. However, if you really have no option but to stick with 10g you have a couple of options.
The first is to grind through the whole table, doing the lookups of each boss for every employee. This is just about bearable for a toy table such as EMP but likely to be a performance disaster in real life.
The better option is to fake compound triggers using the same workaround we all used to apply: write a package. We rely on global variables - collections - to maintain state across calls to packaged procedures, and have different triggers to make those calls. Basically you need one procedure call for each trigger and one trigger for each step in the compound trigger. #JustinCave posted an example of how to do this on another question; it should be simple to translate my code above to his template.

Please handle these kind of validations/business logic at application or at DB level using procedures/functions instead of using triggers which most of the times slows down the DML operations/statements on which the triggers are based.
If you handle business logic at application or procedure level then, the DB server will have to execute only DML statements; it does not have to execute TRIGGER-executing trigger involves handling exceptions; prior to that DML statement will place a lock on the table on which DML (except for INSERT statement-Exclusive shared lock) is being executed until TRIGGER is executed.

Related

Trigger wont release locks

I am working on a project that will be replacing an existing older project. On one side I am taking data from external views and on the other I am taking the existing production data. The data between the two systems was supposed to have been in-sync, but they werent at any meaningful level. For the most part this has not been a huge issue as I have largely merged them where possible. In some cases the rows were matched on employee id's, and in others using only surname and birthdate.
The external view data always has two pieces of distinct information, an employee ID and another identifier. The Production data will always have one distinct ID and thats file_number. Sometimes production will have the external view keys, but this is not normal. These keys are the primary keys on either side, not surname and birthdate.
Its this last comparison which has caused the issue as we have several requirements where we are only allowed to join on this criteria, such as when the employee exists in the production side gets an entry on the external views. As surname and birthdate are not particularly distinct, I created an exclusion table for these records that would otherwise cause issues but are valid (twins for example). The exclusion table takes all of the offending records from both sides.
As I was unable to figure out a way to get some kind of constraint where these duplicate records would automatically get entered into the exclusion table (have poor control over data entry), I turned to triggers.
Error
Error report -
SQL Error: ORA-04021: timeout occurred while waiting to lock object
ORA-06512: at "USER.EXCLUSION_TRG", line 4
ORA-04088: error during execution of trigger 'USER.EXCLUSION_TRG'
ORA-06512: at "USER.VIEW_DUPLICATE_TRG", line 4
ORA-04088: error during execution of trigger 'USER.VIEW_DUPLICATE_TRG'
04021. 00000 - "timeout occurred while waiting to lock object %s%s%s%s%s"
*Cause: While waiting to lock a library object, a timeout is occurred.
*Action: Retry the operation later.
First trigger for VIEW Table
CREATE OR REPLACE TRIGGER VIEW_DUPLICATE_TRG
AFTER INSERT OR UPDATE ON VIEW_PERSON
BEGIN
INSERT INTO VIEW_EXCLUSION_PERSON (EMPLID, PRI, COMMENTS)
select emplid, PRI, 'VIEW CREATED '||SYSDATE from (
select upper(CONVERT(last_name, 'US7ASCII')) LAST_NAME, birthdate,first_name,emplid, pri, count(*) over (partition by upper(CONVERT(last_name, 'US7ASCII')), birthdate) duplicate_count from VIEW_PERSON
) K where duplicate_count > 1
and NOT EXISTS (select emplid from exclusion_person Z WHERE K.EMPLID=Z.EMPLID);
END;
Second trigger for Prod table
CREATE OR REPLACE TRIGGER PROD_DUPLICATE_TRG
AFTER INSERT OR UPDATE ON BACKGROUND_INFO
BEGIN
INSERT INTO EXCLUSION_PERSON (FILE_NUMBER, COMMENTS)
SELECT FILE_NUMBER, 'PROD CREATED '||SYSDATE
FROM BACKGROUND_INFO
WHERE (SURNAME, BIRTHDATE) IN
(SELECT SURNAME, BIRTHDATE
FROM BACKGROUND_INFO
GROUP BY SURNAME, BIRTHDATE
HAVING COUNT(*) > 1
)
AND FILE_NUMBER NOT IN (SELECT FILE_NUMBER FROM exclusion_person WHERE FILE_NUMBER IS NOT NULL);
END;
Third trigger for Exclusion table
CREATE OR REPLACE TRIGGER EXCLUSION_TRG
AFTER INSERT ON EXCLUSION_PERSON
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
EXECUTE IMMEDIATE 'ALTER TRIGGER EXCLUSION_TRG DISABLE';
merge into EXCLUSION_PERSON E
using (select file_number, DECODE(TRIM(PRI), '99999999', NULL, PRI) PRI from administrative_info) A
on (E.PRI=A.PRI)
when matched then update set E.file_number = A.file_number, E.COMMENTS = E.COMMENTS||', MATCHED ON PRI ON '||SYSDATE
WHERE E.FILE_NUMBER IS NULL AND E.PRI IS NOT NULL AND A.PRI IS NOT NULL;
MERGE INTO VIEW_EXCLUSION_PERSON E
USING (SELECT FILE_NUMBER, EMPLID FROM VIEW_PERSON) P
ON (P.EMPLID = E.EMPLID)
WHEN MATCHED THEN UPDATE SET
E.FILE_NUMBER = P.FILE_NUMBER,
E.COMMENTS = E.COMMENTS||' MATCHED FROM PERSON '||SYSDATE
WHERE E.FILE_NUMBER IS NULL AND E.EMPLID IS NOT NULL AND P.FILE_NUMBER IS NOT NULL;
EXECUTE IMMEDIATE 'ALTER TRIGGER VIEW_DUPLICATE_TRG DISABLE';
MERGE INTO VIEW_PERSON P
USING (SELECT FILE_NUMBER, EMPLID FROM EXCLUSION_PERSON ) E
ON (P.EMPLID = E.EMPLID)
WHEN MATCHED THEN UPDATE SET P.FILE_NUMBER = E.FILE_NUMBER
WHERE P.FILE_NUMBER IS NULL AND E.EMPLID IS NOT NULL AND E.FILE_NUMBER IS NOT NULL;
EXECUTE IMMEDIATE 'ALTER TRIGGER VIEW_DUPLICATE_TRG ENABLE';
EXECUTE IMMEDIATE 'ALTER TRIGGER EXCLUSION_TRG ENABLE';
END;
So the issue seems to be that the first trigger VIEW_DUPLICATER_TRG is locking something and not releasing that lock when EXCLUSION_TRG is attempting to run. When I go looking for this %S%S%S%S%S object, I just cant find it, and none of my code is calling this object.
The major issue is here:
EXECUTE IMMEDIATE 'ALTER TRIGGER EXCLUSION_TRG DISABLE';
This tries to disable the trigger while the trigger itself is executing!
This will never work: you can't disable a trigger while it's executing. So to try to change its state from within its own execution will be blocked.
You can see this with the following:
create table t (
c1 int
);
create or replace trigger trg
after insert on t
declare
pragma autonomous_transaction;
begin
execute immediate 'alter trigger trg disable';
end;
/
insert into t values ( 1 );
The insert will be stuck, waiting to try and disable the trigger. But the trigger is executing. So you can't disable it. AAAAARGGGH!
This whole process needs redesigning. Ideally without any triggers!

Statement level trigger to enforce a constraint

I am trying to implement a statement level trigger to enforce the following "An applicant cannot apply for more than two positions in one day".
I am able to enforce it using a row level trigger (as shown below) but I have no clue how to do so using a statement level trigger when I can't use :NEW or :OLD.
I know there are alternatives to using a trigger but I am revising for my exam that would have a similar question so I would appreciate any help.
CREATE TABLE APPLIES(
anumber NUMBER(6) NOT NULL, /* applicant number */
pnumber NUMBER(8) NOT NULL, /* position number */
appDate DATE NOT NULL, /* application date*/
CONSTRAINT APPLIES_pkey PRIMARY KEY(anumber, pnumber)
);
CREATE OR REPLACE TRIGGER app_trigger
BEFORE INSERT ON APPLIES
FOR EACH ROW
DECLARE
counter NUMBER;
BEGIN
SELECT COUNT(*) INTO counter
FROM APPLIES
WHERE anumber = :NEW.anumber
AND to_char(appDate, 'DD-MON-YYYY') = to_char(:NEW.appDate, 'DD-MON-YYYY');
IF counter = 2 THEN
RAISE_APPLICATION_ERROR(-20001, 'error msg');
END IF;
END;
You're correct that you don't have :OLD and :NEW values - so you need to check the entire table to see if the condition (let's not call it a "constraint", as that term has specific meaning in the sense of a relational database) has been violated:
CREATE OR REPLACE TRIGGER APPLIES_AIU
AFTER INSERT OR UPDATE ON APPLIES
BEGIN
FOR aRow IN (SELECT ANUMBER,
TRUNC(APPDATE) AS APPDATE,
COUNT(*) AS APPLICATION_COUNT
FROM APPLIES
GROUP BY ANUMBER, TRUNC(APPDATE)
HAVING COUNT(*) > 2)
LOOP
-- If we get to here it means we have at least one user who has applied
-- for more than two jobs in a single day.
RAISE_APPLICATION_ERROR(-20002, 'Applicant ' || aRow.ANUMBER ||
' applied for ' || aRow.APPLICATION_COUNT ||
' jobs on ' ||
TO_CHAR(aRow.APPDATE, 'DD-MON-YYYY'));
END LOOP;
END APPLIES_AIU;
It's a good idea to add an index to support this query so it will run efficiently:
CREATE INDEX APPLIES_BIU_INDEX
ON APPLIES(ANUMBER, TRUNC(APPDATE));
dbfiddle here
Best of luck.
Your rule involves more than one row at the same time. So you cannot use a FOR ROW LEVEL trigger: querying on APPLIES as you propose would hurl ORA-04091: table is mutating exception.
So, AFTER statement it is.
CREATE OR REPLACE TRIGGER app_trigger
AFTER INSERT OR UPDATE ON APPLIES
DECLARE
cursor c_cnt is
SELECT 1 INTO counter
FROM APPLIES
group by anumber, trunc(appDate) having count(*) > 2;
dummy number;
BEGIN
open c_cnt;
fetch c_cnt in dummy;
if c_cnt%found then
close c_cnt;
RAISE_APPLICATION_ERROR(-20001, 'error msg');
end if;
close c_cnt;
END;
Obviously, querying the whole table will be inefficient at scale. (One of the reasons why triggers are not recommended for this sort of thing). So this is a situation in which we might want to use a compound trigger (assuming we're on 11g or later).

ORA-04091 Mutating Table error during AFTER INSERT Trigger

I have the following AFTER INSERT trigger on a table called DM_USER_ROLE
create or replace TRIGGER "DM_USER_ROLE_T1"
AFTER
insert on "DM_USER_ROLE"
for each row
DECLARE
v_cert_enrolment_id number;
v_user_role_id number;
begin
v_cert_enrolment_id := "DM_CERTIFICATION_ENROLMEN_SEQ".nextval;
v_user_role_id := :new.USER_ROLE_ID;
/*
When a user is assigned a role, we create an enrolment record
in DM Certification record linked to this user/role combination.
We also insert into the DM_COURSE_ENROLMENT table the courses
associated with the certfication
*/
--FIRST AN ENROLMENT RECORD IS CREATED IN DM_CERTIFICATION_ENROLMENT
INSERT INTO DM_CERTIFICATION_ENROLMENT
(CERTIFICATION_ENROLMENT_ID, ALLOCATED_DT, DEADLINE_DATE, STATUS, USER_ROLE_ID)
VALUES
(
v_cert_enrolment_id,
trunc(sysdate),
trunc(sysdate) + 60,
'Enrolled',
v_user_role_id
);
--COURSES LINKED TO THE CERTIFICATION ARE INSERTED INTO DM_COURSE_ENROLMENT
INSERT INTO DM_COURSE_ENROLMENT
(
CERTIFICATION_ENROLMENT_ID,
COURSE_ID,
ALLOCATED_DT,
DEADLINE_DT,
STATUS
)
SELECT v_cert_enrolment_id,
COURSE.COURSE_ID,
trunc(sysdate),
trunc(sysdate) + 60,
'Enrolled'
FROM DM_CERTIFICATION_COURSE COURSE
WHERE CERTIFICATION_ID =
(
SELECT C.CERTIFICATION_ID FROM
DM_CERTIFICATION_ENROLMENT A,
DM_USER_ROLE B,
DM_ROLE_CERTIFICATION C
WHERE
A.USER_ROLE_ID = B.USER_ROLE_ID
AND
B.ROLE_ID = C.ROLE_ID
AND
A.CERTIFICATION_ENROLMENT_ID = v_cert_enrolment_id
);
EXCEPTION
WHEN NO_DATA_FOUND
THEN
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM(-20299)));
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SUBSTR(SQLERRM, 1, 2000));
end;
I need to populate 2 separate tables when an insert happens in this table, and I thought AFTER INSERT triggers avoided issues with mutating tables?
I am not sure what is causing it, perhaps the read in the second INSERT statement from DM_USER_ROLE, which is where this trigger is initiated...but I was under the impression AFTER INSERTs were safe to avoid mutations, as the update has already happened.
Error is:
ORA-04091: table AZLEARN_BACKUP.DM_USER_ROLE is mutating,
trigger/function may not see it
The first insert happens, the second one does not.
This article led me to believe AFTER triggers were safe.
http://www.dba-oracle.com/t_avoiding_mutating_table_error.htm
-------UPDATE---------------
I changed it to do row by row insert using two parameterised cursors and it worked...still not sure what the error was:
create or replace TRIGGER "DM_USER_ROLE_T1"
AFTER
insert on "DM_USER_ROLE"
for each row
DECLARE
v_cert_enrolment_id number;
v_user_role_id number;
v_role_id number;
v_certification_id number;
cursor certs_for_role(p_role_id number) is
select * from DM_ROLE_CERTIFICATION where ROLE_ID = p_role_id;
r_certs_for_role certs_for_role%rowtype;
cursor courses_for_certs(p_cert_id number) is
select * from DM_CERTIFICATION_COURSE where CERTIFICATION_ID = p_cert_id;
r_courses_for_certs courses_for_certs%rowtype;
begin
v_user_role_id := :new.USER_ROLE_ID;
v_role_id := :new.ROLE_ID;
open certs_for_role(v_role_id);
loop
fetch certs_for_role into r_certs_for_role;
exit when certs_for_role%notfound;
v_cert_enrolment_id := "DM_CERTIFICATION_ENROLMEN_SEQ".nextval;
INSERT INTO DM_CERTIFICATION_ENROLMENT
(CERTIFICATION_ENROLMENT_ID, ALLOCATED_DT, DEADLINE_DATE, STATUS, USER_ROLE_ID, CERTIFICATION_ID)
VALUES
(
v_cert_enrolment_id,
trunc(sysdate),
trunc(sysdate) + 60,
'Enrolled',
v_user_role_id,
r_certs_for_role.CERTIFICATION_ID
);
open courses_for_certs(r_certs_for_role.CERTIFICATION_ID);
loop
fetch courses_for_certs into r_courses_for_certs;
exit when courses_for_certs%notfound;
INSERT INTO DM_COURSE_ENROLMENT
(
CERTIFICATION_ENROLMENT_ID,
COURSE_ID,
ALLOCATED_DT,
DEADLINE_DT,
STATUS
)
VALUES
(
v_cert_enrolment_id,
r_courses_for_certs.COURSE_ID,
trunc(sysdate),
trunc(sysdate) + 60,
'Enrolled'
);
end loop;
close courses_for_certs;
end loop;
close certs_for_role;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
DBMS_OUTPUT.PUT_LINE(TO_CHAR(SQLERRM(-20299)));
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SUBSTR(SQLERRM, 1, 2000));
end;
The reason is simply you cannot select from table DM_USER_ROLE where the ROW LEVEL trigger is based on.
In your first solution you have a
SELECT ...
FROM DM_USER_ROLE ...
This is not allowed. Your second trigger does not select table DM_USER_ROLE thus it is working.
The advise in linked page is correct but misleading when they state 'Use an "after" or "instead of" trigger' - It should be more precisely 'Use an "after statement" or "instead of" trigger' . Oracle provides triggers based on following actions:
DML statements (INSERT, UPDATE, DELETE) on a particular table or view
DDL statements (CREATE or ALTER primarily)
Database events, such as logon/logoff, errors, or startup/shutdown
You have a DML trigger which can have different timing points:
Before the triggering statement executes
After the triggering statement executes
Before each row that the triggering statement affects
After each row that the triggering statement affects
Compound Trigger -> this one combines the four triggers listed above
INSTEAD OF Trigger (only for views)
Many people miss the difference between a statement level trigger and a row level trigger.
Row Level Triggers have keyword FOR EACH ROW and run for each row like the keyword implies. If you skip the FOR EACH ROW keyword, then the trigger is executed only once for each statement, no matter how many rows are affected by your INSERT/UPDATE/DELETE statement.
The most likely cause of a mutating table error is the misuse of triggers. Here is a typical example:
1.you insert a row in table A
2.a trigger on table A (for each row) executes a query on table A, for example to compute a summary column
3.Oracle throws an ORA-04091: table A is mutating, trigger/function may not see it
This is an expected and normal behaviour, Oracle wants to protect you from yourself since Oracle guarantees:
•(i) that each statement is atomic (i.e will either fail or succeed
completely)
•(ii) that each statement sees a consistent view of the data
Now in your trigger, when you do the second insert, it's doing a join on the DM_USER_ROLE table to fetch the records and that's the reason you face
ORA-04091: table AZLEARN_BACKUP.DM_USER_ROLE is mutating,
trigger/function may not see it

Processing a huge amount of records in a FOR LOOP in PL/SQL

I have an Oracle (11g) database with a table with about 200 millions of records. I have to modify two columns of each row in the table. What would be the most efficient solution for this?
I tried using a code similiar to the following, but after a while the execution crashed due to ORA-04030 - Out of process memory.
BEGIN
FOR i IN (SELECT * FROM FOO WHERE BAR > 2000)
LOOP
-- Do the processing and update row
END LOOP;
END;
I can't change any OS-level parameters. I have to solve this using the code.
Since I don't know the exact column modifications, I'll just make something up; lets say I need to change the name to be lowercase but the first letter in caps and that I have to give a salary rise depending on job.
This is the most efficient way.
Let's assume I have a 200M row MYEMP table.
create table temp_emp
as select * from myemp
where 1=2;
alter session enable parallel dml
insert /*+ APPEND */ into temp_emp
select
EMPNO
, initcap(ENAME)
, JOB
, MGR
, HIREDATE
, case when job = 'MANAGER' then SAL * 1.1 else SAL * 1.05 end
, COMM
, DEPTNO
from myemp
;
drop table myemp
;
you could rename rather than drop MYEMP, if you need to be "safe" and
double check
rename temp_emp to myemp;
A quick test, with 14million rows, took 16s, using serial execution. By employing database parallelism, the runtime could be reduced even further.
One way is using bulk collect with limit. Only limited rows (as defined by the limit clause) as fetched into the memory, which you can loop over to do the required processing.
declare
cursor cur_foo is
select * from foo where bar > 2000;
type tab is table of foo%rowtype;
v_tab tab;
BEGIN
open cur_foo;
loop
fetch cur_foo bulk collect into v_tab limit 100;
exit when v_tab.count = 0;
for idx in 1..v_tab.count loop
-- do something with v_tab(idx)
end loop;
end loop;
close cur_foo;
END;
You can also use FORALL to do bulk insert, update or delete operations.
See this article on these features on Oracle website
http://www.oracle.com/technetwork/issue-archive/2012/12-sep/o52plsql-1709862.html
and this question on AskTOM:
https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::p11_question_id:5918938803188

trigger after insert on table

create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
declare
nmarcacaoa number;
ncartaoa number;
begin
select nmarcacao into nmarcacaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
select ncartao into ncartaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
insert_pagamentos(nmarcacaoa, ncartaoa); --this is a procedure
exception when others then
raise_application_error(-20001, 'Error in Trigger!!!');
end addpagamento;
when i try to run the insert statement to the table "marcacoes_refeicoes" this procedure gives error: like the table is mutating
create or replace
procedure insert_pagamentos
(nmarcacaoa in number, ncartaoa in number)
AS
BEGIN
insert into pagamentos (nmarcacao, datapagamento, ncartao) values (nmarcacaoa, sysdate, ncartaoa);
commit;
END INSERT_PAGAMENTOS;
Short (oversimplified) answer:
You can't modify a table in a trigger that changes the table.
Long answer:
http://www.oracle-base.com/articles/9i/mutating-table-exceptions.php has a more in-depth explanation, including suggestions how to work around the problem.
You're hitting the mutating-table problem because you're selecting from the same table the trigger is on, but what you seem to be trying to do doesn't make sense. Your queries to get a value for nmarcacaoa and ncartaoa will return a no_data_found or too_many_rows error unless the table had exactly 2 rows in it before your insert:
select nmarcacao from marcacoes_refeicoes
where rownum < (select count(*) from marcacoes_refeicoes);
will return all rows except one; and the one that's excluded will be kind of random as you have no ordering. Though the state is undetermined within the trigger, so you can't really how many rows there are, and it won't let you do this query anyway. You won't normally be able to get a single value, anyway, and it's not obvious which value you actually want.
I can only imagine that you're trying to use the values from the row you are currently inserting, and putting them in your separate payments (pagamentos) table. If so there is a built-in mechanism to do that using correlation names, which lets you refer to the newly inserted row as :new (by default):
create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
begin
insert_pagamentos(:new.nmarcacaoa, :new.ncartaoa);
end addpagamento;
The :new is referring to the current row, so :new.nmarcacaoa is the nmarcacaoa being inserted. You don't need to (and can't) get that value from the table itself. (Even with the suggested autonomous pragma, that would be a separate transaction and would not be able to see your newly inserted and uncommitted data).
Or you can just do the insert directly; not sure what the procedure is adding here really:
create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
begin
insert into pagamentos(nmarcacao, datapagamento, ncartao)
values (:new.nmarcacaoa, sysdate, :new.ncartaoa);
end addpagamento;
I've removed the exception handler as all it was doing was masking the real error, which is rather unhelpful.
Editing this answer in view of the comments below:
You can use PRAGMA AUTONOMOUS_TRANSACTION to get rid of the error, but DONOT USE it as it will NOT solve any purpose.
Use PRAGMA AUTONOMOUS_TRANSACTION:
create or replace
trigger addpagamento
after insert on marcacoes_refeicoes
for each row
declare
PRAGMA AUTONOMOUS_TRANSACTION;
nmarcacaoa number;
ncartaoa number;
begin
select nmarcacao into nmarcacaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
select ncartao into ncartaoa from marcacoes_refeicoes where rownum < (select count(*) from marcacoes_refeicoes);
insert_pagamentos(nmarcacaoa, ncartaoa); --this is a procedure
exception when others then
raise_application_error(-20001, 'Error in Trigger!!!');
end addpagamento;
However in this case , select count(*) from marcacoes_refeicoes will give you the new count after the current insertion into the table.

Resources