reset sequence by trigger - oracle

I am using a trigger to reset a sequence every year,
but there is some issue when calling a procedure into the triggers
CREATE OR REPLACE TRIGGER t_dmd_pk
BEFORE INSERT
ON S_DEMANDE
FOR EACH ROW
BEGIN
IF (TO_CHAR (SYSDATE, 'dd') = '16' AND TO_CHAR (SYSDATE, 'mm') = '12')
THEN
reset_seq ('SEQ_ID_DMD');
END IF;
SELECT SEQ_ID_DMD.NEXTVAL || TO_CHAR (SYSDATE, 'yyyy')
INTO :new.DMD_ID
FROM DUAL;
END;
/
and that's my procedure
CREATE OR REPLACE PROCEDURE reset_seq (p_seq_name IN VARCHAR2)
IS
l_val NUMBER;
BEGIN
EXECUTE IMMEDIATE 'select ' || p_seq_name || '.nextval from dual'
INTO l_val;
EXECUTE IMMEDIATE
'alter sequence ' || p_seq_name || ' increment by -' || l_val;
END;
/

The trigger is executed inside an INSERT statement, and the trigger call a procedure that tries to commit the transaction (ALTER SEQUENCE is a DDL statatement, so it is auto-commited).
To ensure statement atomicity the transaction can only be commited when the last statement is finalized. So it is not possible to commit the current transaction inside a trigger.
But you can execute your trigger or procedure as an autonomous transaction (Oracle opens a new transaction and executes the code of your trigger or porcedure inside this new transaction).
See this link for more details: http://www.oracle-base.com/articles/misc/autonomous-transactions.php
But remember:
the autonomous transaction cannot see your still uncommited data, and
if you finally rollback your current transaction (after the execution of the trigger and the commit of the autonomous transaction) the inserted tuples will be rolled back, but the autonomous transaction will not be rolled back.

Your procedure doesn't work the way you think it should.. If your sequence last value was 10, then you are altering the sequence to increment by -10 every time it is called. I am guessing the first time you execute it, you get an ORA-08004 because your minvalue is probably 1 and it would be trying to return 0 (which isn't allowed). Even if that didn't error, the next time you called it would, as it would try to return -10 in my example. What I believe you really want is:
CREATE OR REPLACE PROCEDURE reset_seq (p_seq_name IN VARCHAR2)
IS
l_val NUMBER;
BEGIN
-- Get Current Value of Sequence
EXECUTE IMMEDIATE 'select ' || p_seq_name || '.nextval from dual'
INTO l_val;
-- Alter to sequence to allow to go to 0 and decrease by current value
EXECUTE IMMEDIATE
'alter sequence ' || p_seq_name || ' minvalue 0 increment by -' || l_val;
-- Get value from sequence again (should set seq to 0)
EXECUTE IMMEDIATE 'select ' || p_seq_name || '.nextval from dual'
INTO l_val;
-- Alter sequence to increase by 1 again
EXECUTE IMMEDIATE
'alter sequence ' || p_seq_name || ' increment by 1';
END;
This allows the sequence to be 0 (which you need if you want the next call to return 1), sets it to 0, then changes it back to increment by 1 with each successive call. However, it is probably much easier to just drop and recreate the sequence.
The real question though, is why you would ever want to do this. This looks like bad design. A sequence is just supposed to return a unique number. Nothing more, nothing less. There should be no meaning behind the number, and it sure seems like you are trying to assign meaning here. Sequences don't guarantee your rows will be inserted in order and don't guarantee there won't be gaps, they just provide a unique number. Concatenating the year on the end of this unique number makes this design more suspect.

i found a solution, i used dbms_job instead of trigger, that works fine for me

Related

Can a insert operation make another DDL operation wait?

