Delete Oracle rows based on size - sql

I have this Oracle table which I want to clean from time to time when I reach 2000 rows of data:
CREATE TABLE AGENT_HISTORY(
EVENT_ID INTEGER NOT NULL,
AGENT_ID INTEGER NOT NULL,
EVENT_DATE DATE NOT NULL
)
/
How I can delete the oldest row from the table when the table reaches 2000 rows?

You can delete all but the newest 2000 rows with the following query:
DELETE FROM agent_history a
WHERE 2000 < ( SELECT COUNT(1) cnt FROM agent_history b WHERE b.event_date < a.event_date )
The query checks every row in the table (a) to see how many rows have an event_date LESS than that row. If there are more than 2000 rows less than it, then it will delete that row.
Let me know if this doesn't work.

Create a DBMS_JOB or DBMS_SCHEDULER, that kicks off after certain interval and call a procedure. In that procedure check the count and delete the rows based on event_date.
Sorry, I didn't see your comment until now. Here is the code you were looking for. Make sure you have the grants to create scheduler program and jobs. This code assumes that the event_id is a sequence of #s and keeps up with the event_date. Otherwise change the rank based on both time and id or of your choice. Also you can change time interval. Check DBMS_SCHEDULER package documentation for any errors and corrections.
create or replace procedure proc_house_keeping is
begin
delete
from (
select rank() over (order by event_id desc) rnk
from agent_history
)
where rnk > 2000;
commit;
end;
/
begin
dbms_scheduler.create_program(
program_name => 'PROG_HOUSE_KEEPING',
program_type => 'STORED_PROCEDURE',
program_action => 'PROC_HOUSE_KEEPING',
number_of_arguments => 0,
enabled => FALSE,
comments => 'Procedure to delete rows greater than 2000');
end;
/
begin
dbms_scheduler.create_job(
job_name => 'table_house_keeping',
program_name => 'PROG_HOUSE_KEEPING',
start_date => dbms_scheduler.stime,
repeat_interval => 'FREQ=MINUTELY;INTERVAL=1',
end_date => dbms_scheduler.stime+1,
enabled => false,
auto_drop => false,
comments => 'table house keeping, runs every minute');
end;
/

An approach may be adding a trigger to your table, so that it checks and deletes the oldest rows at every INSERT statement; for example, assuming not more than 3 rows:
CREATE OR REPLACE TRIGGER DELETE_3
AFTER INSERT ON AGENT_HISTORY
DECLARE
vNum number;
minDate date;
BEGIN
delete AGENT_HISTORY
where (event_id, agent_id, event_date) in
( select event_id, agent_id, event_date
from (
select event_id, agent_id, event_date, row_number() over (order by event_date desc) num
from AGENT_HISTORY
)
where num > 3 /* MAX NUMBER OF ROWS = 3*/
);
END;
Say we insert 5 rows:
SQL> begin
2 insert into AGENT_HISTORY(EVENT_ID , AGENT_ID, EVENT_DATE) values ( 1, 1, sysdate);
3 dbms_lock.sleep(1);
4 insert into AGENT_HISTORY(EVENT_ID , AGENT_ID, EVENT_DATE) values ( 2, 2, sysdate);
5 dbms_lock.sleep(1);
6 insert into AGENT_HISTORY(EVENT_ID , AGENT_ID, EVENT_DATE) values ( 3, 3, sysdate);
7 dbms_lock.sleep(1);
8 insert into AGENT_HISTORY(EVENT_ID , AGENT_ID, EVENT_DATE) values ( 4, 4, sysdate);
9 dbms_lock.sleep(1);
10 insert into AGENT_HISTORY(EVENT_ID , AGENT_ID, EVENT_DATE) values ( 5, 5, sysdate);
11 commit;
12 end;
13 /
PL/SQL procedure successfully completed.
we only have the newest 3:
SQL> select * from AGENT_HISTORY;
EVENT_ID AGENT_ID EVENT_DATE
---------- ---------- ---------------------------------------------------------------------------
3 3 18-FEB-16 17:05:24,000000
4 4 18-FEB-16 17:05:25,000000
5 5 18-FEB-16 17:05:26,000000

