Automation Script to maintain Oracle Table Subpartitions - oracle

I am new to PLSQL.
Can someone please provide me automation script to maintain(drop and create) subpartition in the Oracle table.
I know the script to maintain the table partition but unable to frame for Subpartitions.
Some details:
Range-Range partition
Subpartition: On date column (Monthly)
Retention : 180days data
Here is how Oracle table definition:
CREATE TABLE PART_TABLE
(
"REQUEST_ITEM_ID" NUMBER NOT NULL ENABLE,
"X_CLOB" CLOB,
"ENQUEUED_COUNT" NUMBER,
"UPDATE_LAST_META" NUMBER(1,0),
"CFI_TYPE_ID" NUMBER,
) SEGMENT CREATION IMMEDIATE
LOB ("X_CLOB") STORE AS BASICFILE (TABLESPACE "DATA_TS" ENABLE
PARTITION BY RANGE (PRIORITY)
SUBPARTITION BY RANGE (CREATED)
(PARTITION PART_01 VALUES less THAN(2)
(
SUBPARTITION PART_01_FEB_2020 VALUES LESS THAN (TO_DATE('2020-03-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')),
SUBPARTITION PART_01_MAR_2020 VALUES LESS THAN (TO_DATE('2020-04-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')),
SUBPARTITION PART_01_MAX VALUES LESS THAN (MAXVALUE)
),
PARTITION PART_02 VALUES less THAN(3)
(
SUBPARTITION PART_02_FEB_2020 VALUES LESS THAN (TO_DATE('2020-03-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')),
SUBPARTITION PART_02_MAR_2020 VALUES LESS THAN (TO_DATE('2020-04-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')),
SUBPARTITION PART_02_MAX VALUES LESS THAN (MAXVALUE)
)
PARTITION PART_03 VALUES less THAN(4)
(
SUBPARTITION PART_03_FEB_2020 VALUES LESS THAN (TO_DATE('2020-03-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')),
SUBPARTITION PART_03_MAR_2020 VALUES LESS THAN (TO_DATE('2020-04-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')),
SUBPARTITION PART_03_MAX VALUES LESS THAN (MAXVALUE)
));

I would say, your table has wrong partition pattern. It should be better like this:
CREATE TABLE ... (
your columns
)
PARTITION BY RANGE (CREATED) INTERVAL (INTERVAL '1' MONTH)
SUBPARTITION BY RANGE (PRIORITY)
SUBPARTITION TEMPLATE
(
SUBPARTITION PART_01 VALUES LESS THAN(2),
SUBPARTITION PART_02 VALUES LESS THAN(3),
SUBPARTITION PART_03 VALUES LESS THAN(4),
SUBPARTITION PART_MAX VALUES LESS THAN (MAXVALUE)
)
(
PARTITION P_BEFORE_2019 VALUES LESS THAN (TIMESTAMP '2019-01-01 00:00:00')
);
Then new partitions will be created automatically by Oracle and deletion would be easier as well.
Anyway, a procedure for maintenance could be like this (not tested):
DECLARE
ts TIMESTAMP;
sqlstr VARCHAR2(1000);
CURSOR TabSubPartitions IS
SELECT TABLE_NAME, PARTITION_NAME, SUBPARTITION_NAME, HIGH_VALUE
FROM USER_TAB_SUBPARTITIONS
WHERE TABLE_NAME = 'PART_TABLE'
ORDER BY PARTITION_NAME, SUBPARTITION_NAME;
BEGIN
FOR aSubPart IN TabSubPartitions LOOP
IF aSubPart.HIGH_VALUE <> 'MAXVALUE' THEN
EXECUTE IMMEDIATE 'BEGIN :ret := '||aSubPart.HIGH_VALUE||'; END;' USING OUT ts;
IF ts < SYSTIMESTAMP - INTERVAL '180' DAY THEN
-- As far as I remember you cannot drop a subpartition, thus you can only truncate it
sqlstr := 'ALTER TABLE '||aSubPart.TABLE_NAME||' TRUNCATE SUBPARTITION '||aSubPart.SUBPARTITION_NAME||' UPDATE GLOBAL INDEXES';
EXECUTE IMMEDIATE sqlstr;
END IF;
ELSE
IF TRUNC(LAST_DAY(SYSDATE)) = TRUNC(SYSDATE) THEN
-- If last day of current months then create new monthly partition
-- Perhaps further checks/exception handler are needed to check whether subpartition already exist.
sqlstr := 'ALTER TABLE '||aSubPart.TABLE_NAME||' SPLIT SUBPARTITION '||aSubPart.SUBPARTITION_NAME
||' AT (TIMESTAMP '''||TO_CHAR(SYSDATE+1, 'YYYY-MM-DD HH24:MI:SS')||''') INTO ('
||' SUBPARTITION '||aSubPart.PARTITION_NAME||'_'||TO_CHAR(SYSDATE+1, 'MON_YYYY')||'),'
||' SUBPARTITION '||aSubPart.PARTITION_NAME||'_MAX) UPDATE GLOBAL INDEXES';
EXECUTE IMMEDIATE sqlstr;
END IF;
END IF;
END LOOP;
END;
With a "proper" table design as given at the top the maintenance would be simpler:
DECLARE
CANNOT_DROP_LAST_PARTITION EXCEPTION;
PRAGMA EXCEPTION_INIT(CANNOT_DROP_LAST_PARTITION, -14758);
ts TIMESTAMP;
sqlstr VARCHAR2(1000);
CURSOR TabPartitions IS
SELECT TABLE_NAME, PARTITION_NAME, HIGH_VALUE
FROM USER_TAB_PARTITIONS
WHERE TABLE_NAME = 'PART_TABLE'
ORDER BY PARTITION_NAME;
BEGIN
FOR aPart IN TabPartitions LOOP
BEGIN
EXECUTE IMMEDIATE 'BEGIN :ret := '||aPart.HIGH_VALUE||'; END;' USING OUT ts;
IF ts < SYSTIMESTAMP - INTERVAL '180' DAY THEN
sqlstr := 'ALTER TABLE '||aPart.TABLE_NAME||' DROP PARTITION '||aSubPart.PARTITION_NAME||' UPDATE GLOBAL INDEXES';
EXECUTE IMMEDIATE sqlstr;
END IF;
EXCEPTION
WHEN CANNOT_DROP_LAST_PARTITION THEN
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL ()';
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' DROP PARTITION ('||aPart.PARTITION_NAME||') UPDATE INDEXES';
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL (INTERVAL ''1'' MONTH)';
END;
END LOOP;
END;
You may skip the exception handler WHEN CANNOT_DROP_LAST_PARTITION THEN ..., this exception appears only once and when you get then you can run the three command manually. After that this exception will never raised again.

Related

Add a new partition to other partition based on the date column and 1 partition for each day

I want to Design a table like table F and partition it in such a way that a new partition is created by adding a new data assuming it is not in the existing partitions. (partition based on the date column and 1 partition for each day) . I am new in oracle, please help me ,what is the best idea ? How can I write this code .
Table F is :
DATE Amount ID
2015-05-18 1000 1
2015-05-19 2000 2
2015-05-20 3000 3
2015-05-21 4000 4
2015-05-21 5000 5
2015-05-21 3000 6
2015-05-22 2002 7
You can create several different partitions on a date column ie ( daily, weekly, monthly, quarterly or yearly) and include a local or global index on the column you need to decide what fits your needs
Below is an example of a weekly PARTITION with a global index. The PARTITIONs will be automatically added with a system generated name
when a new date is inserted into the table.
/* weekly PARTITION */
CREATE TABLE t1 (
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
dt DATE)
PARTITION BY RANGE (dt)
INTERVAL ( NUMTODSINTERVAL (7, 'DAY') ) (
PARTITION OLD_DATA VALUES LESS THAN (DATE '2022-04-01')
);
/
INSERT into t1 (dt)
with dt (dt, interv) as (
select date '2022-04-01', numtodsinterval(1,'DAY') from dual
union all
select dt.dt + interv, interv from dt
where dt.dt + interv < date '2022-08-31')
select dt from dt;
/
create index t1_global_ix on T1 (dt);
/
Every time a new partition is created Oracle generates a system name, which is quite cryptic
Here is a list of the PARTITION names, which Oracle generated when I loaded the data above
SELECT PARTITION_NAME
FROM USER_TAB_PARTITIONS
WHERE TABLE_NAME = 'T1'
PARTITION_NAME
OLD_DATA
SYS_P458773
SYS_P458774
SYS_P458775
SYS_P458776
SYS_P458777
SYS_P458778
SYS_P458779
SYS_P458780
SYS_P458781
SYS_P458782
SYS_P458783
SYS_P458784
SYS_P458785
SYS_P458786
SYS_P458787
SYS_P458788
SYS_P458789
SYS_P458790
SYS_P458791
SYS_P458792
SYS_P458793
SYS_P458794
Although PARTITION management will work with system GENERATED PARTITION names, I use the procedure below to rename them to something more meaningful.
Let's create and run the procedure and take a look at the names again. As you can see, since we are working with weekly partitions the name is P_ for partittion YYYY 4 digit year the PARTITION is in, W for Week of the year, and ## for the week number within the year.
I would suggest using the scheduler to run this process at least once a day. You can run it as many times as you want as it will not cause any harm.
CREATE OR REPLACE PROCEDURE MaintainPartitions IS EXPRESSION_IS_OF_WRONG_TYPE EXCEPTION;
PRAGMA EXCEPTION_INIT(EXPRESSION_IS_OF_WRONG_TYPE, -6550);
CURSOR PartTables IS
SELECT TABLE_NAME, INTERVAL
FROM USER_PART_TABLES
WHERE PARTITIONING_TYPE = 'RANGE'
ORDER BY TABLE_NAME;
CURSOR TabParts(aTableName VARCHAR2) IS
SELECT PARTITION_NAME, HIGH_VALUE
FROM USER_TAB_PARTITIONS
WHERE regexp_like(partition_name,'^SYS_P[[:digit:]]{1,10}') AND
TABLE_NAME = aTableName AND
table_name not like 'BIN$%'
and interval is not null
ORDER BY PARTITION_POSITION;
ym INTERVAL YEAR TO MONTH;
ds INTERVAL DAY TO SECOND;
newPartName VARCHAR2(30);
PERIOD TIMESTAMP;
BEGIN
FOR aTab IN PartTables LOOP
BEGIN
EXECUTE IMMEDIATE 'BEGIN :ret := '||aTab.INTERVAL||'; END;' USING OUT ds;
ym := NULL;
EXCEPTION
WHEN EXPRESSION_IS_OF_WRONG_TYPE THEN
EXECUTE IMMEDIATE 'BEGIN :ret := '||aTab.INTERVAL||'; END;' USING OUT ym;
ds := NULL;
END;
FOR aPart IN TabParts(aTab.TABLE_NAME) LOOP
EXECUTE IMMEDIATE 'BEGIN :ret := '||aPart.HIGH_VALUE||'; END;' USING OUT PERIOD;
IF ds IS NOT NULL THEN
IF ds >= INTERVAL '7' DAY THEN
-- Weekly partition
EXECUTE IMMEDIATE 'BEGIN :ret := TO_CHAR('||aPart.HIGH_VALUE||' - :int, :fmt); END;' USING OUT newPartName, INTERVAL '1' DAY, '"P_"IYYY"W"IW';
ELSE
-- Daily partition
EXECUTE IMMEDIATE 'BEGIN :ret := TO_CHAR('||aPart.HIGH_VALUE||' - :int, :fmt); END;' USING OUT newPartName, INTERVAL '1' DAY, '"P_"YYYYMMDD';
END IF;
ELSE
IF ym = INTERVAL '3' MONTH THEN
-- Quarterly partition
EXECUTE IMMEDIATE 'BEGIN :ret := TO_CHAR('||aPart.HIGH_VALUE||' - :int, :fmt); END;' USING OUT newPartName, INTERVAL '1' DAY, '"P_"YYYY"Q"Q';
ELSE
-- Monthly partition
EXECUTE IMMEDIATE 'BEGIN :ret := TO_CHAR('||aPart.HIGH_VALUE||' - :int, :fmt); END;' USING OUT newPartName, INTERVAL '1' DAY, '"P_"YYYYMM';
END IF;
END IF;
IF newPartName <> aPart.PARTITION_NAME THEN
EXECUTE IMMEDIATE 'ALTER TABLE '||aTab.TABLE_NAME||' RENAME PARTITION '||aPart.PARTITION_NAME||' TO '||newPartName;
END IF;
END LOOP;
END LOOP;
END MaintainPartitions;
/
EXEC MaintainPartitions
SELECT PARTITION_NAME
FROM USER_TAB_PARTITIONS
WHERE TABLE_NAME = 'T1'
PARTITION_NAME
OLD_DATA
P_2022W14
P_2022W15
P_2022W16
P_2022W17
P_2022W18
P_2022W19
P_2022W20
P_2022W21
P_2022W22
P_2022W23
P_2022W24
P_2022W25
P_2022W26
P_2022W27
P_2022W28
P_2022W29
P_2022W30
P_2022W31
P_2022W32
P_2022W33
P_2022W34
SELECT COUNT(*) FROM USER_TAB_PARTITIONS
COUNT(*)
31
Next step is setting up your RETENTION table. There should be an entry for each interval range PARTITION.
The RETENTION value is for you to decide. In my example, I chose 30 days fir table T1. This means, when the high value for a PARTITION is greater than 30 days its eligible to be dropped. So chose wisely when setting up these values.
Note: I listed the names of other tables to show how each table has its own value.
CREATE TABLE PARTITION_RETENTION (
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
TABLE_NAME VARCHAR2(30),
RETENTION INTERVAL DAY(3) TO SECOND(0),
CONSTRAINT
partition_retention_pk primary key (table_name),
CONSTRAINT CHK_NON_ZERO_DAYS CHECK (
RETENTION > INTERVAL '0' DAY
),
CONSTRAINT CHK_WHOLE_DAYS CHECK (
EXTRACT(HOUR FROM RETENTION) = 0
AND EXTRACT(MINUTE FROM RETENTION) = 0
AND EXTRACT(SECOND FROM RETENTION) = 0
)
);
insert into PARTITION_RETENTION (TABLE_NAME, RETENTION)
select 'T0', interval '10' day from dual union all
select 'T1', interval '30' day from dual union all
select 'T2', interval '15' day from dual union all
select 'T3', interval '30' day from dual union all
select 'T4', 15 * interval '1' day from dual union all
select 'T5', 5 * interval '1 00:00:00' day to second from dual;
Below are 3 procedures that need to be created.
The ddl procedure is a wrapper, which shows you what is being processed and how long it takes.
The rebuild_index procedure is obvious it rebuilds any invalid indexes. As I mentioned above if you are using a global index and a PARTITION is dropped then the index needs to be rebuilt. I hardcoded parallel 4 in this example but if you have plenty if CPU power you may want to increase the number to fit your needs.
In addition, there are other ways indexes can be marked unusable so you may want to consider scheduling that task.
Lastly, is the anonymous block. Which actually drops the PARTITIONS for, which the retention PERIOD has passed. This needs to be scheduled once a day!!
If you look carefully at the anonymous block the last step is a call to the rebuild index procedure. So if an index is unusable it will be rebuilt.
Now let's run the process and see what happens.
CREATE OR REPLACE PROCEDURE ddl(p_cmd varchar2)
authid current_user
is
t1 pls_integer;
BEGIN
t1 := dbms_utility.get_time;
dbms_output.put_line(p_cmd);
execute immediate p_cmd;
dbms_output.put_line((dbms_utility.get_time - t1)/100 || ' seconds');
END;
/
CREATE OR REPLACE PROCEDURE rebuild_index
authid current_user
is
BEGIN
for i in (
select index_owner, index_name, partition_name, 'partition' ddl_type
from all_ind_partitions
where status = 'UNUSABLE'
union all
select owner, index_name, null, null
from all_indexes
where status = 'UNUSABLE'
)
loop
if i.ddl_type is null then
ddl('alter index '||i.index_owner||'.'||i.index_name||' rebuild parallel 4 online');
else
ddl('alter index '||i.index_owner||'.'||i.index_name||' modify '||i.ddl_type||' '||i.partition_name||' rebuild parallel 4 online');
end if;
end loop;
END;
/
DECLARE
CANNOT_DROP_LAST_PARTITION EXCEPTION;
PRAGMA EXCEPTION_INIT(CANNOT_DROP_LAST_PARTITION, -14758);
CANNOT_DROP_ONLY_ONE_PARTITION EXCEPTION;
PRAGMA EXCEPTION_INIT(CANNOT_DROP_ONLY_ONE_PARTITION, -14083);
ts TIMESTAMP;
CURSOR TablePartitions IS
SELECT TABLE_NAME, PARTITION_NAME, p.HIGH_VALUE, t.INTERVAL, RETENTION, DATA_TYPE
FROM USER_PART_TABLES t
JOIN USER_TAB_PARTITIONS p USING (TABLE_NAME)
JOIN USER_PART_KEY_COLUMNS pk ON pk.NAME = TABLE_NAME
JOIN USER_TAB_COLS tc USING (TABLE_NAME, COLUMN_NAME)
JOIN PARTITION_RETENTION r USING (TABLE_NAME)
WHERE pk.object_type = 'TABLE' AND
t.partitioning_type = 'RANGE' AND
REGEXP_LIKE (tc.data_type, '^DATE$|^TIMESTAMP.*');
BEGIN
FOR aPart IN TablePartitions LOOP
EXECUTE IMMEDIATE 'BEGIN :ret := '||aPart.HIGH_VALUE||'; END;' USING OUT ts;
IF ts < SYSTIMESTAMP - aPart.RETENTION THEN
BEGIN
ddl('alter table '||aPart.TABLE_NAME||' drop partition '||aPart.partition_name);
EXCEPTION
WHEN CANNOT_DROP_ONLY_ONE_PARTITION THEN
DBMS_OUTPUT.PUT_LINE('Cant drop the only partition '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
ddl('ALTER TABLE '||aPart.TABLE_NAME||' TRUNCATE PARTITION '||aPart.PARTITION_NAME);
WHEN CANNOT_DROP_LAST_PARTITION THEN
BEGIN
DBMS_OUTPUT.PUT_LINE('Drop last partition '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL ()';
ddl('alter table '||aPart.TABLE_NAME||' drop partition '||aPart.partition_name);
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL( '||aPart.INTERVAL||' )';
EXCEPTION
WHEN CANNOT_DROP_ONLY_ONE_PARTITION THEN
-- Depending on the order the "last" partition can be also the "only" partition at the same time
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL( '||aPart.INTERVAL||' )';
DBMS_OUTPUT.PUT_LINE('Cant drop the only partition '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
ddl('ALTER TABLE '||aPart.TABLE_NAME||' TRUNCATE PARTITION '||aPart.PARTITION_NAME);
END;
END;
END IF;
END LOOP;
rebuild_index();
END;
alter table T1 drop partition OLD_DATA
.02 seconds
alter table T1 drop partition P_2022W14
.01 seconds
alter table T1 drop partition P_2022W15
.02 seconds
alter table T1 drop partition P_2022W16
.01 seconds
alter table T1 drop partition P_2022W17
.02 seconds
alter table T1 drop partition P_2022W18
.01 seconds
alter table T1 drop partition P_2022W19
.02 seconds
alter table T1 drop partition P_2022W20
.01 seconds
alter table T1 drop partition P_2022W21
.01 seconds
alter table T1 drop partition P_2022W22
.02 seconds
alter table T1 drop partition P_2022W23
.01 seconds
alter table T1 drop partition P_2022W24
.01 seconds
alter table T1 drop partition P_2022W25
.01 seconds
alter table T1 drop partition P_2022W26
.01 seconds
alter table T1 drop partition P_2022W27
.02 seconds
alter index SQL_WUKYPRGVPTOUVLCAEKUDCRCQI.T1_GLOBAL_IX rebuild parallel 4 online
.1 seconds
…
…
…
alter index SQL_WUKYPRGVPTOUVLCAEKUDCRCQI.T1_GLOBAL_IX rebuild parallel 4 online
.1 seconds
SELECT count(*) from USER_TAB_PARTITIONS
Where
table_name not like 'BIN$%'
8
SELECT PARTITION_NAME
FROM USER_TAB_PARTITIONS
WHERE TABLE_NAME = 'T1'
AND
table_name not like 'BIN$%'
P_2022W28
P_2022W29
P_2022W30
P_2022W31
P_2022W32
P_2022W33
P_2022W34
P_2022W35

How to automate partition drop and alter table activity in oracle sql developer

I need a procedure that will help me to drop partition older than 3 months (current month + last 3 older months). And then I need to create the new partition in sequence.
for example if current partition is May22, then I need to drop Jan22 and create Jan23 partition. Can someone help me with the procedure?
Here is the process we use to RENAME and drop PARTITIONs that are range INTERVAL. Just add the procedures to your scheduler.
They work great
CREATE OR REPLACE PROCEDURE ddl(p_cmd varchar2)
authid current_user
is
t1 pls_integer;
BEGIN
t1 := dbms_utility.get_time;
dbms_output.put_line(p_cmd);
execute immediate p_cmd;
dbms_output.put_line((dbms_utility.get_time - t1)/100 || ' seconds');
END;
/
CREATE TABLE PARTITION_RETENTION (
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
TABLE_NAME VARCHAR2(30),
RETENTION INTERVAL DAY(3) TO SECOND(0),
CONSTRAINT
partition_retention_pk primary key (table_name),
CONSTRAINT CHK_NON_ZERO_DAYS CHECK (
RETENTION > INTERVAL '0' DAY
),
CONSTRAINT CHK_WHOLE_DAYS CHECK (
EXTRACT(HOUR FROM RETENTION) = 0
AND EXTRACT(MINUTE FROM RETENTION) = 0
AND EXTRACT(SECOND FROM RETENTION) = 0
)
);
insert into PARTITION_RETENTION (TABLE_NAME, RETENTION)
select 'T1', interval '10' day from dual union all
select 'T3', interval '15' day from dual union all
select 'T4', 15 * interval '1' day from dual union all
select 'T5', 5 * interval '1 00:00:00' day to second from dual;
CREATE TABLE t1 (
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
dt DATE
)
PARTITION BY RANGE (dt)
INTERVAL (NUMTODSINTERVAL(7,'DAY'))
(
PARTITION OLD_DATA values LESS THAN (TO_DATE('2022-01-01','YYYY-MM-DD'))
);
/
INSERT into t1 (dt)
with dt (dt, interv) as (
select date '2022-01-01', numtodsinterval(1,'DAY') from dual
union all
select dt.dt + interv, interv from dt
where dt.dt + interv < date '2022-02-01')
select dt from dt;
/
create index t1_global_ix on t1 (dt);
/
CREATE OR REPLACE PROCEDURE MaintainPartitions IS EXPRESSION_IS_OF_WRONG_TYPE EXCEPTION;
PRAGMA EXCEPTION_INIT(EXPRESSION_IS_OF_WRONG_TYPE, -6550);
CURSOR PartTables IS
SELECT TABLE_NAME, INTERVAL
FROM USER_PART_TABLES
WHERE PARTITIONING_TYPE = 'RANGE'
ORDER BY TABLE_NAME;
CURSOR TabParts(aTableName VARCHAR2) IS
SELECT PARTITION_NAME, HIGH_VALUE
FROM USER_TAB_PARTITIONS
WHERE regexp_like(partition_name,'^SYS_P[[:digit:]]{1,10}') AND
TABLE_NAME = aTableName AND
table_name not like 'BIN$%'
and interval is not null
ORDER BY PARTITION_POSITION;
ym INTERVAL YEAR TO MONTH;
ds INTERVAL DAY TO SECOND;
newPartName VARCHAR2(30);
PERIOD TIMESTAMP;
BEGIN
FOR aTab IN PartTables LOOP
BEGIN
EXECUTE IMMEDIATE 'BEGIN :ret := '||aTab.INTERVAL||'; END;' USING OUT ds;
ym := NULL;
EXCEPTION
WHEN EXPRESSION_IS_OF_WRONG_TYPE THEN
EXECUTE IMMEDIATE 'BEGIN :ret := '||aTab.INTERVAL||'; END;' USING OUT ym;
ds := NULL;
END;
FOR aPart IN TabParts(aTab.TABLE_NAME) LOOP
EXECUTE IMMEDIATE 'BEGIN :ret := '||aPart.HIGH_VALUE||'; END;' USING OUT PERIOD;
IF ds IS NOT NULL THEN
IF ds >= INTERVAL '7' DAY THEN
-- Weekly partition
EXECUTE IMMEDIATE 'BEGIN :ret := TO_CHAR('||aPart.HIGH_VALUE||' - :int, :fmt); END;' USING OUT newPartName, INTERVAL '1' DAY, '"P_"IYYY"W"IW';
ELSE
-- Daily partition
EXECUTE IMMEDIATE 'BEGIN :ret := TO_CHAR('||aPart.HIGH_VALUE||' - :int, :fmt); END;' USING OUT newPartName, INTERVAL '1' DAY, '"P_"YYYYMMDD';
END IF;
ELSE
IF ym = INTERVAL '3' MONTH THEN
-- Quarterly partition
EXECUTE IMMEDIATE 'BEGIN :ret := TO_CHAR('||aPart.HIGH_VALUE||' - :int, :fmt); END;' USING OUT newPartName, INTERVAL '1' DAY, '"P_"YYYY"Q"Q';
ELSE
-- Monthly partition
EXECUTE IMMEDIATE 'BEGIN :ret := TO_CHAR('||aPart.HIGH_VALUE||' - :int, :fmt); END;' USING OUT newPartName, INTERVAL '1' DAY, '"P_"YYYYMM';
END IF;
END IF;
IF newPartName <> aPart.PARTITION_NAME THEN
EXECUTE IMMEDIATE 'ALTER TABLE '||aTab.TABLE_NAME||' RENAME PARTITION '||aPart.PARTITION_NAME||' TO '||newPartName;
END IF;
END LOOP;
END LOOP;
END MaintainPartitions;
/
CREATE OR REPLACE PROCEDURE rebuild_index
authid current_user
is
BEGIN
for i in (
select index_owner, index_name, partition_name, 'partition' ddl_type
from all_ind_partitions
where status = 'UNUSABLE'
union all
select owner, index_name, null, null
from all_indexes
where status = 'UNUSABLE'
)
loop
if i.ddl_type is null then
ddl('alter index '||i.index_owner||'.'||i.index_name||' rebuild parallel 4 online');
else
ddl('alter index '||i.index_owner||'.'||i.index_name||' modify '||i.ddl_type||' '||i.partition_name||' rebuild parallel 4 online');
end if;
end loop;
END;
/
EXEC MaintainPartitions;
DECLARE
CANNOT_DROP_LAST_PARTITION EXCEPTION;
PRAGMA EXCEPTION_INIT(CANNOT_DROP_LAST_PARTITION, -14758);
CANNOT_DROP_ONLY_ONE_PARTITION EXCEPTION;
PRAGMA EXCEPTION_INIT(CANNOT_DROP_ONLY_ONE_PARTITION, -14083);
ts TIMESTAMP;
CURSOR TablePartitions IS
SELECT TABLE_NAME, PARTITION_NAME, p.HIGH_VALUE, t.INTERVAL, RETENTION, DATA_TYPE
FROM USER_PART_TABLES t
JOIN USER_TAB_PARTITIONS p USING (TABLE_NAME)
JOIN USER_PART_KEY_COLUMNS pk ON pk.NAME = TABLE_NAME
JOIN USER_TAB_COLS tc USING (TABLE_NAME, COLUMN_NAME)
JOIN PARTITION_RETENTION r USING (TABLE_NAME)
WHERE pk.object_type = 'TABLE' AND
t.partitioning_type = 'RANGE' AND
REGEXP_LIKE (tc.data_type, '^DATE$|^TIMESTAMP.*');
BEGIN
FOR aPart IN TablePartitions LOOP
EXECUTE IMMEDIATE 'BEGIN :ret := '||aPart.HIGH_VALUE||'; END;' USING OUT ts;
IF ts < SYSTIMESTAMP - aPart.RETENTION THEN
BEGIN
ddl('alter table '||aPart.TABLE_NAME||' drop partition '||aPart.partition_name);
EXCEPTION
WHEN CANNOT_DROP_ONLY_ONE_PARTITION THEN
DBMS_OUTPUT.PUT_LINE('Cant drop the only partition '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
ddl('ALTER TABLE '||aPart.TABLE_NAME||' TRUNCATE PARTITION '||aPart.PARTITION_NAME);
WHEN CANNOT_DROP_LAST_PARTITION THEN
BEGIN
DBMS_OUTPUT.PUT_LINE('Drop last partition '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL ()';
ddl('alter table '||aPart.TABLE_NAME||' drop partition '||aPart.partition_name);
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL( '||aPart.INTERVAL||' )';
EXCEPTION
WHEN CANNOT_DROP_ONLY_ONE_PARTITION THEN
-- Depending on the order the "last" partition can be also the "only" partition at the same time
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL( '||aPart.INTERVAL||' )';
DBMS_OUTPUT.PUT_LINE('Cant drop the only partition '||aPart.PARTITION_NAME ||' from table '||aPart.TABLE_NAME);
ddl('ALTER TABLE '||aPart.TABLE_NAME||' TRUNCATE PARTITION '||aPart.PARTITION_NAME);
END;
END;
END IF;
END LOOP;
rebuild_index();
END;
Use interval partitioning - available starting from Oracle 11.
You will need no procedure for a creation of a partition, because the partition will be created automatically with the first insert of the data with the corresponding date.
You will still need a basic code for drop partition - rolling window is not supported in the interval partitioning.
Using the partition extended names makes it much easier that if you use partition names
Example
Create Interval Partitioned Table
CREATE TABLE test_part
(trans_date DATE,
pad VARCHAR2(100)
)
PARTITION BY RANGE (trans_date)
INTERVAL (NUMTODSINTERVAL(1,'DAY'))
(
PARTITION part_init values LESS THAN (DATE'2020-01-01')
);
Note - use a less than date long in the history for the initial partition so that no data will be inserted there.
This partition will remain empty and will not be dropped, otherwise you encounter
ORA-14083: cannot drop the only partition of a partitioned table.
Create Partition
Now populate that table with few rows.
Note that there is no need for explicite create partition as we use interval partitioning and the partitions are created automatically.
insert into test_part(trans_date, pad)
select add_months(trunc(sysdate),-3)-rownum trans_date, 'X' pad
from dual connect by level <= 3;
Drop Partition
The implenetation of the rolling window consists of two steps
First check the transactions dates that are older than three months
select distinct trunc(trans_date) drop_date from test_part
where trans_date < add_months(trunc(sysdate),-3)
order by 1 desc;
DROP_DATE
-------------------
08.01.2022 00:00:00
07.01.2022 00:00:00
06.01.2022 00:00:00
Now you need to drop partitions for the above selected days.
You can use the *partition extended names to do so, without a need to know the partition name and the ugly LONG HIGH_VALUE.
Example for the day DATE'2022-07-07'
alter table TEST_PART drop partition for (DATE'2022-07-07');
You may pack the clean up logic in a simple procedure
create or replace procedure rolling_window as
v_sql varchar2(4000);
begin
for cur in (
select distinct trunc(trans_date) drop_date from test_part
where trans_date < add_months(trunc(sysdate),-3)
order by 1 desc)
loop
v_sql := q'[alter table TEST_PART drop partition for (DATE']'||
to_char(cur.drop_date,'YYYY-MM-DD')||q'[') ]';
dbms_output.put_line(v_sql);
execute immediate v_sql;
end loop;
end;
/
If you use global indexes you may want to add the clause UPDATE INDEXES in the ALTER TABLEto keep the index usable after the operation.

Create table with Partition year and subpartition month In Oracle 12c

I'm using Oracle 12c, I want to create a table with partition by year and subpartition by month. When I run the following code, it raise ORA-00922: missing or invalid option error. How can I fix it?
create table Log_his
(
ID NUMBER(20) not null,
Year number(4,0), -- Partition
Monthly number(2,0)), -- Subpartition
Log_reason VARCHAR2(5000),
Log_detail VARCHAR2(5000),
constraint PK_ID primary key (ID)
) PARTITION BY RANGE(Year) INTERVAL (1)
SUBPARTITION BY RANGE(Monthly)
SUBPARTITION TEMPLATE
(
SUBPARTITION January VALUES LESS THAN (02)tablespace MONTHLY_PARTITION,
SUBPARTITION February VALUES LESS THAN (03)tablespace MONTHLY_PARTITION,
SUBPARTITION March VALUES LESS THAN (04)tablespace MONTHLY_PARTITION,
SUBPARTITION April VALUES LESS THAN (05)tablespace MONTHLY_PARTITION,
SUBPARTITION May VALUES LESS THAN (06)tablespace MONTHLY_PARTITION,
SUBPARTITION June VALUES LESS THAN (07)tablespace MONTHLY_PARTITION,
SUBPARTITION July VALUES LESS THAN (08)tablespace MONTHLY_PARTITION,
SUBPARTITION August VALUES LESS THAN (09)tablespace MONTHLY_PARTITION,
SUBPARTITION September VALUES LESS THAN (10)tablespace MONTHLY_PARTITION,
SUBPARTITION October VALUES LESS THAN (11)tablespace MONTHLY_PARTITION,
SUBPARTITION November VALUES LESS THAN (12)tablespace MONTHLY_PARTITION,
SUBPARTITION December VALUES LESS THAN (MAXVALUE)
);
My purpose is create a table which auto create partition each year and It save log in 12 months. If I add one more month, the last month will be deleted and this will be done by daly job. How can I do that?
Thanks!
I would suggest to create simple monthly partition. You can do it like this:
create table Log_his
(
ID NUMBER(20) not null,
Year number(4,0),
Monthly number(2,0)),
Log_reason VARCHAR2(5000),-- Oracle default limit is VARCHAR2(4000)
Log_detail VARCHAR2(5000),
PARTITION_KEY TIMESTAMP(0) GENERATED ALWAYS AS (TO_TIMESTAMP(year||'-'||Monthly, 'YYYY-MM')) VIRTUAL,
constraint PK_ID primary key (ID)
)
PARTITION BY RANGE (PARTITION_KEY) INTERVAL (INTERVAL '1' MONTH)
(PARTITION P_INITIAL VALUES LESS THAN (TIMESTAMP '2018-01-01 00:00:00'));
New partitions will be added automatically. For maintenance you can create a procedure like below. You can run it by a daily job.
CREATE OR REPLACE PROCEDURE MaintainPartitions IS
CANNOT_DROP_LAST_PARTITION EXCEPTION;
PRAGMA EXCEPTION_INIT(CANNOT_DROP_LAST_PARTITION, -14758);
sqlstr VARCHAR2(10000);
ts TIMESTAMP;
newName VARCHAR2(30);
CURSOR TabPartitions IS
SELECT TABLE_NAME, PARTITION_NAME, HIGH_VALUE
FROM USER_TAB_PARTITIONS t
WHERE TABLE_NAME = 'LOG_HIS'
ORDER BY TABLE_NAME, PARTITION_POSITION;
BEGIN
FOR aPart IN TabPartitions LOOP
EXECUTE IMMEDIATE 'BEGIN :ret := '||aPart.HIGH_VALUE||'; END;' USING OUT ts;
ts := ADD_MONTHS(ts, -1);
newName := 'P_'||TO_CHAR(ts,'yyyy_fmmonth');
IF aPart.PARTITION_NAME <> newName THEN
sqlstr := 'ALTER TABLE '||aPart.TABLE_NAME||' RENAME PARTITION '||aPart.PARTITION_NAME||' TO '||newName;
EXECUTE IMMEDIATE sqlstr;
END IF;
END LOOP;
FOR aPart IN TabPartitions LOOP
BEGIN
EXECUTE IMMEDIATE 'BEGIN :ret := '||aPart.HIGH_VALUE||'; END;' USING OUT ts;
ts := ts - INTERVAL '1' DAY;
IF ts < ADD_MONTHS(SYSDATE, -12) THEN
sqlstr := 'ALTER TABLE '||aPart.TABLE_NAME||' DROP PARTITION '||aPart.PARTITION_NAME||' UPDATE GLOBAL INDEXES';
EXECUTE IMMEDIATE sqlstr;
END IF;
EXCEPTION
WHEN CANNOT_DROP_LAST_PARTITION THEN
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL ()';
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' DROP PARTITION ('||aPart.PARTITION_NAME||') UPDATE GLOBAL INDEXES';
EXECUTE IMMEDIATE 'ALTER TABLE '||aPart.TABLE_NAME||' SET INTERVAL (INTERVAL ''1'' MONTH)';
END;
END LOOP;
END MaintainPartitions;

How to drop multiple interval partitions based on date?

I have a table based on daily partitions.
I can drop a paritition using the below query
ALTER TABLE MY_TABLE DROP PARTITION FOR(TO_DATE('19-DEC-2017','dd-MON-yyyy'))
How can I drop all the partitions (multiple partitions) before 15 days?
You can use PL/SQL like this.
DECLARE
CANNOT_DROP_LAST_PARTITION EXCEPTION;
PRAGMA EXCEPTION_INIT(CANNOT_DROP_LAST_PARTITION, -14758);
ts TIMESTAMP;
BEGIN
FOR aPart IN (SELECT PARTITION_NAME, HIGH_VALUE FROM USER_TAB_PARTITIONS WHERE TABLE_NAME = 'MY_TABLE') LOOP
EXECUTE IMMEDIATE 'BEGIN :ret := '||aPart.HIGH_VALUE||'; END;' USING OUT ts;
IF ts < SYSTIMESTAMP - INTERVAL '15' DAY THEN
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE MY_TABLE DROP PARTITION '||aPart.PARTITION_NAME|| ' UPDATE GLOBAL INDEXES';
EXCEPTION
WHEN CANNOT_DROP_LAST_PARTITION THEN
EXECUTE IMMEDIATE 'ALTER TABLE MY_TABLE SET INTERVAL ()';
EXECUTE IMMEDIATE 'ALTER TABLE MY_TABLE DROP PARTITION '||aPart.PARTITION_NAME;
EXECUTE IMMEDIATE 'ALTER TABLE MY_TABLE SET INTERVAL( INTERVAL ''1'' DAY )';
END;
END IF;
END LOOP;
END;
For interval partitioned tables (that you probably use based on the exception ORA-14758) you may profit from using the partition_extended_name syntax.
You need not know the partition name, you reference the partition with a DATE, e.g.
alter table INT_PART drop partition for (DATE'2018-09-01')
So to drop your last 15 partitions starting with the current day this loop is to be performed:
declare
v_sql VARCHAR2(4000);
begin
for cur in (select
trunc(sysdate,'MM') - numtodsinterval(rownum - 1, 'day') my_month
from dual connect by level <= 15)
loop
v_sql := q'[alter table INT_PART drop partition for (DATE']'||
to_char(cur.my_month,'YYYY-MM-DD')||q'[')]';
execute immediate v_sql;
end loop;
end;
/
You must use execute immediateas the DATEin the ALTER TABLE statement must by static.
Following statements are generated and executed:
alter table INT_PART drop partition for (DATE'2018-09-01')
alter table INT_PART drop partition for (DATE'2018-08-31')
....
alter table INT_PART drop partition for (DATE'2018-08-19')
alter table INT_PART drop partition for (DATE'2018-08-18')
Additional to the exception ORA-14758 (that I ignore - see the note below) you should handle the exception
ORA-02149: Specified partition does not exist
dependent on you business this should be probably ignored - for this day no partition exists (and you will never reference this day using the partition dictionary metadata).
Note to workaround the ORA-14758 Last partition in the range section cannot be dropped exception you may use this little trick.
I create a dummy partition (without an extent) called P_MINVALUE that plays the role of the interval start in the far past and it will therefore never be dropped.
CREATE TABLE int_part
(
transaction_date TIMESTAMP not null,
vc_pad VARCHAR2(100)
)
SEGMENT CREATION DEFERRED
PARTITION BY RANGE (transaction_date) INTERVAL (NUMTODSINTERVAL(1,'DAY'))
(
PARTITION P_MINVALUE VALUES LESS THAN (TO_DATE('2000-01-01', 'YYYY-MM-DD') )
);

Exchange/Move partitions between tables

I have this table as my main table (100M rows):
create table prova_log(
id_dispositive number,
type number,
date_verification date,
status number
)
partition by range (date_verification) interval (NUMTODSINTERVAL(3,'DAY'))
subpartition by list (type)
subpartition TEMPLATE (
SUBPARTITION type1 VALUES (1),
SUBPARTITION type2 VALUES (2),
SUBPARTITION type3 VALUES (3),
SUBPARTITION type4 VALUES (4)
)
(
partition p0816 values less than (to_date('01/09/2016','dd/mm/yyyy'))
);
And I want to make some sort of backup with older values, so I created this (0 rows):
create table prova_log_old (
id_dispositive number,
type number,
date_verification date,
status number
)
partition by range (date_verification) interval (NUMTODSINTERVAL(3,'DAY'))
subpartition by list (type)
subpartition TEMPLATE (
SUBPARTITION type1 VALUES (1),
SUBPARTITION type2 VALUES (2),
SUBPARTITION type3 VALUES (3),
SUBPARTITION type4 VALUES (4)
)
(
partition p_old values less than (to_date('01/09/2016','dd/mm/yyyy'))
);
So I want to move/copy/exchange (whatever term) old partitions (15 days+) to prova_log_old.
To do so I've created this job:
PROCEDURE move_data_from_huge_table
IS
-- This will move all data after 'vcountdaystokeepdata' days
vcountdaystokeepdata NUMBER := 15;
vcountdatainsidepartition NUMBER := 0;
BEGIN
FOR item IN
(SELECT *
FROM (SELECT partition_name,
TO_DATE
(TRIM
('''' FROM REGEXP_SUBSTR
(EXTRACTVALUE
(DBMS_XMLGEN.getxmltype
( 'select high_value from all_tab_partitions where table_name='''
|| table_name
|| ''' and table_owner = '''
|| table_owner
|| ''' and partition_name = '''
|| partition_name
|| ''''
),
'//text()'
),
'''.*?'''
)
),
'syyyy-mm-dd hh24:mi:ss'
) high_value
FROM all_tab_partitions
WHERE table_name = 'PROVA_LOG')
WHERE high_value < SYSDATE - vcountdaystokeepdata)
LOOP
EXECUTE IMMEDIATE 'alter table PROVA_LOG EXCHANGE PARTITION '
|| item.partition_name
|| ' with table PROVA_LOG_OLD';
EXECUTE IMMEDIATE 'select count(*) from PROVA_LOG partition ('
|| item.partition_name
|| ')'
INTO vcountdatainsidepartition;
IF vcountdatainsidepartition = 0
THEN
EXECUTE IMMEDIATE 'ALTER TABLE PROVA_LOG DROP PARTITION '
|| item.partition_name
|| '';
END IF;
END LOOP;
END;
But when I run the procedure I got
ORA-14292 partitioning type of table must match subpartitioning type of composite partition
I assume that I must have a partition in my backup table with the same name as my main partitioned table right?
How can I make this work?
I tried to add a partition to my backup table but without success..It's important to mention that all partition's name are random (oracle generates it).
I still don't understand why you want to move the partitions, anyway I have a solution.
First of all, you can address a partition either like
SELECT COUNT(*) FROM PROVA_LOG PARTITION (SYS_P7138);
or you can do it as
SELECT COUNT(*) FROM PROVA_LOG PARTITION FOR (TO_DATE('2016-10-01', 'YYYY-MM-DD'));
or if you prefer DATE literals
SELECT COUNT(*) FROM PROVA_LOG PARTITION FOR (DATE '2016-10-01');
An automatic solution for you problem could be this one:
DECLARE
CURSOR TabPartitions IS
SELECT TABLE_NAME, PARTITION_NAME, HIGH_VALUE
FROM USER_TAB_PARTITIONS
WHERE TABLE_NAME = 'PROVA_LOG'
ORDER BY 1,2;
ts DATE;
BEGIN
FOR aPart IN TabPartitions LOOP
EXECUTE IMMEDIATE 'BEGIN :ret := '||aPart.HIGH_VALUE||'; END;' USING OUT ts;
IF ts <> DATE '2016-09-10' AND ts < SYSDATE - 15 THEN
--EXECUTE IMMEDIATE 'INSERT INTO PROVA_LOG_OLD SELECT * FROM PROVA_LOG PARTITION FOR (DATE '''||TO_CHAR(ts, 'yyyy-mm-dd')||''')';
--EXECUTE IMMEDIATE 'ALTER TABLE PROVA_LOG DROP PARTITION FOR (DATE '''||TO_CHAR(ts, 'yyyy-mm-dd')||''') UPDATE GLOBAL INDEXES';
EXECUTE IMMEDIATE 'ALTER TABLE PROVA_LOG EXCHANGE PARTITION FOR (DATE '''||TO_CHAR(ts, 'yyyy-mm-dd')||''') WITH TABLE PROVA_LOG_OLD INCLUDING INDEXES';
END IF;
END LOOP;
END;
Your backup table must be like this:
CREATE TABLE prova_log_old (
id_dispositive NUMBER,
TYPE NUMBER,
date_verification DATE,
status NUMBER
)
PARTITION BY LIST (TYPE)
(
PARTITION type1 VALUES (1),
PARTITION type2 VALUES (2),
PARTITION type3 VALUES (3),
PARTITION type4 VALUES (4)
);
or no partitioning at all
CREATE TABLE prova_log_old (
id_dispositive NUMBER,
TYPE NUMBER,
date_verification DATE,
status NUMBER
);
You are doing it wrong. You exchange partition with all partitioned table not with it partition, just look one more at your code
EXECUTE IMMEDIATE 'alter table PROVA_LOG EXCHANGE PARTITION '
|| item.partition_name
|| ' with table PROVA_LOG_OLD';
In case of exchange partition you should do as follows
Create empty table without partition with same structure like PROVA_LOG but not partitioned.
Exchange partition in production table with new_table
Exchange partition in hist table with new_table

Resources