Oracle Partitioning Survival Guide

Martin Zahn, 24.03.2009


Overview

Anyone with un-partitioned databases over 500 gigabytes is a disaster. Databases become unmanageable, and ...


... serious problems occur like

  • SQL queries with full-table scans take hours to complete

  • Recovery - Files recovery takes days, not minutes

  • Maintenance - Rebuilding indexes may last very long

There are many compelling reasons to implement Oracle partitioning for larger databases, and Oracle partitioning has become the de-facto standard for systems over 500 gigabytes.

Key Issues

Partitioning addresses key issues in supporting very large tables and indexes by

  •  Letting you decompose them into smaller and more manageable pieces called partitions.

  •  SQL queries and DML statements do not need to be modified in order to access partitioned tables.

  •  Partitioning is entirely transparent to applications.

Each partition of a table or index must have the same logical attributes, such as column names, datatypes, and constraints, but each partition can have separate physical attributes such as pctfree

Partitioning Methods

Oracle provides the following partitioning methods

  • Range Partitioning

  • List Partitioning

  • Hash Partitioning

  • Composite Partitioning

Partition Key

Each row in a partitioned table is unambiguously assigned to a single partition. The partition key is a set of one or more columns that determines the partition for each row. Oracle automatically directs insert, update, and delete operations to the appropriate partition through the use of the partition key.

Index Partitioning

Just like partitioned tables, partitioned indexes improve manageability, availability, performance, and scalability. They can either be

  • Partitioned independently (global indexes)
  • Automatically linked to a table's partitioning method (local indexes).

Whenever possible, you should try to use local indexes because they are easier to manage.

Local prefixed Index

  • 1 Index per table partition
  • Leftmost attribute of the index is the partition key
  • UNIQUE index is possible

Local NON prefixed Index

  • 1 Index per table partition
  • Leftmost attribute of the index is NOT the partition key
  • UNIQUE possible if partition key is part of the index.

Global prefixed Index

  • Independent of table partition
  • Number of index partitions must not correspond with number of table partitions.
  • Establish fast search over all table partitions, but index rebuild necessary if table partition are changed. This must be done manually.

Global NON prefixed Index

  • Oracle10 does not support global NON prefixed indexes.

    ORA-14038: GLOBAL partitioned index must be prefixed

Range Partitioning

Range partitioning maps data to partitions based on ranges of partition key values that you establish for each partition. It is the most common type of partitioning and is often used with dates. For example, you might want to partition sales data into monthly partitions.

When using range partitioning, consider the following rules:

  • Each partition has a VALUES LESS THAN clause, which specifies a noninclusive upper bound for the partitions. Any values of the partition key equal to or higher than this literal are added to the next higher partition.
     

  • All partitions, except the first, have an implicit lower bound specified by the VALUES LESS THAN clause on the previous partition.
     

  • A MAXVALUE literal can be defined for the highest partition. MAXVALUE represents a virtual infinite value that sorts higher than any other possible value for the partition key, including the null value.

A typical example is given below. The statement creates a table (sales) that is range partitioned on the sdate field which is the partition key.

CREATE TABLE sales
(
   sid     NUMBER(5),
   sdate   DATE,
   name    VARCHAR2(30),
   amount  NUMBER(10)
)
PARTITION BY RANGE (sdate)
(
   PARTITION sales_jan2009 VALUES LESS THAN(TO_DATE('02/01/2009','MM/DD/YYYY')),
   PARTITION sales_feb2009 VALUES LESS THAN(TO_DATE('03/01/2009','MM/DD/YYYY')),
   PARTITION sales_mar2009 VALUES LESS THAN(TO_DATE('04/01/2009','MM/DD/YYYY')),
   PARTITION sales_apr2009 VALUES LESS THAN(TO_DATE('05/01/2009','MM/DD/YYYY')),
   PARTITION sales_max VALUES LESS THAN(MAXVALUE)
)
ENABLE ROW MOVEMENT;

MAXVALUE

