PL SQL Compund Triggers on Batch Inserts - oracle

I've written a compound trigger to fire on inserts. Multiple inserts are batched together and sent to the DB where the compound trigger picks it up. My problem is that i need to perform an update query on the same table for certain inserts depending on the data provided by the query. I can't run a row level action since that would result in a mutating trigger table error (ORA-4091). Best thing i could think of was to have the update query in the before or after statement blocks. i cannot have it on the before statement block since each update is dependent on individual inserts and there's no way of knowing the values before actually reaching that query. so i created a "Type" table and updated it before each row is modified and then later at the after statement block i iterate through the Type table and perform update queries using the data on the table. No matter what i tried the After statement block will only perform update queries for the last insert only.
TYPE apple IS RECORD ( v_size apple_t.size%Type, v_color apple_t.color%Type);
TYPE t_apple IS TABLE OF apple INDEX BY VARCHAR2(20);
BEFORE ROW
t_apple(key).v_size := :New.size;
t_apple(key).v_color := :New.color;
END BEFORE ROW
AFTER STATEMENT
Iterator := t_apple.First;
LOOP EXIT WHEN ITERATOR IS NULL;
UPDATE apple_t SET SIZE = 10
WHERE color = t_apple(Iterator).color;
Iterator := t_apple.Next(Iterator);
END LOOP
END AFTER STATEMENT
This basically is how the trigger is designed. Using a second table is out of the question since trigger cost is a major factor. Any Pointers? Please and Thankyou

I dont fully understand but I think you can get your keys after each row ,then update data in after statament block as follows.
declare
idx number := 1 ;
type array_t is varray(10000) of varchar2(100) ;
colorArr array_t := array_t();
AFTER EACH ROW IS
BEGIN
if inserting then
colorArr (idx) := :new.color;
idx := idx + 1 ;
end if;
END
AFTER EACH ROW;
AFTER STATEMENT IS
BEGIN
for i in 1..sicilNoCol.count
loop
-- update here
end loop;
END AFTER STATEMENT;
or why dont you write a simple before insert trigger that you can manuplate :new.size in it? Does it give table mutable error?

Related

how to pass a whole row as record to a function in oracle triggers

I create a oracle package with function where in the input is a whole row and the output is varray. i want to use this package function to apply on all the rows of a table using trigger as soon as new record is inserted. I tried like this but the output is bad bind variable
create or replace trigger custdata_caferrors
before insert or update on customer_data
referencing new as n old as o
for each row
declare
v_remarks varchar(500) :=' ';
t_remarks caf_errors :=caf_errors();
rec customer_data%rowtype;
begin
-- rec = :n;
t_remarks := CUSTDATA_VERIFY.VERIFY_TERMSTATUS(:n);
for x in 1..t_remarks.count
loop
v_remarks :=v_remarks||' , '||t_remarks(x);
end loop;
:n.record_status1 :=v_remarks;
end;
/
Unfortunately there is no way to reference a whole new row in a trigger. I agree it would be neat if this were possible, but alas.
So you will have to explicitly populate your rec variable with the column values you require and pass that to your function, like this
create or replace trigger custdata_caferrors
before insert or update on customer_data
referencing new as n old as o
for each row
declare
v_remarks varchar(500) :=' ';
t_remarks caf_errors :=caf_errors();
rec customer_data%rowtype;
begin
rec.id := :n.id;
rec.col1 := :n.col1;
-- etc
t_remarks := CUSTDATA_VERIFY.VERIFY_TERMSTATUS(rec);
for x in 1..t_remarks.count
loop
v_remarks :=v_remarks||' , '||t_remarks(x);
end loop;
:n.record_status1 :=v_remarks;
end;
/
Do you really need to pass the whole row to CUSTDATA_VERIFY.VERIFY_TERMSTATUS()? If not you might get away with populating just the columns the function actually uses (although that does contravene the Law of Demeter).

How to remove a row from an Oracle cursor but not the database table

I have a FOR LOOP cursor and while looping through it I need to delete some of the rows from the cursor but not from the database table. Is that possible to do?
What I am trying to accomplish is to be left with only those rows in the cursor my code did not process by removing each processed row that met certain criteria
Bulk collect the cursor rows into a collection. Then as each row in processed delete it from the collection. What's left will be the rows not originally processed. The following provides a skeleton for the process needed:
declare
cursor c_cursor
is
select ... ;
type c_cursor_t is table of c_cursor%rowtype;
l_cursor_data c_cursor_t;
l_cursor_indx integer;
begin
open c_cursor;
fetch c_cursor
bulk collect
into l_cursor_data;
close c_cursor;
l_cursor_indx := l_cursor_data.first; -- set collection index to 1st value
while l_cursor_indx is not null
loop
if <row should be processed> -- determine which rows to process
then
<process_row>; -- and process them
l_cursor_data.delete(l_cursor_indx); -- then delete processed rows
end if ;
l_cursor_indx := l_cursor_data.next(l_cursor_indx); -- set collection index to next row or null if no morw rows.
end loop;
--- Handle anything left in l_cursor_data collection as they have not been processed.
--- THE SAME LOOP STRUCTURE AN BE USED FOR THE COLLECTION IF NEEDED.
end ;
Of course as #MT0 it would be much easier to eliminate those that will not be processed from the query to begin with. Just retrieve the rows you want to process is always the best practice.
You cannot modify a cursor while you are reading it; if you want to exclude rows then you will need to do it when you are generating the cursor.
Use the WHERE clause to exclude the rows from the cursor:
DECLARE
OPEN cursor_name FOR
SELECT *
FROM my_table
WHERE primary_key_column NOT IN ( 1, 2, 3 ); -- criteria to exclude.
BEGIN
-- process cursor with excluded rows.
END;
/
Load a collection with the results from your query, making sure the collection contains a 'processed' flag that is initialized to False. Then loop through the collection, processing as you need to. Flip the flag to True when done.
Then you could loop through the collection again where the processed_flag is False to get your untouched rows.

