Pages

Wednesday, December 24, 2014

Partition an existing Table with DBMS_REDEFINITION, General.

=============================
General
=============================
Example of partitioning existing table using 
DBMS_REDEFINITION package.

DBMS_REDEFINITION package was introduced in Oracle 9i.
This would be the easiest was to partition an existing table.

The implementation is using Materialized Views and MV Log tables.
The inetrim table is manually created. This table would be dropped at the end of the process.
Until the transition is completed Oracle is tracking ongoing DML operation with MV Logs.

Oracle DBMS_REDEFINITION Reference.
Orafaq DBMS_REDEFINITION Reference

=============================
General steps to partition existing table with DBMS_REDEFINITION
=============================

A. Check is the redefinition is possible 
DBMS_REDEFINITION.can_redef_table

B. Manually create a new partitioned table, same as original table.
     This would be a temporary table, that would be eventually dropped.

C. Start the redefinition 
DBMS_REDEFINITION.start_redef_table

D. Copy dependent objects
DBMS_REDEFINITION.copy_table_dependents
An alternative to this step is to manually create Constraints and Indexes on the new table.

E. Check for errors.
Query DBA_REDEFINITION_ERRORS

F. Synchronize data between old and new tables
DBMS_REDEFINITION.sync_interim_table

G. Complete the Redefinition Process
DBMS_REDEFINITION.finish_redef_table

At this point the partitioned table has become the "real" table and their names have been switched in the data dictionary.

H. Drop the interim table.
DROP TABLE ... PURGE;

J. Gather statistics on the new table.
EXEC DBMS_STATS.gather_table_stats

=============================
General example of using  DBMS_REDEFINITION to partition existing table.
=============================

A. Check if the redefinition is possible
BEGIN
  DBMS_REDEFINITION.CAN_REDEF_TABLE ('MY_USER', 'MONTHLY_CALLS_AGG', DBMS_REDEFINITION.CONS_USE_PK);
END;
/
PL/SQL procedure successfully completed


If the table has no PK, following error is raised:
BEGIN
  DBMS_REDEFINITION.can_redef_table ('MANAGER', 'STATEMENTS_AUDIT', DBMS_REDEFINITION.CONS_USE_PK);
END;
/
ORA-12089: cannot online redefine table "MANAGER"."STATEMENTS_AUDIT" with no primary key
ORA-06512: at "SYS.DBMS_REDEFINITION", line 115
ORA-06512: at "SYS.DBMS_REDEFINITION", line 1136

ORA-06512: at line 3

Note:
The redefinition process can be based on a Primary Key or ROWID
Two constants are defined for this purpose:
- DBMS_REDEFINITION.CONS_USE_PK (the default) DBMS_REDEFINITION.CONS_USE_ROWID. 
This is required so Oracle will know how to create the mview log: WITH ROWID, or WITH PRIMARY KEY.

B. Manually create a new partitioned table, same as original table.

CREATE TABLE MY_USER.MONTHLY_CALLS_AGG_INTERIM
partition by RANGE(year)
(PARTITION P_1012 VALUES LESS THAN(2013),
 PARTITION P_1013 VALUES LESS THAN(2014),
 PARTITION P_1014 VALUES LESS THAN(2015),
 PARTITION P_1015 VALUES LESS THAN(2016)
 )
AS SELECT * FROM MY_USER.MONTHLY_CALLS_AGG
  WHERE 1=2;

Table created

C. Start the redefinition 
BEGIN
  DBMS_REDEFINITION.START_REDEF_TABLE('MY_USER', 'MONTHLY_CALLS_AGG', 'MONTHLY_CALLS_AGG_INTERIM');
END;
/
PL/SQL procedure successfully completed


D. Copy dependent objects
DECLARE
  num_errors PLS_INTEGER;
BEGIN
  DBMS_REDEFINITION.copy_table_dependents ('MY_USER', 'MONTHLY_CALLS_AGG', 'MONTHLY_CALLS_AGG_INTERIM', DBMS_REDEFINITION.CONS_ORIG_PARAMS, TRUE, TRUE, TRUE, TRUE, num_errors);
END;
/
PL/SQL procedure successfully completed

E. Check for errors.
SELECT object_name, base_table_name, ddl_txt 
FROM DBA_REDEFINITION_ERRORS;