What happens when an incoming record in an INSERT statement has a value in the partitioning key column, for which no partitions have been defined? The INSERT will fail. However, if you have defined a default partition by, for example, using the MAXVALUE clause, the INSERT will not fail and the new record will go into the default partition.

Row Movement

The row movement clause, either ENABLE ROW MOVEMENT or DISABLE ROW MOVEMENT can be specified. This clause either enables or disables the migration of a row to a new partition if its key is updated. The default is DISABLE ROW MOVEMENT.

ORA-14039

The index used to enforce the primary key can be local if and only if the partition key is, in fact, contained in the primary key. Otherwise, you will receive an ORA-14039 error (partitioning columns must form a subset of key columns of a unique index).

Local NON Prefixed Index

ALTER TABLE sales ADD
(
   CONSTRAINT pk_sales
   PRIMARY KEY (sid,sdate)
   USING INDEX
   LOCAL
   (
      PARTITION sales_jan2009 TABLESPACE users PCTFREE 20,
      PARTITION sales_feb2009 TABLESPACE users PCTFREE 50,
      PARTITION sales_mar2009 TABLESPACE users PCTFREE 20,
      PARTITION sales_apr2009 TABLESPACE users PCTFREE 20,
      PARTITION sales_max TABLESPACE users PCTFREE 20
   )
);

Local Prefixed Index

ALTER TABLE sales ADD
(
   CONSTRAINT pk_sales
   PRIMARY KEY (sdate,sid)
   USING INDEX
   LOCAL
   (
      PARTITION sales_jan2009 TABLESPACE users PCTFREE 20,
      PARTITION sales_feb2009 TABLESPACE users PCTFREE 50,
      PARTITION sales_mar2009 TABLESPACE users PCTFREE 20,
      PARTITION sales_apr2009 TABLESPACE users PCTFREE 20,
      PARTITION sales_max TABLESPACE users PCTFREE 20
   )
);

Global Prefixed Index

ALTER TABLE sales ADD
(
   CONSTRAINT pk_sales
   PRIMARY KEY (sid)
   USING INDEX
   GLOBAL PARTITION BY RANGE (sid)
   (
      PARTITION p1 VALUES LESS THAN (10000),
      PARTITION p2 VALUES LESS THAN (20000),
      PARTITION p3 VALUES LESS THAN (30000),
      PARTITION p4 VALUES LESS THAN (40000),
      PARTITION p5 VALUES LESS THAN (50000),
      PARTITION max VALUES LESS THAN (MAXVALUE)
   )
);

CREATE INDEX sales_idx ON sales (sid,amount)
GLOBAL PARTITION BY RANGE (sid)
   (
      PARTITION p1 VALUES LESS THAN (10000),
      PARTITION p2 VALUES LESS THAN (20000),
      PARTITION p3 VALUES LESS THAN (30000),
      PARTITION p4 VALUES LESS THAN (40000),
      PARTITION p5 VALUES LESS THAN (50000),
      PARTITION max VALUES LESS THAN (MAXVALUE)
   );

Global NON Prefixed Index

CREATE INDEX sales_idx ON sales (amount,sid)
GLOBAL PARTITION BY RANGE (sid)
   (
      PARTITION p1 VALUES LESS THAN (10000),
      PARTITION p2 VALUES LESS THAN (20000),
      PARTITION p3 VALUES LESS THAN (30000),
      PARTITION p4 VALUES LESS THAN (40000),
      PARTITION p5 VALUES LESS THAN (50000),
      PARTITION max VALUES LESS THAN (MAXVALUE)
   );

GLOBAL PARTITION BY RANGE (sid)
ORA-14038: GLOBAL partitioned index must be prefixed

Hash Partitioning

Hash partitioning enables easy partitioning of data that does not lend itself to range or list partitioning. It does this with a simple syntax and is easy to implement. The number of partitions must be a power of 2 (2, 4, 8, 16...) and can be specified by the PARTITIONS ... STORE IN clause:

It is a better choice than range partitioning when:

  • You do not know beforehand how much data maps into a given range
  • The sizes of range partitions would differ quite substantially or would be difficult to balance manually