Related

Conditionally insert into another table if id exists in another table else insert into both both tables in oracle

If customer id exists in table A, insert order in table B.
if customer id does not exist in table A, insert customer id in table A and then order in table B.
I have been trying to achieve this with if/else and merge but keep running into
invalid sql statement.
IF EXISTS (SELECT CustomerID FROM Customer_T WHERE CustomerID = 18)
Insert into Order_T
values(79,18,to_date('09/28/2021','mm/dd/yyyy'),to_date('10/01/2021','mm/dd/yyyy'),1,3)
ELSE
insert INTO Customer_T VALUES (18,'Capitol Industries Ltd', '999 Fifth Avenue', 'New York', 'NY','10015')
insert into Order_T values (79,18,to_date('09/28/2021','mm/dd/yyyy'),to_date('10/01/2021','mm/dd/yyyy'),1,3)
END IF;
The IF THEN ELSE logic is not needed for this case. Instead make use of the database built-in functionality. customerid should be your primary key, so if you try to insert and it already exists that will raise the DUP_VAL_ON_INDEX exception.
Check the following example:
-- create tables
create table customers (
id number generated by default on null as identity
constraint customers_id_pk primary key,
name varchar2(255 char)
)
;
create table orders (
id number generated by default on null as identity
constraint orders_id_pk primary key,
customer_id number
constraint orders_customer_id_fk
references customers on delete cascade,
product varchar2(100 char)
)
;
BEGIN
BEGIN
insert INTO customers VALUES (2,'Capitol Industries Ltd');
EXCEPTION WHEN DUP_VAL_ON_INDEX THEN
NULL;
END;
insert into orders (customer_id,product) values (2,'a book');
END;
/
run the above block a couple of times. Only the first time it will insert a customer.
Suppose you have 2 very simple tables.
Tables
create table T1( num_ )
as
select 1 from dual ;
create table T2( num_ )
as
select 200 from dual ;
An anonymous block, containing IF .. ELSE .. END IF and EXISTS() similar to the code in your question, causes an error:
function or pseudo-column 'EXISTS' may be used inside a SQL statement
only
begin
if exists( select num_ from T1 where num_ = 2 ) then
insert into T2( num_ ) values( 2 ) ;
dbms_output.put_line( 'if' ) ;
else
insert into T1( num_ ) values( 2 ) ;
insert into T2( num_ ) values( 2 ) ;
dbms_output.put_line( 'else' ) ;
end if ;
end ;
/
-- error:
... function or pseudo-column 'EXISTS' may be used inside a SQL statement only
One solution may be to do the following (see asktom.oracle.com - Equivalent for EXISTS() in an IF statement)
begin
for x in ( select count(*) cnt
from dual
where exists ( select num_ from T1 where num_ = 2 )
) loop
if ( x.cnt = 1 ) then -- found
insert into T2( num_ ) values( 2 ) ;
dbms_output.put_line( 'if' ) ;
else -- not found
insert into T1( num_ ) values( 2 ) ;
insert into T2( num_ ) values( 2 ) ;
dbms_output.put_line( 'else' ) ;
end if;
end loop;
end;
/
-- output:
1 rows affected
dbms_output:
else
After the first execution of the anonymous block, the tables contain the following rows:
select num_, '<- T1' as table_ from T1
union all
select num_, '<- T2' from T2 ;
-- result
NUM_ TABLE_
1 <- T1
2 <- T1
200 <- T2
2 <- T2
Execute the anonymous block again, and you get ...
1 rows affected
dbms_output:
if
-- tables
NUM_ TABLE_
1 <- T1
2 <- T1
200 <- T2
2 <- T2
2 <- T2
DBfiddle here.
You may use multitable insert and use the fact that aggregate function without group by always returns a row with null in case of absent (= not satisfying where condition) row.
The code is below:
insert into customers(id, name, company, state)
values (1, 'Some name', 'Some company', 'NY')
1 rows affected
insert all
when cust_exists = 0
then
into customers (id, name, company, state)
values (cust_id, cust_name, company, state)
when 1 = 1
then
into orders (id, customer_id, order_date, due_date, some_id)
values(order_id, cust_id, order_date, due_date, some_id)
select
1 as order_id,
1 as cust_id,
'Some other name' as cust_name,
'Company' as company,
'NY' as state,
date '2021-09-28' as order_date,
date '2021-10-03' as due_date,
100 as some_id,
nvl(max(1), 0) as cust_exists
from customers
where id = 1
1 rows affected
insert all
when cust_exists = 0
then
into customers (id, name, company, state)
values (cust_id, cust_name, company, state)
when 1 = 1
then
into orders (id, customer_id, order_date, due_date, some_id)
values(order_id, cust_id, order_date, due_date, some_id)
select
2 as order_id,
2 as cust_id,
'Some other name' as cust_name,
'Company' as company,
'NY' as state,
date '2021-09-28' as order_date,
date '2021-10-03' as due_date,
100 as some_id,
nvl(max(1), 0) as cust_exists
from customers
where id = 2
2 rows affected
You may also use two inserts with documented ignore_row_on_dupkey_index hint, which does what its name implies.
insert /*+
ignore_row_on_dupkey_index(customers(id))
*/
into customers (id, name, company, state)
values (2, 'Name', 'Comp', 'NY')
✓
insert into orders (id, customer_id, order_date, due_date, some_id)
values (3, 2, date '2021-09-30', date '2021-10-07', 5)
1 rows affected
select *
from customers
ID
NAME
COMPANY
STATE
1
Some name
Some company
NY
2
Some other name
Company
NY
db<>fiddle here