F. Synchronize data between old and new tables
BEGIN
  DBMS_REDEFINITION.SYNC_INTERIM_TABLE ('MY_USER', 'MONTHLY_CALLS_AGG', 'MONTHLY_CALLS_AGG_INTERIM');
END;
/
PL/SQL procedure successfully completed


G. Complete the Redefintion Process
BEGIN
  DBMS_REDEFINITION.FINISH_REDEF_TABLE ('MY_USER', 'MONTHLY_CALLS_AGG', 'MONTHLY_CALLS_AGG_INTERIM');
END;
/
PL/SQL procedure successfully completed


H. Drop the interim table.
DROP TABLE MY_USER.MONTHLY_CALLS_AGG_INTERIM CASCADE CONSTRAINTS PURGE;

CASCADE CONSTRAINTS - Would drop all referential integrity constraints that refer to
                                                   primary and unique keys in the dropped table.
PURGE - Using PURGE is equivalent to first dropping the table and then purging it from
                the recycle bin. 
                You cannot recover the table if you have dropped it with the PURGE clause.


J. Gather statistics on the new partitioned table.
BEGIN
  DBMS_STATS.GATHER_TABLE_STATS (ownname =>  'MY_USER', tabname => 'MONTHLY_CALLS_AGG');
END;
/
PL/SQL procedure successfully completed

=============================
Now check the table, after it was partitioned.
=============================
SELECT * FROM USER_TAB_PARTITIONS WHERE TABLE_NAME = 'MONTHLY_CALLS_AGG'

========================
Changing Tablespace
========================
1. Table
Notice that the new table is on user default tablespace, because when creating the new MONTHLY_CALLS_AGG_INTERIM table, tablespace clause was omitted.
It was moved from MONTHLY_CALLS_AGG_TB to DEFAULT_TABLESPACE

2. Index
The Primary Key unique index in not Local, as might be expected.

The Primary Key unique index tablespace was not changed during 
the DBMS_REDEFINITION.copy_table_dependents command.
It is still on MY_USER_MONTHLY_AGG_IX tablespace.

========================
Changing Table Tablespace

========================
Lets move the table MONTHLY_CALLS_AGG from DEFAULT_TABLESPACE back to it original tablespace MONTHLY_CALLS_AGG_TB.


Option A. 
ALTER TABLE XXX MOVE TABLESPACE

ALTER TABLE MONTHLY_CALLS_AGG MOVE TABLESPACE MONTHLY_CALLS_AGG_TB

ORA-14511: cannot perform operation on a partitioned object

Option B. 
ALTER TABLE XXX MOVE PARTITION

Move partition by partition, from tablespace A to tablespace B.
Need to generate scripts, to move tablespace per partition.

set heading off
set linesize 200
SELECT 'ALTER TABLE '||TABLE_OWNER ||'.'||TABLE_NAME||' MOVE PARTITION '||partition_name||' TABLESPACE MONTHLY_AGG_TB NOLOGGING;' 
FROM  DBA_TAB_PARTITIONS 
WHERE table_owner = 'MY_USER'
  AND table_name = 'MONTHLY_CALLS_AGG';

ALTER TABLE MY_USER.MONTHLY_CALLS_AGG MOVE PARTITION P_1012 TABLESPACE MONTHLY_AGG_TB NOLOGGING;
ALTER TABLE MY_USER.MONTHLY_CALLS_AGG MOVE PARTITION P_1013 TABLESPACE MONTHLY_AGG_TB NOLOGGING;
ALTER TABLE MY_USER.MONTHLY_CALLS_AGG MOVE PARTITION P_1014 TABLESPACE MONTHLY_AGG_TB NOLOGGING;
ALTER TABLE MY_USER.MONTHLY_CALLS_AGG MOVE PARTITION P_1015 TABLESPACE MONTHLY_AGG_TB NOLOGGING;

Now execute the generated commands.
ALTER TABLE MY_USER.MONTHLY_CALLS_AGG MOVE PARTITION P_1012 TABLESPACE MONTHLY_AGG_TB NOLOGGING;
ALTER TABLE MY_USER.MONTHLY_CALLS_AGG MOVE PARTITION P_1013 TABLESPACE MONTHLY_AGG_TB NOLOGGING;
ALTER TABLE MY_USER.MONTHLY_CALLS_AGG MOVE PARTITION P_1014 TABLESPACE MONTHLY_AGG_TB NOLOGGING;
ALTER TABLE MY_USER.MONTHLY_CALLS_AGG MOVE PARTITION P_1015 TABLESPACE MONTHLY_AGG_TB NOLOGGING;
Table altered