CREATE TABLE sales
(
   sid     NUMBER(5),
   sdate   DATE,
   name    VARCHAR2(30),
   amount  NUMBER(10)
)
PARTITION BY HASH (name)
PARTITIONS 4
STORE IN
(
   users,
   users,
   users,
   users
);

or:

CREATE TABLE sales
(
   sid     NUMBER(5),
   sdate   DATE,
   name    VARCHAR2(30),
   amount  NUMBER(10)
)
PARTITION BY HASH (name)
(
   PARTITION p1 TABLESPACE users,
   PARTITION p2 TABLESPACE users,
   PARTITION p3 TABLESPACE users,
   PARTITION p4 TABLESPACE users
);

List Partitioning

List partitioning enables you to explicitly control how rows map to partitions. You do this by specifying a list of discrete values for the partitioning key in the description for each partition. This is different from range partitioning, where a range of values is associated with a partition and from hash partitioning, where a hash function controls the row-to-partition mapping. The advantage of list partitioning is that you can group and organize unordered and unrelated sets of data in a natural way.

The details of list partitioning can best be described with an example. In this case, let's say you want to partition a sales table by region.

CREATE TABLE sales
(
   sid     NUMBER(5),
   sdate   DATE,
   region  VARCHAR2(30),
   amount  NUMBER(10)
)
PARTITION BY LIST (region)
(
   PARTITION sales_region1 VALUES ('BE','ZH','VS'),
   PARTITION sales_region2 VALUES ('GR','SO','TG','JU'),
   PARTITION sales_region3 VALUES ('GE','NE','VD'),
   PARTITION sales_other VALUES(DEFAULT)
);


insert into sales (sid,sdate,region,amount) values(1,'05-JAN-2009','BE',200);
insert into sales (sid,sdate,region,amount) values(1,'05-JAN-2009','TI',500);
commit;

select count(*) from sales partition (sales_region1);

  COUNT(*)
----------
         1

select count(*) from sales partition (sales_region2);

  COUNT(*)
----------
         0


select count(*) from sales partition (sales_region3);

  COUNT(*)
----------
         0

select count(*) from sales partition (sales_other);

  COUNT(*)
----------
         1

Composite partitioning

Composite partitioning partitions data using the range method, and within each partition, subpartitions it using the hash or list method.

Range-Hash Example

This statement creates a table sales that is range partitioned
on the sdate field and hash subpartitioned on sid.

 CREATE TABLE SALES
(
  sid     NUMBER(5),
  sdate   DATE,
  region  VARCHAR2(30 BYTE),
  amount  NUMBER(10)
)
PARTITION BY RANGE (sdate)
SUBPARTITION BY HASH (sid)
(
   PARTITION sales01 VALUES LESS THAN(TO_DATE('01/01/2000','MM/DD/YYYY'))
   TABLESPACE USERS
   (
      SUBPARTITION sales01_01    TABLESPACE USERS,
      SUBPARTITION sales01_02    TABLESPACE USERS
   ),
   PARTITION sales02 VALUES LESS THAN(TO_DATE('02/01/2000','MM/DD/YYYY'))
   TABLESPACE USERS
   (
      SUBPARTITION sales02_01    TABLESPACE USERS,
      SUBPARTITION sales02_02    TABLESPACE USERS
   ),
   PARTITION sales03 VALUES LESS THAN(TO_DATE('03/01/2000','MM/DD/YYYY'))
   TABLESPACE USERS
   (
      SUBPARTITION sales03_01    TABLESPACE USERS,
      SUBPARTITION sales03_02    TABLESPACE USERS
   ),
   PARTITION sales04 VALUES LESS THAN(TO_DATE('04/01/2000','MM/DD/YYYY'))
   TABLESPACE USERS
   (
      SUBPARTITION sales04_01    TABLESPACE USERS,
      SUBPARTITION sales04_02    TABLESPACE USERS
   ),
   PARTITION sales05 VALUES LESS THAN (MAXVALUE)
   TABLESPACE USERS
   (
      SUBPARTITION sales05_01    TABLESPACE USERS,
      SUBPARTITION sales05_02    TABLESPACE USERS
   )
)
ENABLE ROW MOVEMENT;