Oracle converting an INSERT to MERGE ot not EXISTS

I have the following code, which is working fine. If I run the procedure more than once with the same values I get a PRIMARY KEY violation, which I expect. Could the INSERT be converted into a MERGE or NOT EXISTS to avoid this issue?
The examples I saw online appear to be using literal values or an ON statement with the MERGE.
As I am a novice SQL developer any help or sample code, which reflects my requirement would be greatly appreciated.
Thanks in advance to all who answer.
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE OR REPLACE TYPE nt_date IS TABLE OF DATE;
/
CREATE OR REPLACE FUNCTION generate_dates_pipelined(
p_from IN DATE,
p_to IN DATE
)
RETURN nt_date PIPELINED DETERMINISTIC
IS
v_start DATE := TRUNC(LEAST(p_from, p_to));
v_end DATE := TRUNC(GREATEST(p_from, p_to));
BEGIN
LOOP
PIPE ROW (v_start);
EXIT WHEN v_start >= v_end;
v_start := v_start + INTERVAL '1' DAY;
END LOOP;
RETURN;
END generate_dates_pipelined;
/
create table schedule_assignment(
schedule_id number(4),
schedule_date DATE,
employee_id NUMBER(6) DEFAULT 0,
constraint sa_chk check (schedule_date=trunc(schedule_date, 'dd')),
constraint sa_pk primary key (schedule_id, schedule_date)
);
CREATE OR REPLACE PROCEDURE
create_schedule_assignment (
p_schedule_id IN NUMBER,
p_start_date IN DATE,
p_end_date IN DATE
)
IS
BEGIN
INSERT INTO schedule_assignment(
schedule_id,
schedule_date
)
SELECT
p_schedule_id,
COLUMN_VALUE
FROM TABLE(generate_dates_pipelined(p_start_date, p_end_date));
END;
EXEC create_schedule_assignment (1, DATE '2021-08-21', DATE '2021-08-30');
Rewrite procedure to
SQL> CREATE OR REPLACE PROCEDURE
2 create_schedule_assignment (
3 p_schedule_id IN NUMBER,
4 p_start_date IN DATE,
5 p_end_date IN DATE
6 )
7 IS
8 BEGIN
9 merge into schedule_assignment s
10 using (select p_schedule_id as schedule_id,
11 column_value as schedule_date
12 from table(generate_dates_pipelined(p_start_date, p_end_date))
13 ) x
14 on ( x.schedule_id = s.schedule_id
15 and x.schedule_date = s.schedule_date
16 )
17 when not matched then insert (schedule_id, schedule_date)
18 values (x.schedule_id, x.schedule_date);
19 END;
20 /
Procedure created.
SQL>
Testing: initially, table is empty:
SQL> select schedule_id, min(schedule_date) mindat, max(schedule_date) maxdate, count(*)
2 from schedule_assignment group by schedule_id;
no rows selected
Run the procedure for the 1st time:
SQL> EXEC create_schedule_assignment (1, DATE '2021-08-21', DATE '2021-08-30');
PL/SQL procedure successfully completed.
Table contents:
SQL> select schedule_id, min(schedule_date) mindat, max(schedule_date) maxdate, count(*)
2 from schedule_assignment group by schedule_id;
SCHEDULE_ID MINDAT MAXDATE COUNT(*)
----------- ---------- ---------- ----------
1 21/08/2021 30/08/2021 10
Run the procedure with same parameters again:
SQL> EXEC create_schedule_assignment (1, DATE '2021-08-21', DATE '2021-08-30');
PL/SQL procedure successfully completed.
SQL> EXEC create_schedule_assignment (1, DATE '2021-08-21', DATE '2021-08-30');
PL/SQL procedure successfully completed.
SQL> EXEC create_schedule_assignment (1, DATE '2021-08-21', DATE '2021-08-30');
PL/SQL procedure successfully completed.
Result: nothing changed, no rows in table (but no error either):
SQL> select schedule_id, min(schedule_date) mindat, max(schedule_date) maxdate, count(*)
2 from schedule_assignment group by schedule_id;
SCHEDULE_ID MINDAT MAXDATE COUNT(*)
----------- ---------- ---------- ----------
1 21/08/2021 30/08/2021 10
SQL>
Run the procedure with the same SCHEDULE_ID, but different dates:
SQL> EXEC create_schedule_assignment (1, DATE '2021-08-29', DATE '2021-09-02');
PL/SQL procedure successfully completed.
SQL> select schedule_id, min(schedule_date) mindat, max(schedule_date) maxdate, count(*)
2 from schedule_assignment group by schedule_id;
SCHEDULE_ID MINDAT MAXDATE COUNT(*)
----------- ---------- ---------- ----------
1 21/08/2021 02/09/2021 13
SQL>
Right; number of rows is now increased to 13 (was 10 previously, because 31.08., 01.09. and 02.09. were added).
New SCHEDULE_ID:
SQL> EXEC create_schedule_assignment (2, DATE '2021-09-05', DATE '2021-09-07');
PL/SQL procedure successfully completed.
SQL> select schedule_id, min(schedule_date) mindat, max(schedule_date) maxdate, count(*)
2 from schedule_assignment group by schedule_id;
SCHEDULE_ID MINDAT MAXDATE COUNT(*)
----------- ---------- ---------- ----------
1 21/08/2021 02/09/2021 13
2 05/09/2021 07/09/2021 3
SQL>
Looks OK to me.