Check current situation

SELECT table_owner, table_name, partition_name, tablespace_name, num_rows 
  FROM DBA_TAB_PARTITIONS 
 WHERE table_owner = 'MY_USER'
   AND table_name = 'MONTHLY_CALLS_AGG';
  
TABLE_OWNER TABLE_NAME          PARTITION_NAME  TABLESPACE_NAME        NUM_ROWS
----------- ------------------- --------------- -------------------- ----------
DWNG        MONTHLY_CALLS_AGG   P_1012          DWNG_MONTHLY_AGG_TB     1542881
DWNG        MONTHLY_CALLS_AGG   P_1013          DWNG_MONTHLY_AGG_TB      782200
DWNG        MONTHLY_CALLS_AGG   P_1014          DWNG_MONTHLY_AGG_TB      744540
DWNG        MONTHLY_CALLS_AGG   P_1015          DWNG_MONTHLY_AGG_TB       58696


Looks like first partition need to be split into 2012 and 2011.


========================
Split Existing Partition
========================

First try - Add a new partition with values less than 2012.
ALTER TABLE MY_USER.MONTHLY_CALLS_AGG
  ADD PARTITION P_1011 VALUES LESS THAN ( 2012 )
      TABLESPACE DWNG_MONTHLY_AGG_TB;   
ORA-14074: partition bound must collate higher than that of the last partition


Second try - Split existing partition into two.
ALTER TABLE MY_USER.MONTHLY_CALLS_AGG SPLIT PARTITION P_1012 AT (2012) INTO (PARTITION P_1011, PARTITION P_1012A);   
Table altered

ALTER TABLE MY_USER.MONTHLY_CALLS_AGG RENAME PARTITION P_1012A TO P_1012  
Table altered


SELECT table_owner, table_name, partition_name, tablespace_name, num_rows 
  FROM DBA_TAB_PARTITIONS 
 WHERE table_owner = 'MY_USER'
   AND table_name = 'MONTHLY_CALLS_AGG';
  
TABLE_OWNER TABLE_NAME          PARTITION_NAME  TABLESPACE_NAME        NUM_ROWS
----------- ------------------- --------------- -------------------- ----------
DWNG        MONTHLY_CALLS_AGG   P_1011          DWNG_MONTHLY_AGG_TB     
DWNG        MONTHLY_CALLS_AGG   P_1012          DWNG_MONTHLY_AGG_TB     
DWNG        MONTHLY_CALLS_AGG   P_1013          DWNG_MONTHLY_AGG_TB      782200
DWNG        MONTHLY_CALLS_AGG   P_1014          DWNG_MONTHLY_AGG_TB      744540
DWNG        MONTHLY_CALLS_AGG   P_1015          DWNG_MONTHLY_AGG_TB       58696

Need to gather statistics to get the num_rows value.

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS (ownname => 'MY_USER', tabname => 'MONTHLY_CALLS_AGG');

END;
ORA-20000: index "MY_USER"."MONTHLY_CALLS_AGG_PK" or partition of such index is in unusable state
ORA-06512: at "SYS.DBMS_STATS", line 12639
ORA-06512: at "SYS.DBMS_STATS", line 12658

ORA-06512: at line 3

ALTER INDEX MY_USER.MONTHLY_CALLS_AGG_PK REBUILD PARTITION P_1011
ORA-14075: partition maintenance operations may only be performed on partitioned indices

ALTER INDEX MY_USER.MONTHLY_CALLS_AGG_PK REBUILD GLOBAL INDEX
ORA-14075: partition maintenance operations may only be performed on partitioned indices

From the error, it looks like the index on the Partitioned table, is not partitioned, neither Local or Global.

ALTER INDEX DWNG.MONTHLY_CALLS_AGG_PK REBUILD ONLINE;

Index altered

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS (ownname => 'MY_USER', tabname => 'MONTHLY_CALLS_AGG');

END;
PL/SQL procedure successfully completed


SELECT table_owner, table_name, partition_name, tablespace_name, num_rows 
  FROM DBA_TAB_PARTITIONS 
 WHERE table_owner = 'MY_USER'
   AND table_name = 'MONTHLY_CALLS_AGG';
  