Range-List Example

This statement creates a table sales that is range partitioned on the sdate field and list subpartitioned on region. When you use a template, Oracle names the subpartitions by concatenating the partition name, an underscore, and the subpartition name from the template. Oracle places this subpartition in the tablespace specified in the template.

CREATE TABLE SALES
(
  sid     NUMBER(5),
  sdate   DATE,
  region  VARCHAR2(30 BYTE),
  amount  NUMBER(10)
)
PARTITION BY RANGE (sdate)
SUBPARTITION BY LIST (region)
SUBPARTITION TEMPLATE
(
   SUBPARTITION east VALUES('NY','VA','FL') TABLESPACE users,
   SUBPARTITION west VALUES('CA','OR','HI') TABLESPACE users,
   SUBPARTITION cent VALUES('IL','TX','MO') TABLESPACE users
)
(
   PARTITION sales01 VALUES LESS THAN(TO_DATE('01/01/2000','MM/DD/YYYY')),
   PARTITION sales02 VALUES LESS THAN(TO_DATE('02/01/2000','MM/DD/YYYY')),
   PARTITION sales03 VALUES LESS THAN(TO_DATE('03/01/2000','MM/DD/YYYY')),
   PARTITION sales04 VALUES LESS THAN(TO_DATE('04/01/2000','MM/DD/YYYY')),
   PARTITION sales05 VALUES LESS THAN(TO_DATE('05/01/2000','MM/DD/YYYY')),
   PARTITION sales06 VALUES LESS THAN(TO_DATE('06/01/2000','MM/DD/YYYY')),
   PARTITION sales07 VALUES LESS THAN (MAXVALUE)
);

Alter Partitions

Some Maintenance Operations on Partitions include

  • Adding Partitions

  • Coalescing Partitions

  • Dropping Partitions

  • Exchanging Partitions

  • Merging Partitions

  • Moving Partitions

  • Renaming Partitions

  • Splitting Partitions

  • Truncating Partitions

Some operations are show next, first create the following Partition Table

CREATE TABLE req (
   bkg_id    NUMBER(15)   NOT NULL,   -- Primary Key
   req_id    NUMBER(15)   NOT NULL,   -- Primary Key
   date_req  DATE         NOT NULL,   -- Partition Key
   status    NUMBER(2)    NOT NULL)
PARTITION BY RANGE (date_req)
(
   PARTITION req_01_2009 VALUES LESS THAN (TO_DATE('01.02.2009','DD.MM.YYYY')),
   PARTITION req_02_2009 VALUES LESS THAN (TO_DATE('01.03.2009','DD.MM.YYYY')),
   PARTITION req_03_2009 VALUES LESS THAN (TO_DATE('01.04.2009','DD.MM.YYYY')),
   PARTITION req_04_2009 VALUES LESS THAN (TO_DATE('01.05.2009','DD.MM.YYYY')),
   PARTITION req_05_2009 VALUES LESS THAN (MAXVALUE)
);

CREATE UNIQUE INDEX pk_req ON req (bkg_id,req_id)
GLOBAL
PARTITION BY RANGE (bkg_id,req_id)
(
   PARTITION pk_req_01 VALUES LESS THAN (100000,100000),
   PARTITION pk_req_02 VALUES LESS THAN (200000,200000),
   PARTITION pk_req_03 VALUES LESS THAN (300000,300000),
   PARTITION pk_req_04 VALUES LESS THAN (MAXVALUE,MAXVALUE)
);

ALTER TABLE req ADD
(
   CONSTRAINT pk_req
   PRIMARY KEY (bkg_id,req_id)
);

Create a local Prefixed Index

CREATE INDEX idx_req1 ON req (date_req,req_id)
LOCAL
(
   PARTITION req_01_2009,
   PARTITION req_02_2009,
   PARTITION req_03_2009,
   PARTITION req_04_2009,
   PARTITION req_05_2009
);

Create a Bitmap Index