I am trying to understand the reason why i get the below error.
`ORA-04021: timeout occurred while waiting to lock object`
This error is thrown from a procedure while running the command alter table <<T_NAME>> truncate subpartition <<SUBPARTITION_NAME>>.
v_dyncursor_stmt := 'with obj as (select /*+ materialize */ data_object_id, subobject_name from user_objects where object_name = UPPER(''' ||
p_table_name ||
''') and object_type = ''TABLE SUBPARTITION'') select '||p_hint||' distinct subobject_name from ' ||
p_table_name || ' t, obj where data_object_id = DBMS_MView.PMarker(t.rowid) and ' || p_where;
/* log */
log_text(v_unit_name, 'INFO', 'Open cursor', v_dyncursor_stmt);
/* loop over partitions which needs to be truncated */
v_counter := 0;
open c_subpartitions for v_dyncursor_stmt;
loop
FETCH c_subpartitions
INTO v_subpartition_name;
EXIT WHEN c_subpartitions%NOTFOUND;
v_statement := 'alter table ' || p_table_name || ' truncate subpartition "' || v_subpartition_name || '"';
execStmt(v_statement);
the code is calling above procedure twice and the first attempt is successful. it truncates the subpartition fine. In the second attempt it is failing... The execStmt function is given below, the error is thrown from EXCEUTE IMMEDITE line...
procedure execStmt(p_statement IN VARCHAR2) IS
v_unit_name varchar2(1024) := 'execStmt';
v_simulate varchar2(256);
begin
v_simulate := utilities.get_parameter('PART_PURGE_SIMULATE', '0');
if (v_simulate = '1') then
log_text(v_unit_name, 'INFO', 'Statement skipped. (PART_PURGE_SIMULATE=1)',
p_statement);
else
/* log */
log_text(v_unit_name, 'INFO', 'Executing statement', p_statement);
EXECUTE IMMEDIATE p_statement;
end if;
end;
As this happens mostly over the weekend, i do not get a chance to inspect the lock tables to see what has locked the object. but i know for sure that it is a table which has alot of inserts happening.
So my question is can an insert operation on a table prevent the above DDL ??
from oracle docs,i see that an insert aquires a SX lock which is explained as below,
A row exclusive lock (RX), also called a subexclusive table lock (SX), indicates that the transaction holding the lock has updated table rows or issued SELECT ... FOR UPDATE. An SX lock allows other transactions to query, insert, update, delete, or lock rows concurrently in the same table. Therefore, SX locks allow multiple transactions to obtain simultaneous SX and SS locks for the same table.
This error happens because partition you are trying to truncate is in use at that time. And as mentioned by you, these insert statements are running that time, and it can affect DDL operation.

How to delete sequences and procedures during logoff trigger?

Could you please help me in a unique situation I am in. I am receiving "ORA-30511: invalid DDL operation in system triggers" when dropping sequences and procedures during logoff trigger.
I need to delete tables, sequences and procedures of users before logoff event happens. I am writing the table details in DB_OBJECTS table upon create using a separate trigger. Below is my logoff trigger - could you please help me where I am doing wrong. Dropping tables is working fine in the below code. Only Dropping sequences and procedures is giving me "ORA-30511: invalid DDL operation in system triggers" error.
CREATE OR REPLACE TRIGGER DELETE_BEFORE_LOGOFF
BEFORE LOGOFF ON DATABASE
DECLARE
USER_ID NUMBER := SYS_CONTEXT('USERENV', 'SESSIONID');
BEGIN
FOR O IN (SELECT USER, OBJECT_NAME, OBJECT_TYPE
FROM DB_OBJECTS WHERE SID = USER_ID
AND USERNAME = USER AND SYSDATE > CREATED_DTTM) LOOP
IF O.OBJECT_TYPE = 'TABLE' THEN
EXECUTE IMMEDIATE 'DROP TABLE ' || O.USER || '.' || O.OBJECT_NAME || ' CASCADE CONSTRAINTS';
ELSIF O.OBJECT_TYPE = 'SEQUENCE' THEN
EXECUTE IMMEDIATE 'DROP SEQUENCE ' || O.USER || '.' || O.OBJECT_NAME;
ELSIF O.OBJECT_TYPE = 'PROCEDURE' THEN
EXECUTE IMMEDIATE 'DROP PROCEDURE ' || O.USER || '.' || O.OBJECT_NAME;
END IF;
END LOOP;
EXCEPTION WHEN NO_DATA_FOUND THEN NULL;
END;
/
That's a simple one.
Error code: ORA-30511
Description: invalid DDL operation in system triggers
Cause: An attempt was made to perform an invalid DDL operation in a system trigger. Most DDL operations currently are not supported in system triggers. The only currently supported DDL operations are table operations and ALTER/COMPILE operations.
Action: Remove invalid DDL operations in system triggers.
That's why only
Dropping tables is working fine
succeeded.
Therefore, you can't do that using trigger.
You asked (in a comment) how to drop these objects, then. Manually, as far as I can tell. Though, that's quite unusual - what if someone accidentally logs off? You'd drop everything they created. If you use that schema for educational purposes (for example, every student gets their own schema), then you could create a "clean-up" script you'd run once class is over. Something like this:
SET SERVEROUTPUT ON;
DECLARE
l_user VARCHAR2 (30) := 'SCOTT';
l_str VARCHAR2 (200);
BEGIN
IF USER = l_user
THEN
FOR cur_r IN (SELECT object_name, object_type
FROM user_objects
WHERE object_name NOT IN ('EMP',
'DEPT',
'BONUS',
'SALGRADE'))
LOOP
BEGIN
l_str :=
'drop '
|| cur_r.object_type
|| ' "'
|| cur_r.object_name
|| '"';
DBMS_OUTPUT.put_line (l_str);
EXECUTE IMMEDIATE l_str;
EXCEPTION
WHEN OTHERS
THEN
NULL;
END;
END LOOP;
END IF;
END;
/
PURGE RECYCLEBIN;
It is far from being perfect; I use it to clean up my Scott schema I use to answer questions on various sites so - once it becomes a mess, I run that PL/SQL code several times (because of possible foreign key constraint).
Other option is to keep a create user script(s) (along with all grant statements) and - once class is over - drop existing user and simply recreate it.
Or, if that user contains some pre-built tables, keep export file (I mean, result of data pump export) and import it after the user is dropped.
There are various options - I don't know whether I managed to guess correctly, but now you have something to think about.

Execute Oracle For Loop in parallel

I am looping through a list of table and updating a list of column in each table. Is it possible to execute the loop parallely, that is update more than one table at a time.
FOR Table_rec IN Table_list_cur
LOOP
--Check if the table is partitioned
IF Check_if_partitioned (Table_rec.Table_name, Table_rec.Owner)
THEN
--If Yes, loop through each parition
EXECUTE IMMEDIATE
'Select partition_name from USER_TAB_PARTITIONS where table_name = '''
|| Table_rec.Table_name
|| ''' and owner = '''
|| Table_rec.Owner
|| ''''
BULK COLLECT INTO L_part;
FOR I IN L_part.FIRST .. L_part.LAST
LOOP
--Update each parition
DBMS_OUTPUT.Put_line ('V_sql_stmt' || V_sql_stmt);
V_sql_stmt :=
'UPDATE /*+ PARALLEL(upd_tbl,4) */ '
|| Table_rec.Table_name
|| ' PARTITION ('
|| L_part (I)
|| ') upd_tbl'
|| ' SET '
|| V_sql_stmt_col_list;
DBMS_OUTPUT.Put_line ('V_sql_stmt' || V_sql_stmt);
EXECUTE IMMEDIATE V_sql_stmt;
END IF;
END LOOP;
END LOOP;
Not directly, no.
You could take the guts of your loop, factor that out into a stored procedure call, and then submit a series of jobs to do the actual processing that would run asynchronously. Using the dbms_job package so that the job submission is part of the transaction, that would look something like
CREATE OR REPLACE PROCEDURE do_update( p_owner IN VARCHAR2,
p_table_name IN VARCHAR2 )
AS
BEGIN
<<your dynamic SQL>>
END;
and then run the loop to submit the jobs
FOR Table_rec IN Table_list_cur
LOOP
--Check if the table is partitioned
IF Check_if_partitioned (Table_rec.Table_name, Table_rec.Owner)
THEN
dbms_job.submit( l_jobno,
'begin do_update( ''' || table_rec.owner || ''', ''' || table_rec.table_name || '''); end;' );
END IF;
END LOOP;
commit;
Once the commit runs, the individual table jobs will start running (how many will run is controlled by the job_queue_processes parameter) while the rest are queued up.
Now, that said, your approach seems a bit off. First, it's almost never useful to specify a partition name explicitly. You almost certainly want to submit a single UPDATE statement, omit the partition name, and let Oracle do the updates to the various partitions in parallel. Running one update statement per partition rather defeats the purpose of partitioning. And if you really want 4 parallel threads for each partition, you probably don't want many of those updates running in parallel. The point of parallelism is that one statement can be allowed to consume a large fraction of the system's resources. If you really want, say 16 partition-level updates to be running simultaneously and each of those to run 4 slaves, it would make far more sense to let Oracle run 64 slaves for a single update (or whatever number of slaves you really want to devote to this particular task depending on how many resources you want to leave for everything else the system has to do).

How to reset automatically the oracle sequence number before it reach to the maximum number

I have a sequence in my oracle database and set the maximum number to 99999. I like to automate resetting to 10000 before it reach to 99999. Below is my script of myseq.
create sequence myseq
minvalue 1
maxvalue 99999
increment by 1
start with 10000;
The simplest way is to define sequence as CYCLE:
create sequence myseq minvalue 10000 maxvalue 99999 increment by 1 CYCLE;
It will start from minvalue again when reaches maxvalue, i.e. generate numbers in cycle.
Oracle spec says:
CYCLE - Specify CYCLE to indicate that the sequence continues to generate values after reaching either its maximum or minimum value.
After an ascending sequence reaches its maximum value, it generates
its minimum value. After a descending sequence reaches its minimum, it
generates its maximum value.
If you are in 12c, you can use the IDENTITY column and get rid off the SEQUENCE mechanism.
If you are not on 12c, assuming that your primary key is populated via sequence using a trigger. What you can do is :
Create a trigger with the logic to reset the sequence back to normal,
i.e. after every time you purge the table, the sequence would START
WITH 1 and INCREMENT BY 1. using ALTER SEQUENCE.
The sequence logic part using alter statement (Thanks to Tom Kyte for
this) :
create or replace
procedure reset_sequence(p_seq in varchar2)
is
l_value number;
begin
-- Select the next value of the sequence
execute immediate
'select ' || p_seq ||
'.nextval from dual' INTO l_value;
-- Set a negative increment for the sequence,
-- with value = the current value of the sequence
execute immediate
'alter sequence ' || p_seq ||
' increment by -' || l_value || ' minvalue 0';
-- Select once from the sequence, to
-- take its current value back to 0
execute immediate
'select ' || p_seq ||
'.nextval from dual' INTO l_value;
-- Set the increment back to 1
execute immediate
'alter sequence ' || p_seq ||
' increment by 1 minvalue 0';
end;
/
Update : Don't know what I was thinking initially. Based on the other answer, you need ALTER SEQUENCE to TURN ON CYCLE.
ALTER SEQUENCE sequence_name CYCLE;

first attempt at learning oracle triggers

I am just starting to learn triggers so please bear with me. If the row being inserted has a gift that is the same as any gift already in the table, print a message saying that the gift was already given to receiver from donor.
create or replace TRIGGER Same_Gift_Given
BEFORE INSERT ON GIVING
FOR EACH ROW
DECLARE
giftgiven varchar(255);
BEGIN
SELECT giftname INTO giftgiven from GIVING;
IF :new.giftname = giftgiven then
dbms_output.put_line(giftgiven || ' has already been gifted to ' || giving.receiver || ' by ' || giving.donor);
end if;
END;
This is a really awful homework problem. You would never, ever, ever us a trigger to do anything like this in a real system. It will break most INSERT operations and it will fail if there are ever multiple users. In reality, you would use a constraint. In reality, if for some reason you were forced at gunpoint to use a trigger, you would need a series of three triggers, a package, and a collection to do it properly.
What the professor is probably looking for
Just to emphasize, though, you would never, ever consider doing this in a real system
create or replace trigger same_gift_given
before insert on giving
for each row
declare
l_existing_row giving%rowtype;
begin
select *
into l_existing_row
from giving
where giftname = :new.giftname
and rownum = 1;
dbms_output.put_line( :new.giftname ||
' has already been gifted to ' ||
l_existing_row.receiver ||
' from ' ||
l_existing_row.donor );
exception
when no_data_found
then
null;
end;
This does not prevent you from inserting duplicate rows. It will throw a mutating trigger error if you try to do anything other than an INSERT ... VALUES on the giving table. It is inefficient. It does not handle multiple sessions. In short, it is absolutely atrocious code that should never be used in any real system.
What you would do in reality
In reality, you would create a constraint
ALTER TABLE giving
ADD CONSTRAINT unique_gift UNIQUE( giftname );
That will work in a multi-user environment. It will not throw a mutating trigger exception. It is much more efficient. It is much less code. It actually prevents duplicate rows from being inserted.
Let's try something a bit different:
CREATE OR REPLACE TRIGGER GIVING_COMPOUND_INSERT
FOR INSERT ON GIVING
COMPOUND TRIGGER
TYPE STRING_COL IS TABLE OF VARCHAR2(255) INDEX BY VARCHAR2(255);
colGiftnames STRING_COL;
aGiftname VARCHAR2(255);
nCount NUMBER;
-- Note that the way the associative array is used here is a bit of a cheat.
-- In the BEFORE EACH ROW block I'm putting the string of interest into the
-- collection as both the value *and* the index. Then, when iterating the
-- collection only the index is used - the value is never retrieved (but
-- since it's the same as the index, who cares?). I do this because I'd
-- rather not write code to call a constructor and maintain the collections
-- size - so I just use an associative array and let Oracle do the work for
-- me.
BEFORE EACH ROW IS
BEGIN
colGiftnames(:NEW.GIFTNAME) := :NEW.GIFTNAME;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
aGiftname := colGiftnames.FIRST;
WHILE aGiftname IS NOT NULL LOOP
SELECT COUNT(*)
INTO nCount
FROM GIVING
WHERE GIFTNAME = aGiftname;
IF nCount > 1 THEN
DBMS_OUTPUT.PUT_LINE('Found ' || nCount || ' instances of gift ''' ||
aGiftname || '''');
RAISE_APPLICATION_ERROR(-20001, 'Found ' || nCount ||
' instances of gift ''' ||
aGiftname || '''');
END IF;
aGiftname := colGiftnames.NEXT(aGiftname);
END LOOP;
END AFTER STATEMENT;
END GIVING_COMPOUND_INSERT;
Again, this is a LOUSY way to try to guarantee uniqueness. In practice the "right way" to do this is with a constraint (either UNIQUE or PRIMARY KEY). Just because you can do something doesn't mean you should.
Share and enjoy.

Resources