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).
Related
I have a litte task. Firstly I added a column in my table with specific constraints. Then I added a trigger for other jobs.
But I need a rollbacksql and have no idea what to proceed. Can anybody help or give an advice about it? I am adding my sql snippet.
ALTER TABLE FCBSADM.GL_DEF ADD GL_TP NUMBER;
ALTER TABLE FCBSADM.GL_DEF ADD CONSTRAINTS CH_COL CHECK (GL_TP between 1 and 10);
CREATE OR REPLACE TRIGGER GL_DEF_GL_TP_TRG
BEFORE INSERT OR
DELETE OR
UPDATE OF CDATE, CMPNY_DEF_ID, CUSER, DESCR, GL_DEF_ID, MNY_TP_ID, ST, UDATE, UUSER, GL_TP
ON FCBSADM.GL_DEF
FOR EACH ROW
DECLARE
cnt number := 0;
BEGIN
IF INSERTING
THEN
IF :NEW.GL_TP = 2
THEN
SELECT 1 into cnt from dual where exists( select *
FROM LOOKUP_GLCODE_IND_CEZA lookup
WHERE lookup.indirim_glcode = :NEW.gl_Def_id);
IF (cnt = 1) THEN
raise_application_error
(-20101, 'Please insert record into LOOKUP_GLCODE_IND_CEZA before inserting GL_DEF');
END IF;
END IF;
END IF;
END;
What do you want to rollback? Adding a column and creating a trigger? If so, drop them, both.
alter table gl_def drop column gl_tp;
drop trigger gl_def_gl_tp_trg;
A trigger and newly added table can be rolled back only by using drop and alter statements. This is ok if its being done inside a script that executes only a few times. But is highly inefficient if both drop and alter are called frequently for n number of records.
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).
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?
I am developing stored procedure for Oracle 10g and I am hitting pretty heavy performance bottle neck while trying to pass list of about 2-3k items into procedure. Here's my code:
TYPE ty_id_list
AS TABLE OF NUMBER(11);
----------------------------------------------------------
PROCEDURE sp_performance_test (
p_idsCollection IN schema.ty_id_list )
AS
TYPE type_numeric_table IS TABLE OF NUMBER(11) INDEX BY BINARY_INTEGER;
l_ids type_numeric_table;
data type_numeric_table;
empty type_numeric_table;
BEGIN
EXECUTE IMMEDIATE
'ALTER TABLE schema.T_TEST_TABLE NOLOGGING';
COMMIT;
SELECT COLUMN_VALUE BULK COLLECT INTO l_ids
FROM TABLE(p_idsCollection);
FOR j IN 1 .. l_ids.COUNT LOOP
data(data.count+1) := l_ids(j);
IF(MOD(data.COUNT,500) = 0 ) THEN
FORALL i IN 1 .. data.COUNT
INSERT INTO schema.T_TEST_TABLE (REF_ID, ACTIVE)
VALUES (data(i), 'Y');
data := empty;
END IF;
END LOOP;
IF(data.count IS NOT NULL) THEN
FORALL i IN 1 .. data.COUNT
INSERT INTO schema.T_TEST_TABLE (REF_ID, ACTIVE)
VALUES (data(i), 'Y');
END IF;
COMMIT;
EXECUTE IMMEDIATE
'ALTER TABLE schema.T_TEST_TABLE LOGGING';
COMMIT;
END sp_performance_test;
So the thing that adds up to the process quite drastically seems to be this part: data(data.count+1) := l_ids(j); If I skip getting element from the collection and change this line to data(data.count+1) := j;, procedure execution time will be 3-4 times faster (from over 30s to 8-9s for 3k items) - but this logic obviously is not the one i want.
Can You guys give me a hint where could I improve my code to get better performance on inserting data? If any improvements can be done really.
Thanks,
Przemek
I don't follow your logic.
You accept a collection. You copy it to another collection:
SELECT COLUMN_VALUE BULK COLLECT INTO l_ids
FROM TABLE(p_idsCollection);
And then you copy it once again, in a loop:
FOR j IN 1 .. l_ids.COUNT LOOP
data(data.count+1) := l_ids(j);
And only after that you manage to perform your 500-row-chunk bulk insert. What is wrong with bulk inserting p_idsCollection immediately?
p.s. You don't need commits after 'ALTER TABLE', ddl statements issue them implicitly.
that whole block after the DDL can be rewritten as
insert into schema.T_TEST_TABLE (REF_ID, ACTIVE)
select COLUMN_VALUE, 'Y' FROM TABLE(p_idsCollection);
You can also add hint into insert operation.
Insert /*+ append */ into tab (...) values (...)
It's change oracle work logic and it will work faster.
http://www.dba-oracle.com/t_insert_tuning.htm
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;
/