CREATE BITMAP INDEX idx_req2 ON req (status)
LOCAL
(
   PARTITION req_01_2009,
   PARTITION req_02_2009,
   PARTITION req_03_2009,
   PARTITION req_04_2009,
   PARTITION req_05_2009
);

Insert some Values

INSERT INTO req (bkg_id,req_id,date_req,status)
VALUES (100001,100001,TO_DATE('02.02.2009','DD.MM.YYYY'),1);

INSERT INTO req (bkg_id,req_id,date_req,status)
VALUES (200001,200001,TO_DATE('02.03.2009','DD.MM.YYYY'),1);

INSERT INTO req (bkg_id,req_id,date_req,status)
VALUES (300001,300001,TO_DATE('02.04.2009','DD.MM.YYYY'),1);

INSERT INTO req (bkg_id,req_id,date_req,status)
VALUES (400001,400001,TO_DATE('02.05.2009','DD.MM.YYYY'),1);

INSERT INTO req (bkg_id,req_id,date_req,status)
VALUES (500001,500001,TO_DATE('02.06.2009','DD.MM.YYYY'),1);

COMMIT;

Split / Rename Table Partition

ALTER TABLE req
  SPLIT PARTITION req_05_2009 AT (TO_DATE('31.05.2009','DD.MM.YYYY'))
  INTO (PARTITION req_05_2009_1, PARTITION req_05_2009_2);

ALTER TABLE req
   RENAME PARTITION req_05_2009_1 TO req_05_2009;

ALTER TABLE req
   RENAME PARTITION req_05_2009_2 TO req_06_2009;

ALTER INDEX idx_req1
  RENAME PARTITION req_05_2009_1 TO req_05_2009;

ALTER INDEX idx_req1
  RENAME PARTITION req_05_2009_2 TO req_06_2009;

ALTER INDEX idx_req2
  RENAME PARTITION req_05_2009_1 TO req_05_2009;

ALTER INDEX idx_req2
  RENAME PARTITION req_05_2009_2 TO req_06_2009;

Split / Rename Index Partition

ALTER INDEX pk_req
  SPLIT PARTITION pk_req_04 AT (400000,400000)
  INTO (PARTITION pk_req_04_1, PARTITION pk_req_04_2);

ALTER INDEX pk_req RENAME PARTITION pk_req_04_1 TO pk_req_04;
ALTER INDEX pk_req RENAME PARTITION pk_req_04_2 TO pk_req_05;

Move Partition

ALTER TABLE req MOVE PARTITION req_01_2009 TABLESPACE index;

Drop Partition

ALTER TABLE req DROP PARTITION req_06_2009;

Add Partition

ALTER TABLE req ADD PARTITION req_06_2009
   VALUES LESS THAN (TO_DATE('01.06.2009','DD.MM.YYYY'));

Check / Rebuild Index Partitions

Some operations, such as ALTER TABLE DROP PARTITION, mark all Oracle partitions of a partition index unusable. These Indexes must be rebuild.

Check Status

SELECT index_name, partition_name, status
  FROM user_ind_partitions
 WHERE status = 'UNUSABLE'
ORDER BY 1,2;

INDEX_NAME                     PARTITION_NAME                 STATUS
------------------------------ ------------------------------ --------
IDX_REQ1                       REQ_05_2009                    UNUSABLE
IDX_REQ2                       REQ_05_2009                    UNUSABLE
PK_REQ                         PK_REQ_01                      UNUSABLE
PK_REQ                         PK_REQ_02                      UNUSABLE
PK_REQ                         PK_REQ_03                      UNUSABLE
PK_REQ                         PK_REQ_04                      UNUSABLE
PK_REQ                         PK_REQ_05                      UNUSABLE

Rebuild Index

select 'ALTER INDEX ' || index_name
                      || ' REBUILD PARTITION '
                      || partition_name
                      || ' ONLINE;'
  from user_ind_partitions
  where status = 'UNUSABLE';