compound trigger - performance impact

I want to ask question on performance of compound trigger, I have actually implemented it recently and observed great performance improvement also. But there is something which I want to discuss.
Please consider the below scenario for my question :
Suppose there are two tables created as below:
~ Two tables are created :
: CREATE TABLE ORDERS (ORD_ID NUMBER, ITEM_CODE VARCHAR2(100), ORD_QTY NUMBER,ORD_DATE DATE);
: CREATE TABLE ORDER_ARCHIVE(ORD_ID NUMBER, ORD_CODE VARCHAR2(100));
~ Now I have created compound trigger as given below :
create or replace trigger TRG_COMP_SAL
for update or insert on ORDERS
compound trigger --Trigger Type is Compound here
type t_tbl_typ is table of ORDER_ARCHIVE%rowtype index by pls_integer;
v_tbl_events t_tbl_typ;
idx pls_integer := 0;
--After Each row statement
after each row is
begin
IF INSERTING THEN
idx := idx + 1;
v_tbl_events(idx).ORD_ID := :new.ORD_ID;
v_tbl_events(idx).ORD_CODE := :NEW.ITEM_CODE;
END IF;
-- Checking threshold limit for indx which will be used for bulk insert
if idx >= 100 then
forall cnt in 1 .. v_tbl_events.count()
insert into ORDER_ARCHIVE values v_tbl_events (cnt);
v_tbl_events.delete;
idx := 0;-- resetting threshold limit for indx which will be used for bulk insert
end if;
end after each row;
--After statement
after statement is
begin
IF INSERTING THEN
--Using forall to bulk insert data
forall cnt in 1 .. v_tbl_events.count()
insert into ORDER_ARCHIVE values v_tbl_events (cnt);
END IF;
end after statement;
end TRG_COMP_SAL;
This is surely faster in execution as compared with the normal trigger.
But, I am little confused about :
In case of bulk insert, AFTER STATEMENT and BEFORE STATEMENT will be executed only once but for each insert
to check whether BEFORE EACH ROW and AFTER EACH ROW block exists or not in compound trigger the compound trigger will be invoked.
Will it not impact performance?
Please help..
Thanks in advance....
I also come across this need to accumulate the individual inserts and after statement do insert in one go using forall. But in usual way of insert, as insert in itself is one statement, so it will call the after statement for each insert! Also tried with INSERT ALL into ... But that also fires the after statement on each "into ..".
To make all insert into one statement, it is mentioned here:
Best way to do multi-row insert in Oracle?. So after making inserts as one statement then it will flush only after the threshhold or "after statement". In this way even though for each row will be called every time but heavy work will be done only at the time of flush (threshhold or after statement).

How to get table fields from which was called a trigger with Oracle 10g?