TABLE_OWNER TABLE_NAME          PARTITION_NAME  TABLESPACE_NAME        NUM_ROWS
----------- ------------------- --------------- -------------------- ----------
DWNG        MONTHLY_CALLS_AGG   P_1011          DWNG_MONTHLY_AGG_TB      756885   
DWNG        MONTHLY_CALLS_AGG   P_1012          DWNG_MONTHLY_AGG_TB      777009
DWNG        MONTHLY_CALLS_AGG   P_1013          DWNG_MONTHLY_AGG_TB      785961
DWNG        MONTHLY_CALLS_AGG   P_1014          DWNG_MONTHLY_AGG_TB      754797
DWNG        MONTHLY_CALLS_AGG   P_1015          DWNG_MONTHLY_AGG_TB       58347

Look at the numbers under num_rows.
A. Now there are values for the P_2011 and P_2012 partitions.
B. The numbers for other partitions slightly differ, comparing to previous values, DBMS_STATS.GATHER_TABLE_STATS was run only few minutes ago, with no DML activity on the table.
   For example, for P_2013, 785961 vs 782200



========================
Transferring Index to a partitioned state.
========================
There is no way to transfer existing index from a non partitioned state to a partitioned state.
The only option is to drop and re-create the index with LOCAL clause.

How to check if index is Global or Local?
All indexes are listed in DBA_INDEXES
Partitioned indexes appear also in DBA_PART_INDEXES.

Query DBA_TABLES, DBA_PART_TABLES , DBA_INDEXES, DBA_PART_INDEXES, USER_SEGMENTS.
  
SELECT owner, table_name, tablespace_name, partitioned 
FROM DBA_TABLES 
WHERE table_name = 'MONTHLY_CALLS_AGG';

OWNER         TABLE_NAME           TABLESPACE_NAME      PARTITIONED
------------- -------------------- -------------------- -----------
DWNG          MONTHLY_CALLS_AGG                         YES

  
SELECT owner, table_name, partitioning_type 
FROM DBA_PART_TABLES 
WHERE table_name = 'MONTHLY_CALLS_AGG';

OWNER         TABLE_NAME           PARTITIONING_TYPE
------------- -------------------- ----------------------------
DWNG          MONTHLY_CALLS_AGG    RANGE


SELECT segment_name, partition_name, segment_type, tablespace_name 
FROM USER_SEGMENTS 
WHERE segment_name = 'MONTHLY_CALLS_AGG_PK';

SEGMENT_NAME         PARTITION_NAME  SEGMENT_TYPE     TABLESPACE_NAME
-------------------- --------------- ---------------- --------------------

MONTHLY_CALLS_AGG_PK                 INDEX            DWNG_MONTHLY_AGG_IX


SELECT index_name, table_owner, table_name, partitioned 
FROM USER_INDEXES  
WHERE table_name = 'MONTHLY_CALLS_AGG'

INDEX_NAME                     TABLE_OWNE TABLE_NAME           PARTITIONED
------------------------------ ---------- -------------------- -----------

MONTHLY_CALLS_AGG_PK           DWNG       MONTHLY_CALLS_AGG    NO



DROP INDEX MONTHLY_CALLS_AGG_PK
Index dropped

CREATE UNIQUE INDEX MY_USER.MONTHLY_CALLS_AGG_IND1 on MY_USER.MONTHLY_CALLS_AGG (year, month, customer_idservice_namesystem_service) LOCAL;
Index created
SELECT index_name, table_owner, table_name, partitioned 
FROM USER_INDEXES  
WHERE table_name = 'MONTHLY_CALLS_AGG'
INDEX_NAME                     TABLE_OWNE TABLE_NAME           PARTITIONED
------------------------------ ---------- -------------------- -----------
MONTHLY_CALLS_AGG_IND1         DWNG       MONTHLY_CALLS_AGG    YES


=============================
General - Indexes on a Partitioned table
=============================
Option A - Local Indexes
Option B - Global Indexes

Local Indexes
Each partition of a local index is associated with exactly one partition of the table.
Oracle automatically keeps the index partitions in sync with the table partitions.
New partitions are added to local indexes only when you add a partition to the underlying table, and are dropped when a partition is dropped from the underlying table.
It is impossible to manage index partitions manually.