ALTER INDEX PK_REQ REBUILD PARTITION PK_REQ_01 ONLINE;
ALTER INDEX PK_REQ REBUILD PARTITION PK_REQ_04 ONLINE;
ALTER INDEX PK_REQ REBUILD PARTITION PK_REQ_02 ONLINE;
ALTER INDEX PK_REQ REBUILD PARTITION PK_REQ_05 ONLINE;
ALTER INDEX IDX_REQ2 REBUILD PARTITION REQ_05_2009 ONLINE;
ALTER INDEX PK_REQ REBUILD PARTITION PK_REQ_03 ONLINE;
ALTER INDEX IDX_REQ1 REBUILD PARTITION REQ_05_2009 ONLINE;

Very fast Data Load using Partition Exchange

One of the great features about partitioning, and most specifically range-based partitioning, is the ability to load data quickly and easily with minimal impact on the current users using:

alter table call exchange partition data_2007 with table call_temp;

This command swaps over the definitions of the named partition and the CALL table, so that the data suddenly exists in the right place in the partitioned table. Moreover, with the inclusion of the two optional extra clauses, index definitions will be swapped and Oracle will not check whether the data actually belongs in the partition - so the exchange is very quick.

Example with no Parent Child Relation

In this example, the primary key and the partition key in the partitioned table CALL both are build on the same column «id». Therefore the primary key on CALL can be a LOCAL index and the indexes doesn't become UNUSABLE (must not be rebuild) after the EXCHANGE.

-- Create the Partition Table (Destination Table)

CREATE TABLE call (
    id    NUMBER(12,6),
    v1    VARCHAR2(10),
    data  VARCHAR2(100)
)
PARTITION BY RANGE(id) ( -- Partion Key = Primary Key

    PARTITION call_partition VALUES LESS THAN (MAXVALUE)
);

-- Next, create the temporary Table which is used to load the Data offline

CREATE TABLE call_temp (
    id    NUMBER(12,6),
    v1    VARCHAR2(10),
    data  VARCHAR2(100)
);

-- Load 1'000'000 Rows into the offline Table

INSERT /*+ append ordered full(s1) use_nl(s2) */
INTO call_temp
SELECT
        TRUNC((ROWNUM-1)/500,6),
        TO_CHAR(ROWNUM),
        RPAD('X',100,'X')
FROM    all_tables s1,
        all_tables s2
WHERE   ROWNUM <= 1000000;

-- Add LOCAL Primary Key to the Partition Table as a local Index

ALTER TABLE call
  ADD CONSTRAINT pk_call PRIMARY KEY(id)
  USING INDEX (CREATE INDEX pk_call ON CALL(id) NOLOGGING LOCAL);

-- Add Primary Key to the offline Table

ALTER TABLE call_temp
  ADD CONSTRAINT pk_call_temp PRIMARY KEY(id)
  USING INDEX (CREATE INDEX pk_call_temp ON call_temp(id) NOLOGGING);

-- Now swap the offline Table into the Partition

ALTER TABLE CALL
  EXCHANGE PARTITION call_partition WITH TABLE call_temp
  INCLUDING INDEXES
  WITHOUT VALIDATION;

Elapsed: 00:00:00.01

Oracle is checking that the exchange won’t cause a uniqueness problem. The query is searching the entire CALL table (excluding the partition we are exchanging) to see if there are any duplicates of the rows which we are loading. This is particularly daft, since the unique constraint is maintained through a local index, so it must include the partitioning key ID, which means there is only one legal partition in which a row can be, and we have already promised (through the without validation clause) that all the rows belong where we are putting them.

-- For the next load, first truncate the CALL Table, then process the next load.

TRUNCATE TABLE call;

ALTER TABLE CALL
  EXCHANGE PARTITION call_partition WITH TABLE call_temp
  INCLUDING INDEXES
  WITHOUT VALIDATION;

Example with Parent Child Relation

In this example, the primary key and the partition key in the partitioned table CALL are NOT the same. The primary key is build on the column «id», but the partition key is build on «created_date». Therefore the primary key on CALL must be a GLOBAL index and the indexes must be maintained using the clause UPDATE GLOBAL INDEXES in the EXCHANGE.

-- Create and populate a small lookup table

CREATE TABLE lookup (
  id            NUMBER(10),
  description   VARCHAR2(50)
);