Oracle difficulty creating a procedure that has subquery

I am attempting to build a procedure that will INSERT rows into the table emp_attendance.
I call a procedure that generates a list of dates based on a range. I then join that table with each employee_id.
Being a novice SQL developer, I am having difficulty trying to understand why the procedure create_emp_attendance is not being created.
Below is my test CASE. Once I get the rows working for the SELECT I will add the INSERT code as I am trying to take the one little piece at a time.
Thanks in advance for your help and expertise.
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE OR REPLACE TYPE nt_date IS TABLE OF DATE;
CREATE OR REPLACE FUNCTION generate_dates_pipelined(
p_from IN DATE,
p_to IN DATE
)
RETURN nt_date PIPELINED DETERMINISTIC
IS
v_start DATE := TRUNC(LEAST(p_from, p_to));
v_end DATE := TRUNC(GREATEST(p_from, p_to));
BEGIN
LOOP
PIPE ROW (v_start);
EXIT WHEN v_start >= v_end;
v_start := v_start + INTERVAL '1' DAY;
END LOOP;
RETURN;
END generate_dates_pipelined;
CREATE SEQUENCE batch_seq
START WITH 1
MAXVALUE 999999999999999999999999999
MINVALUE 1
NOCYCLE
CACHE 20
NOORDER;
Create table employees(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
work_days VARCHAR2(7)
);
INSERT INTO employees (
employee_id,
first_name,
last_name,
card_num,
work_days
)
WITH names AS (
SELECT 1, 'John', 'Doe', 'D564311','YYYYYNN' FROM dual UNION ALL
SELECT 2, 'Justin', 'Case', 'C224311','YYYYYNN' FROM dual UNION ALL
SELECT 3, 'Mike', 'Jones', 'J288811','YYYYYNN' FROM dual UNION ALL
SELECT 4, 'Jane', 'Smith', 'S564661','YYYYYNN' FROM dual
) SELECT * FROM names;
CREATE TABLE emp_attendance(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
start_date DATE,
end_date DATE,
week_number NUMBER(2),
create_date DATE DEFAULT SYSDATE
);
CREATE OR REPLACE PROCEDURE create_emp_attendance (
p_start_date IN DATE,
p_end_date IN DATE
)
IS l_batch_seq number;
BEGIN
SELECT get_batch_seq INTO l_batch_seq FROM dual;
SELECT
employee_id,
start_date,
start_date+NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(3600,43200)), 'SECOND') AS end_date,
to_char(start_date,'WW') AS week_number
FROM (
-- Need subquery to generate end_date based on start_date.
SELECT
e.employee_id, d.COLUMN_VALUE+ NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(0,86399)), 'SECOND') AS start_date
FROM employees e
INNER JOIN TABLE( generate_dates_pipelined(p_start_date, p_end_date)
) d
) ed
END;
EXEC create_emp_attendanc(DATE '2021-08-07', DATE '2021-08-14');
You didn't refer to the sequence, so I removed that. Your SELECT needs to have a target via INTO clause or be used within the context of an INSERT (or other) statement. The JOIN was missing an ON clause. But you appeared to want a CROSS JOIN.
If you really want to INSERT in one statement, here's the form:
CREATE OR REPLACE PROCEDURE create_emp_attendance (
p_start_date IN DATE,
p_end_date IN DATE
)
IS
BEGIN
INSERT INTO emp_attendance (employee_id, start_date, end_date, week_number)
SELECT employee_id
, start_date
, start_date+NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(3600,43200)), 'SECOND') AS end_date
, to_char(start_date,'WW') AS week_number
FROM ( -- Need subquery to generate end_date based on start_date.
SELECT e.employee_id, d.COLUMN_VALUE + NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(0,86399)), 'SECOND') AS start_date
FROM employees e
CROSS JOIN TABLE( generate_dates_pipelined(p_start_date, p_end_date) ) d
) ed
;
END;
/
EXEC create_emp_attendance(DATE '2021-08-07', DATE '2021-08-14');
/
-- Procedure CREATE_EMP_ATTENDANCE compiled
-- PL/SQL procedure successfully completed.
Read comments within code.
SQL> CREATE OR REPLACE PROCEDURE create_emp_attendance
2 (
3 p_start_date IN DATE,
4 p_end_date IN DATE
5 )
6 IS
7 l_batch_seq number;
8 BEGIN
9 -- there's no GET_BATCH_SEQ function (at least, you didn't post it)
10 -- SELECT get_batch_seq INTO l_batch_seq FROM dual;
11 l_batch_seq := batch_seq.nextval;
12
13 -- In order to avoid TOO_MANY_ROWS, switching to a cursor FOR loop
14 for cur_r in
15 (SELECT
16 employee_id,
17 start_date,
18 start_date+NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(3600,43200)), 'SECOND') AS end_date,
19 to_char(start_date,'WW') AS week_number
20 FROM (-- Need subquery to generate end_date based on start_date.
21 SELECT
22 e.employee_id,
23 d.COLUMN_VALUE + NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(0,86399)), 'SECOND') AS start_date
24 FROM employees e
25 -- not INNER, but CROSS join (or, if it were INNER, on which column(s)?)
26 CROSS JOIN TABLE(generate_dates_pipelined(p_start_date, p_end_date)) d
27 ) ed
28 ) loop
29 -- you'll probably have INSERT statement here, according to what you said
30 null;
31 end loop;
32 END;
33 /
Procedure created.
Testing:
SQL> EXEC create_emp_attendance(DATE '2021-08-07', DATE '2021-08-14');
PL/SQL procedure successfully completed.
SQL>