Global Indexes
Global partitioned indexes are flexible in that the degree of partitioning and the partitioning key are independent from the table's partitioning method.
When a one of the below commands is executed on the underlying table, the index becomes unusable. To overcome this, use UPDATE GLOBAL INDEXES option.
For example:
ALTER TABLE my_table DROP PARTITION part_1 UPDATE GLOBAL INDEXES


Operations on the table that make global index unusable:
ADD (HASH), COALESCE (HASH), DROP, EXCHANGE, MERGE, MOVE, SPLIT, TRUNCATE.


=============================
Zombie index after table Redefinition
=============================
Consider following scenario:
DBMS_REDEFINITION.FINISH_REDEF_TABLE was run successfully.
Now, checking objects referencing old table, an index still exist.
The index, resides on TOOLS tablespace, which is the default user tablespace.
What is going on?

When Oracle is performing the Redefinition Process, it is using Read Only Snapshots.
Per Oracle documentation:

 Oracle creates several internal objects in the schema of the snapshot. Do not alter, change data in, or delete these objects manually. At the snapshot site, Oracle creates a base table, named SNAP$_snapshotname, to store the rows retrieved by the snapshot's defining query. For simple snapshots, Oracle also creates an index on the ROWID column of the base table, named I_SNAP$_snapshotname

Indeed, the weird index name is I_SNAP$_<TABLE_NAME>.
But the underlying object is not there.
Seems like a bug.
Oracle version was 10.1.0.5.0
The solution is to drop the index.
Here are the investigation steps, and resolution.
SELECT index_name, table_name, tablespace_name,status, partitioned 
FROM USER_INDEXES 
WHERE table_name LIKE '%STATEMENTS_AUDIT%'

INDEX_NAME                 TABLE_NAME       TABLESPACE_NAME STATUS   PARTITIONED
-------------------------- ---------------- --------------- -------- -----------
I_SNAP$_STATEMENTS_AUDIT_P STATEMENTS_AUDIT  TOOLS          UNUSABLE NO
STATEMENTS_AUDIT_IX01      STATEMENTS_AUDIT                 N/A      YES
SELECT index_name, table_name, column_name, column_position FROM USER_IND_COLUMNS WHERE table_name LIKE '%STATEMENTS_AUDIT%';

INDEX_NAME                  TABLE_NAME          COLUMN_NAME          COLUMN_POSITION
--------------------------- ------------------- -------------------- ---------------
I_SNAP$_STATEMENTS_AUDIT_P  STATEMENTS_AUDIT    M_ROW$$                            1
STATEMENTS_AUDIT_IX01       STATEMENTS_AUDIT    OBJECT_SCHEMA                      1
STATEMENTS_AUDIT_IX01       STATEMENTS_AUDIT    OBJECT_NAME                        2

SELECT table_name, partitioned,tablespace_name  
FROM USER_TABLES 
WHERE table_name LIKE '%STATEMENTS_AUDIT%';
 
TABLE_NAME                     PARTITIONED TABLESPACE_NAME
------------------------------ ----------- ------------------------------
STATEMENTS_AUDIT               YES




SELECT object_name, object_type, status 
FROM USER_OBJECTS 
WHERE object_name LIKE '%SNAP%';

OBJECT_NAME                    OBJECT_TYPE          STATUS
------------------------------ -------------------- ----------
I_SNAP$_STATEMENTS_AUDIT_P     INDEX                VALID

SELECT * FROM USER_MVIEWS
SELECT * FROM USER_REFRESH
SELECT * FROM USER_SNAPSHOT_LOGS
SELECT * FROM USER_SNAPSHOTS

All return No Data Found


Seems that there is a zombie Index, on ROWID of Snapshot on STATEMENTS_AUDIT table (The table that was Redefined). 
Once Redefinition process has finished, the Snapshot was dropped but the Index was not dropped.
Maybe this is bug in Oracle 10.1.0.5
The solution was simply to drop the zombie index.

SELECT * FROM V$VERSION;
 
BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.1.0.5.0 - Prod
PL/SQL Release 10.1.0.5.0 - Production
CORE 10.1.0.5.0 Production
TNS for Linux: Version 10.1.0.5.0 - Production
NLSRTL Version 10.1.0.5.0 - Production



DROP INDEX I_SNAP$_<TABLE_NAME>;

Index dropped

========================
Reference
========================
http://oracle-base.com Reference for DBMS_REDEFINITION
http://www.orafaq.com Reference for DBMS_REDEFINITION


No comments:

Post a Comment