ALTER TABLE lookup ADD (
  CONSTRAINT pk_lookup PRIMARY KEY (id)
);

INSERT INTO lookup (id, description) VALUES (1, 'ONE');
INSERT INTO lookup (id, description) VALUES (2, 'TWO');
INSERT INTO lookup (id, description) VALUES (3, 'THREE');
COMMIT;

-- Create and populate a temporary table to load the data

CREATE TABLE call_temp (
  id            NUMBER(10),
  created_date  DATE,
  lookup_id     NUMBER(10),
  data          VARCHAR2(50)
);

-- Load the temporary table

DECLARE
  l_lookup_id    lookup.id%TYPE;
  l_create_date  DATE;
BEGIN
  FOR i IN 1 .. 1000000 LOOP
    IF MOD(i, 3) = 0 THEN
      l_create_date := ADD_MONTHS(SYSDATE, -24);
      l_lookup_id   := 2;
    ELSIF MOD(i, 2) = 0 THEN
      l_create_date := ADD_MONTHS(SYSDATE, -12);
      l_lookup_id   := 1;
    ELSE
      l_create_date := SYSDATE;
      l_lookup_id   := 3;
    END IF;
    INSERT INTO call_temp (id, created_date, lookup_id, data)
    VALUES (i, l_create_date, l_lookup_id, 'Loaded Data for ' || i);
  END LOOP;
  COMMIT;
END;
/

-- Apply Primary Key to the CALL_TEMP table.

ALTER TABLE call_temp ADD (
  CONSTRAINT pk_call_temp PRIMARY KEY (id)
);

-- Add Key and Foreign Key Constraint to the CALL_TEMP table.

CREATE INDEX idx_call_temp_created_date ON call_temp(created_date);
CREATE INDEX idx_call_temp_lookup_id ON call_temp(lookup_id);

ALTER TABLE call_temp ADD (
  CONSTRAINT fk_call_temp_lookup_id
  FOREIGN KEY (lookup_id)
  REFERENCES lookup(id)
);

-- Next we create a new table with the appropriate partition structure
-- to act as the destination table. The destination must have the
-- same constraints and indexes defined.


CREATE TABLE call (
  id            NUMBER(10),
  created_date  DATE,
  lookup_id     NUMBER(10),
  data          VARCHAR2(50)
)
PARTITION BY RANGE (created_date)
  (PARTITION call_temp_created_date VALUES LESS THAN (MAXVALUE));


-- Add Primary Key to CALL which must be GLOBAL
-- With LOCAL Index you get: ORA-14196: Specified index
-- cannot be used to enforce the constraint


ALTER TABLE call ADD
  CONSTRAINT pk_call PRIMARY KEY (id)
  USING INDEX (CREATE INDEX pk_call ON CALL(id) GLOBAL
);

-- Add Keys and Foreign Key Constraint to the CALL table
-- which can be LOCAL


CREATE INDEX idx_call_created_date ON call(created_date) LOCAL;
CREATE INDEX idx_call_lookup_id ON call(lookup_id) LOCAL;

ALTER TABLE call ADD (
  CONSTRAINT fk_call_lookup_id
  FOREIGN KEY (lookup_id)
  REFERENCES lookup(id)
);

-- With this destination table in place we can start the conversion.
-- We now switch the segments associated with the source table and the
-- partition in the destination table using EXCHANGE PARTITION

SET TIMING ON;
ALTER TABLE call
  EXCHANGE PARTITION call_temp_created_date
  WITH TABLE call_temp
  WITHOUT VALIDATION
  UPDATE GLOBAL INDEXES;

Elapsed: 00:00:27.19

This is significantly slower than in the previous example!

The UPDATE GLOBAL INDEXES is needed because without it that would leave the global indexes associated with the partition in an UNUSABLE state. If the UPDATE GLOBAL INDEXES clause is added, the performance is reduced since the index rebuild is part of the issued DDL. The index updates are logged and it should only be used when the number of rows is low and data must stay available. For larger numbers of rows index rebuilds are more efficient and allow index reorganization.