compare i and i+1 element oracle

I have table
CREATE table table_x
(
id NUMBER, -- ID
NUMBER_history NUMBER, -- consecutive number
start_date date, -- start_date of next id=end_date of previous
end_date date -- should be >=start_date
);
INSERT INTO table_x VALUES (14 , 1, '13-NOV-92' ,'14-NOV-92' );
INSERT INTO table_x VALUES (15 , 2, '14-NOV-92' ,'16-NOV-92' );
INSERT INTO table_x VALUES (19 , 3, '16-NOV-92' ,'18-NOV-92' );
INSERT INTO table_x VALUES (11 , 4, '18-NOV-92' ,'14-NOV-93' );
INSERT INTO table_x VALUES (17 , 5, '15-NOV-93' ,'16-NOV-93' );
INSERT INTO table_x VALUES (151 , 6, '16-NOV-93' ,'17-NOV-93' );
INSERT INTO table_x VALUES (139 , 7, '17-NOV-93' ,'18-NOV-93' );
INSERT INTO table_x VALUES (121 , 8, '19-NOV-93' ,'20-DEC-93' );
INSERT INTO table_x VALUES (822 , 9, '20-DEC-93' ,'20-DEC-93' );
I want write query where I can find where start_date of next row > end_date of previous. they must be equal.
I try to do something like that using NUMBER_history as counter. C-way where I organize cycle by variable i and compare i and i+1 (NUMBER_history and NUMBER_history+1)
select * INTO row_tblx from table_x where NUMBER_history=NUMBER_history and end_date<(select start_date from table_x where NUMBER_history=NUMBER_history+1);
but I must organize loop by n_counter from 1 to last NUMBER_history value and fetch data into multiple row.
How can I do it?
I try
set serveroutput on
DECLARE
CURSOR cur IS
SELECT * FROM table_x;
TYPE row_tblx_type
IS
TABLE OF cur%ROWTYPE;
row_tblx row_tblx_type;
rowx cur%ROWTYPE;
nh_count NUMBER;
BEGIN
FOR NUMBER_history IN cur LOOP
select * INTO row_tblx from table_x where NUMBER_history=NUMBER_history and end_date<(select start_date from table_x where NUMBER_history=NUMBER_history+1);
DBMS_OUTPUT.PUT_LINE (row_tblx(NUMBER_history).id);
END LOOP;
END;
/
How can I do it using for or another loop, multiple records or table of records , cursor, an table row as counter (NUMBER_history)?
How can I do it without cursor?
You don't need PL/SQL or a loop for that:
select *
from (
select id, number_history, start_date, end_date,
lead(start_date) over (order by number_history) as next_start
from table_x
) t
where next_start > end_date;
For example, here's a solution that does not require PL/SQL or LEAD/LAG function
select a.*, b.*
from table_x a
join table_x b
on a.number_history+1 = b.number_history
where b.start_date > a.end_date