I want to calculate the average of values ​​in a column of my table (PRA_COEFF) (RAPPORT_VISITE) for a field (PRA_NUM) given, when I add or change a row of it. Then I want to save this value in another table (PRACTITIONER) to the row where PRA_NUM worth PRA_NUM given above.
CREATE TABLE "RAPPORT_VISITE"
(
"RAP_NUM" NUMBER (10,0),
"PRA_NUM" NUMBER (10,0),
"PRA_COEFF" NUMBER (10,0),
)
CREATE TABLE "PRATICIEN"
(
"PRA_NUM" NUMBER (10,0),
"PRA_COEFCONF" NUMBER
)
The trigger is called when adding or Modification RAPPORT_VISITE the table. I tried like this, but I can not retrieve the row affected by the trigger, and thus PRA_NUM, that I need to read.
create or replace TRIGGER UDPATE_PRAT_COEFCONF
AFTER INSERT or UPDATE ON RAPPORT_VISITE
DECLARE
somme NUMBER;
nb NUMBER;
moyenne NUMBER;
rapport NUMBER;
pra_id NUMBER;
BEGIN
/*SELECT MAX(RAP_NUM) INTO rapport FROM RAPPORT_VISITE; // Not want I need in case where I modify a row... */
SELECT PRA_NUM INTO pra_id FROM RAPPORT_VISITE WHERE RAP_NUM=rapport;
SELECT SUM(PRA_COEFF) INTO somme FROM RAPPORT_VISITE WHERE PRA_NUM=pra_id;
SELECT COUNT(*) INTO nb FROM RAPPORT_VISITE WHERE PRA_NUM=pra_id;
IF (nb != 0) THEN
moyenne := somme/nb;
moyenne := TRUNC (moyenne,1);
UPDATE PRATICIEN SET PRA_COEFCONF=moyenne WHERE PRA_NUM=pra_id;
END IF;
END;
Here are 2 limits that triggers are usually have:
when invoked for the all affected records, you don't know what exactly have changed
when invoked for individual records (FOR EACH ROW), you are limited with access to the modified table
To address that limitation, starting from Oracle 11g, we can use compound trigger:
CREATE OR REPLACE TRIGGER <trigger-name>
FOR <trigger-action> ON <table-name>
COMPOUND TRIGGER
-- Global declaration.
g_global_variable VARCHAR2(10);
-- block 1
BEFORE STATEMENT IS
BEGIN
NULL; -- Do something here.
END BEFORE STATEMENT;
-- block 2
BEFORE EACH ROW IS
BEGIN
NULL; -- Do something here.
END BEFORE EACH ROW;
-- block 3
AFTER EACH ROW IS
BEGIN
NULL; -- Do something here.
END AFTER EACH ROW;
-- block 4
AFTER STATEMENT IS
BEGIN
NULL; -- Do something here.
END AFTER STATEMENT;
END <trigger-name>;
And looks like this is what you need. In block 1, initialize your variables, in block 2 or 3 collect changes from individual rows, then in block 4 use that information to create the rest of the business logic.
If we are limited by 10g, then we can emulate compound trigger using package variables.
This solution is limited, because package variables are global for the session. If withing a session you have 2 similar operations, their results would be merged.
Here is the solution
You will have 3 separate triggers, that would represent block 1, (2 or 3), and 4 from the trigger above.
You will have a package with variable g_global_variable (from above)
3 actions:
1. in trigger for block 1 initiate g_global_variable
2. in trigger for block 2 or 3, populate it with actual values
3. in trigger for block 4, create your logic
Ofcourse, g_global_variable could be not alone, it could be record or collection.

PL/SQL trigger to prevent duplicates

I have a table called TBLAPPLICATION which holds data specifying an individual's ID number and a JobID of the job they have applied for. Each ID number can have an unlimited number of applications, providing the JobID is different every time, thus having no duplicate applications.
create or replace
TRIGGER trg_duplicateapplication BEFORE INSERT ON tblapplication FOR EACH ROW
BEGIN
IF :NEW.studentrecordnumber_fk_nn = :OLD.studentrecordnumber_fk_nn THEN
IF :NEW.jobid_fk_nn = :OLD.jobid_fk_nn
THEN RAISE_APPLICATION_ERROR( -20003, 'Error: duplicate application. You have already applied for this position.');
END IF;
END IF;
END;
So the above code doesn't work, and I wish it would. Could anyone please highlight my mistake? :)
As it stands, your trigger is comparing the inserted values (:NEW.studentrecordnumber_fk_nn etc) with a non-existent :OLD (:OLD has no meaning to an INSERT trigger—it's fields are always null).
That aside, this should almost certainly be accomplished by DRI instead of a trigger at all— how about a unique index on (studentrecordnumber_fk_nn, jobid_fk_nn)?
You can use the MERGE statement in order to verify each couple (id,application) before inserting in the table (to check whether it is already in the table).
http://docs.oracle.com/cd/E11882_01/server.112/e26088/statements_9016.htm#SQLRF01606
Regards,
Dariyoosh
I am not sure that in your table TBLAPPLICATION which identifier is unique (maybe JobID?) and which you want you not to be duplicated (maybe studentrecordnumber_fk_nn?). But I created a script to prevent duplication on studentrecordnumber_fk_nn.
And in my example “alphabet” I wrote a totally similar script to prevent the duplication: you cannot insert a letter which was inserted into the table earlier.
I hope it will help.
z
CREATE OR REPLACE TRIGGER trg_duplicateapplication
BEFORE INSERT
ON tblapplication
FOR EACH ROW
DECLARE
counter integer;
BEGIN
SELECT * INTO counter FROM
(SELECT COUNT(rownum) FROM tblapplication a
WHERE a.studentrecordnumber_fk_nn = :new.studentrecordnumber_fk_nn);
IF counter = 1 THEN
RAISE_APPLICATION_ERROR( -20003,
'Error: duplicate application. You have already applied for this position.');
END IF;
END;
/
––The Alphabet
CREATE TABLE alphabet
(letter VARCHAR2(2));
INSERT INTO alphabet VALUES ('A');
INSERT INTO alphabet VALUES ('B');
INSERT INTO alphabet VALUES ('C');
INSERT INTO alphabet VALUES ('D');
CREATE OR REPLACE TRIGGER insertvalue
BEFORE INSERT
ON alphabet
FOR EACH ROW
DECLARE counter INTEGER;
BEGIN
SELECT * INTO counter FROM
(SELECT COUNT(rownum) FROM alphabet a WHERE a.letter = :new.letter);
IF counter = 1 THEN
RAISE_APPLICATION_ERROR(-20012,'Duplicated value');
END IF;
END;
/

Resources