remove table fragmentation in database ORACLE - oracle

how we can remove table fragmentation when database in product ?
thank you

The only function of shrink is reclaiming space in a table. It recovers space above and below the high water mark.
To do this you need to have row movement enabled.
create table t as
select rownum x, lpad('x', 500, 'x') xx from dual connect by level <= 10000;
/
-- ALTER TABLE t SHRINK SPACE CHECK;
select bytes from user_segments
where segment_name = 'T';
/
delete t where x <= 9900;
/
select bytes from user_segments
where segment_name = 'T';
/
alter table t enable row movement;
/
alter table t shrink space CASCADE;
/
select bytes from user_segments
where segment_name = 'T';

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

Oracle Dynamic Query for all tables

I have two queries,
first one to get the table name and avg_row_len
select Table_name, AVG_ROW_LEN from all_tables
where Table_name = 'COMPU_ERROR_FACT';
and then i need to pass the avg_row_len to
SELECT COUNT(1) as "Number of Count",
ROUND(COUNT(1) * avg_row_len/1024/1024) AS "Size in MB" ,
TO_CHAR(CREATE_DATE, 'YYYY') AS YEAR,
'CREATE_DATE' AS DATTT
FROM COMPU_ERROR_FACT --table name
GROUP BY TO_CHAR(CREATE_DATE, 'YYYY')
order by 3;
I have more than 2k tables and is there a way to get the details of all tables at one-time execution.
Please suggest,
Not sure if you need just a query or a PLSQL block.
So, if you need a query, you may build it with the following PLSQL code. Run it and run huge sql from the output. Don't forget to remove trailing "union all"
begin
for t in (select * from all_tables) loop
dbms_output.put_line('select count(1) "number of count", ROUND(COUNT(1) * avg_row_len/1024/1024) "Size in MB", TO_CHAR(CREATE_DATE, ''YYYY'') AS YEAR, ''CREATE_DATE'' AS DATTT
FROM t.table_name
GROUP BY TO_CHAR(CREATE_DATE, ''YYYY'')
order by 3');
dbms_output.put_line('union all');
end loop;
end;
In case if PLSQL is acceptable you may use this
declare
cnt number;
avg_len number;
create_year varchar2(4);
dattt varchar(10); -- in youe example it will get constatly populated with 'CREATE_DATE', check if you need it at all
begin
for t in (select * from all_tables) loop
execute immediate 'select count(1) "number of count", ROUND(COUNT(1) * avg_row_len/1024/1024) "Size in MB", TO_CHAR(CREATE_DATE, ''YYYY'') AS YEAR, ''CREATE_DATE'' AS DATTT
FROM ' ||t.owner || '.' || t.table_name ||
' GROUP BY TO_CHAR(CREATE_DATE, ''YYYY'')
order by 3'
into cnt, avg_len, create_year, dattt;
-- do something with data you've just selected
end loop;
end;
And please note that all_tables has names of all tables you have access to. So there will be system tables and tables from another schemas. this is why I added "t.owner".
Perhaps you need user_tables view which has names of objects your user has created only. Check if using of "user_tables" system view will do better job here.
FYI: table size in MB may be obtained from dba_segments table:
select bytes / 1024 / 1024 from dba_segments where segment_name = 'TABLE_NAME'

dropping partitioned tables with global indexes?

PROCEDURE purge_partitions
(
p_owner IN VARCHAR2
,p_name IN VARCHAR2
,p_retention_period IN NUMBER
) IS
BEGIN
FOR partition_rec IN (SELECT partition_name
,high_value
FROM dba_tab_partitions
WHERE table_owner = p_owner
AND table_name = p_name)
LOOP
IF SYSDATE >= add_months(to_date(substr(partition_rec.high_value
,12
,19)
,'YYYY-MM-DD HH24:MI:SS')
,p_retention_period)
THEN
execute_immediate('ALTER TABLE ' || p_owner || '.' ||
p_name || ' DROP PARTITION ' ||
partition_rec.partition_name)
END IF;
END LOOP;
END purge_partitions;
Purge_Partitions procedure deals with dropping partitions based on
specific retention priods mentioned in a seperate Config Table. I am
now trying to enhance this functionality which will take care of
rebuilding global indexes of those partitioned tables. Not sure how to go about this, any help is highly appreciated.
Consider the update_index_clauses, which keeps the indexes valid.
See the documentation and consideration for global indexes here
in your case it will be:
alter table ttttt drop partition pppppp update global indexes;
Alternatively let the indices be invalidated in the DROP PARTITION and rebuild them with alter index xxx rebuild. You can get the list of indexes to rebuild from this query
select OWNER, INDEX_NAME
from all_indexes
where owner = 'ooo' and table_name = 'tttttt' and status = 'UNUSABLE';

ORA-30036: unable to extend segment by 8 in undo tablespace 'UNDOTBS'

I am running cron job which have following PL/SQL block:
declare
begin
--insert into DB_LOGS_TEMP table from DB_LOGS table
INSERT INTO DB_LOGS_TEMP SELECT * FROM DB_LOGS WHERE DB_LOG_ID NOT IN(SELECT DB_LOG_ID from DB_LOGS_TEMP );
--keep the lat 10 records and delete other records
DELETE DB_LOGS where rowid in (
select rid from (
select t.rowid rid,
row_number() over(partition by T.DB_LOG_ID order by T.TIMESTAMP desc) as rn
from DB_LOGS t)
where rn > 10);
end;
The DB_LOGS table has 10247302 rows. When the cron job run it throws an error as ORA-30036: unable to extend segment by 8 in undo tablespace 'UNDOTBS'. Does increasing the tablespce is the only solution for this issue and how to do that? The UNDOTBS has 524288000 bytes.
It works for me while increasing the tablespace and making autoextend on.
ALTER DATABASE DATAFILE '/vg01lv11/oracle//data/undotbs_d1_O2P00R11.dbf' AUTOEXTEND ON MAXSIZE 10g;
ALTER DATABASE DATAFILE '/vg01lv11/oracle//data/undotbs_d1_O2P00R11.dbf'
RESIZE 1000M;
If you can afford deleting in different transactions:
DECLARE
i PLS_INTEGER;
BEGIN
--insert into DB_LOGS_TEMP table from DB_LOGS table
INSERT INTO DB_LOGS_TEMP
SELECT *
FROM DB_LOGS
WHERE DB_LOG_ID NOT IN
(SELECT DB_LOG_ID FROM DB_LOGS_TEMP
);
COMMIT;
i:=50;
--keep the lat 10 records and delete other records
WHILE i>=10
LOOP
DELETE DB_LOGS
WHERE rowid IN
(SELECT rid
FROM
(SELECT t.rowid rid,
row_number() over(partition BY T.DB_LOG_ID order by T.TIMESTAMP DESC) AS rn
FROM DB_LOGS t
)
WHERE rn > i
);
COMMIT;
i:=i-5;
END LOOP;
END;

Extra entries in recycle bin when table dropped

I have created a table TESTTABLE28.
Then I dropped the table.DROP TABLE TESTTABLE28;
The show parameter recyclebin; value was set to on.
When I checked the Recycle Bin, observed some other entries along with TESTTABLE28 are listed,
Once I flashback table TESTTABLE28 to before drop; to restore the table, these extra entries are also moved.
What is this values and why it didn't come, when I dropped another table TESTTABLE31 which has only one varchar2 column?
UPDATE
Also how can I restore the constraints which is showing BIN$5lpccCurTNWwCbOSxCK29w==$1 now instead of TESTTABLE28_PK.
Because you have LOB columns in your table you also have segments and indices to them:
SQL> create table t_lb (x int, y clob, z blob);
SQL> col column_name format a10
SQL> col segment_name format a30
SQL> col index_name format a30
SQL> select column_name, segment_name, index_name from user_lobs
2 where table_name = 'T_LB'
3 /
COLUMN_NAM SEGMENT_NAME INDEX_NAME
---------- ------------------------------ ------------------------------
Y SYS_LOB0000701495C00002$$ SYS_IL0000701495C00002$$
Z SYS_LOB0000701495C00003$$ SYS_IL0000701495C00003$$
SQL> show parameter recyclebin
NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
recyclebin string ON
SQL> drop table t_lb;
SQL> col object_name format a30
SQL> col type format a30
SQL> select object_name, type from recyclebin
2 /
OBJECT_NAME TYPE
------------------------------ ------------------------------
BIN$8hw9X/AeBmngQ0QTGKxsAQ==$0 TABLE
SYS_LOB0000701495C00003$$ LOB
SYS_LOB0000701495C00002$$ LOB
SYS_IL0000701495C00003$$ LOB INDEX
SYS_IL0000701495C00002$$ LOB INDEX
As of constraint/index restoring - you should rename them back:
SQL> create table t (x int, constraint t_pk primary key(x))
2 /
SQL> drop table t;
SQL> select object_name, type from recyclebin;
OBJECT_NAME TYPE
------------------------------ ------------------------------
BIN$8hw9X/AiBmngQ0QTGKxsAQ==$0 TABLE
BIN$8hw9X/AhBmngQ0QTGKxsAQ==$0 INDEX
SQL> flashback table t to before drop;
SQL> select constraint_name, index_name, constraint_type from
2 user_constraints where table_name = 'T';
CONSTRAINT_NAME INDEX_NAME C
------------------------------ ------------------------------ -
BIN$8hw9X/AgBmngQ0QTGKxsAQ==$0 BIN$8hw9X/AhBmngQ0QTGKxsAQ==$0 P
SQL> begin
2 for cur in (select constraint_name, index_name
3 from user_constraints where table_name = 'T'
4 and constraint_type = 'P') loop
5 execute immediate
6 'alter table t rename constraint "'||cur.constraint_name||
7 '" to t_pk';
8 execute immediate
9 'alter index "'||cur.index_name||'" rename to t_pk';
10 end loop;
11 end;
12 /
SQL> select constraint_name, index_name from user_constraints
2 where table_name = 'T';
CONSTRAINT_NAME INDEX_NAME
------------------------------ ------------------------------
T_PK T_PK

Resources