Update previous record version during Oracle insert trigger

This is my insert trigger on Table_A where I store parameters to my system. When I do insert to the table, I want to change end_date of last record in order to keep record versioning.
create or replace trigger parameter_version
before insert
on parameters
for each row
declare
v_is_exist number := 0;
v_rowid rowid;
begin
select count(*) into v_is_exist from parameters where name = :new.name; -- check if parameter exist
select rowid into v_rowid from parameters where name = :new.name and end_date is null; -- record rowid, which sholud be changed
if v_is_exist <> 0 then
set end_date = :new.start_date - 1
end if;
end;
Situation in table before insert is:
| id | name | value | start_date | end_date |
-----------------------------------------------
| 1 |Par_A | 10 | 2016-09-01 | 2016-10-01 |
-----------------------------------------------
| 2 |Par_A | 20 | 2016-10-02 | 2016-10-03 |
-----------------------------------------------
| 3 |Par_A | 30 | 2016-10-05 | <null> |
-----------------------------------------------
Record with id=3 should set end_date on :new_start_date - 1 (close version) and in inserting record I have a next param version with start_date = sysdate.
I've got an ORA-04091 error 'table name is mutating, trigger/function may not see it'.
I know that this case is hard and probably impossible but maybe someone know the solution?
Or maybe exists another solution that case?
You can handle this with an After Statement trigger with the LEAD Analytic Function:
DROP TABLE demo;
CREATE TABLE demo( id NUMBER
, name VARCHAR2( 30 )
, VALUE NUMBER
, start_date DATE
, end_date DATE
);
INSERT INTO demo( id, name, VALUE, start_date, end_date )
VALUES ( 1, 'Par_A', 10, TO_DATE( '2016-09-01', 'YYYY-MM-DD' ), TO_DATE( '2016-10-01', 'YYYY-MM-DD' ) );
INSERT INTO demo( id, name, VALUE, start_date, end_date )
VALUES ( 2, 'Par_A', 20, TO_DATE( '2016-10-02', 'YYYY-MM-DD' ), TO_DATE( '2016-10-04', 'YYYY-MM-DD' ) );
INSERT INTO demo( id, name, VALUE, start_date )
VALUES ( 3, 'Par_A', 30, TO_DATE( '2016-10-05', 'YYYY-MM-DD' ) );
INSERT INTO demo( id, name, VALUE, start_date )
VALUES ( 4, 'Par_A', 40, TO_DATE( '2016-10-07', 'YYYY-MM-DD' ) );
INSERT INTO demo( id, name, VALUE, start_date )
VALUES ( 5, 'Par_A', 50, TO_DATE( '2016-10-11', 'YYYY-MM-DD' ) );
COMMIT;
SELECT id
, name
, start_date
, end_date
, LEAD( start_date ) OVER( PARTITION BY name ORDER BY start_date ) - 1 AS new_date
FROM demo
WHERE end_date IS NULL
ORDER BY id;
CREATE OR REPLACE TRIGGER demo_aius
AFTER INSERT OR UPDATE
ON demo
REFERENCING NEW AS new OLD AS old
DECLARE
CURSOR c_todo
IS
SELECT id, new_date
FROM (SELECT id
, name
, start_date
, end_date
, LEAD( start_date ) OVER( PARTITION BY name ORDER BY start_date ) - 1 AS new_date
FROM demo
WHERE end_date IS NULL)
WHERE new_date IS NOT NULL;
BEGIN
FOR rec IN c_todo
LOOP
UPDATE demo
SET end_date = rec.new_date
WHERE id = rec.id;
END LOOP;
END demo_aius;
/
INSERT INTO demo( id, name, VALUE, start_date )
VALUES ( 6, 'Par_A', 60, TO_DATE( '2016-10-15', 'YYYY-MM-DD' ) );
COMMIT;
SELECT id
, name
, start_date
, end_date
FROM demo
ORDER BY id;
Like the Script shows, such an Update can even handle multiple missing end dates, in case the trigger was accidentally disabled. The "PARTITION BY name" part makes sure that it also functions after complex insert statements.
BtW I agree that Autonomous Transactions in triggers are a last resort. I try to avoid triggers in general by controlling the User Interface and putting all such functionality in packages.
Try something like this:
create or replace trigger parameter_version
before insert
on parameters
for each row
begin
/*Don't care if there's 0 rows updated */
update parameters
set end_date = :new.start_date - 1
where name = :new.name and end_date is null;
:new.end_date := null;
end;