You are on page 1of 21

Partitioned Tables And Indexes

Maintenance of large tables and indexes can become very time and resource
consuming. At the same time, data access performance can reduce drastically for these
objects. Partitioning of tables and indexes can benefit the performance and
maintenance in several ways.

Partition independance means backup and recovery operations can be


performed on individual partitions, whilst leaving the other partitons available.

Query performance can be improved as access can be limited to relevant


partitons only.

There is a greater ability for parallelism with more partitions.

Range Partitioning Tables


Range partitioning is useful when you have distinct ranges of data you want to store
together. The classic example of this is the use of dates. Partitioning a table using date
ranges allows all data of a similar age to be stored in same partition. Once historical
data is no longer needed the whole partition can be removed. If the table is indexed
correctly search criteria can limit the search to the partitions that hold data of a correct
age.
CREATE TABLE invoices
(invoice_no

NUMBER NOT NULL,

invoice_date DATE NOT NULL,


comments

VARCHAR2(500))

PARTITION BY RANGE (invoice_date)


(PARTITION invoices_q1 VALUES LESS THAN (TO_DATE('01/04/2001',
'DD/MM/YYYY')) TABLESPACE users,
PARTITION invoices_q2 VALUES LESS THAN (TO_DATE('01/07/2001',
'DD/MM/YYYY')) TABLESPACE users,
PARTITION invoices_q3 VALUES LESS THAN (TO_DATE('01/09/2001',
'DD/MM/YYYY')) TABLESPACE users,

PARTITION invoices_q4 VALUES LESS THAN (TO_DATE('01/01/2002',


'DD/MM/YYYY')) TABLESPACE users);

Hash Partitioning Tables


Hash partitioning is useful when there is no obvious range key, or range partitioning will
cause uneven distribution of data. The number of partitions must be a power of 2 (2, 4,
8, 16...) and can be specified by the PARTITIONS...STORE IN clause.
CREATE TABLE invoices
(invoice_no

NUMBER NOT NULL,

invoice_date DATE NOT NULL,


comments

VARCHAR2(500))

PARTITION BY HASH (invoice_no)


PARTITIONS 4
STORE IN (users, users, users, users);

Or specified individually.
CREATE TABLE invoices
(invoice_no

NUMBER NOT NULL,

invoice_date DATE NOT NULL,


comments

VARCHAR2(500))

PARTITION BY HASH (invoice_no)


(PARTITION invoices_q1 TABLESPACE users,
PARTITION invoices_q2 TABLESPACE users,
PARTITION invoices_q3 TABLESPACE users,
PARTITION invoices_q4 TABLESPACE users);

Composite Partitioning Tables


Composite partitioning allows range partitions to be hash subpartitioned on a different
key. The greater number of partitions increases the possiblities for parallelism and
reduces the chances of contention. The following example will range partition the table
on invoice_date and subpartitioned these on the invoice_no giving a totol of 32
subpartitions.
CREATE TABLE invoices
(invoice_no

NUMBER NOT NULL,

invoice_date DATE NOT NULL,


comments

VARCHAR2(500))

PARTITION BY RANGE (invoice_date)


SUBPARTITION BY HASH (invoice_no)
SUBPARTITIONS 8
(PARTITION invoices_q1 VALUES LESS THAN (TO_DATE('01/04/2001',
'DD/MM/YYYY')),
PARTITION invoices_q2 VALUES LESS THAN (TO_DATE('01/07/2001',
'DD/MM/YYYY')),
PARTITION invoices_q3 VALUES LESS THAN (TO_DATE('01/09/2001',
'DD/MM/YYYY')),
PARTITION invoices_q4 VALUES LESS THAN (TO_DATE('01/01/2002',
'DD/MM/YYYY'));

Partitioning Indexes
There are two basic types of partitioned index.

Local - All index entries in a single partition will correspond to a single table partition
(equipartitioned). They are created with the LOCAL keyword and support partition
independance. Equipartioning allows oracle to be more efficient whilst devising query
plans.

Global - Index in a single partition may correspond to multiple table partitions. They are
created with the GLOBAL keyword and do not support partition independance. Global

indexes can only be range partitioned and may be partitioned in such a fashion that they
look equipartitioned, but Oracle will not take advantage of this structure.

Both types of indexes can be subdivided further.

Prefixed - The partition key is the leftmost column(s) of the index. Probing this type of
index is less costly. If a query specifies the partition key in the where clause partition
pruning is possible, that is, not all partitions will be searched.

Non-Prefixed - Does not support partition pruning, but is effective in accessing data that
spans multiple partitions. Often used for indexing a column that is not the tables partition
key, when you would like the index to be partitioned on the same key as the underlying
table.

Local Prefixed Indexes


Assuming the INVOICES table is range partitioned on INVOICE_DATE, the followning
are examples of local prefixed indexes.
CREATE INDEX invoices_idx ON invoices (invoice_date) LOCAL;

CREATE INDEX invoices_idx ON invoices (invoice_date) LOCAL


(PARTITION invoices_q1 TABLESPACE users,
PARTITION invoices_q2 TABLESPACE users,
PARTITION invoices_q3 TABLESPACE users,
PARTITION invoices_q4 TABLESPACE users);

Oracle will generate the partition names and build the partitions in the default
tablespace using the default size unless told otherwise.

Local Non-Prefixed Indexes


Assuming the INVOICES table is range partitioned on INVOICE_DATE, the following
example is of a local non-prefixed index. The indexed column does not match the
partition key.

CREATE INDEX invoices_idx ON invoices (invoice_no) LOCAL


(PARTITION invoices_q1 TABLESPACE users,
PARTITION invoices_q2 TABLESPACE users,
PARTITION invoices_q3 TABLESPACE users,
PARTITION invoices_q4 TABLESPACE users);

Global Prefixed Indexes


Assuming the INVOICES table is range partitioned on INVOICE_DATE, the followning
examples is of a global prefixed index.
CREATE INDEX invoices_idx ON invoices (invoice_date)
GLOBAL PARTITION BY RANGE (invoice_date)
(PARTITION invoices_q1 VALUES LESS THAN (TO_DATE('01/04/2001',
'DD/MM/YYYY')) TABLESPACE users,
PARTITION invoices_q2 VALUES LESS THAN (TO_DATE('01/07/2001',
'DD/MM/YYYY')) TABLESPACE users,
PARTITION invoices_q3 VALUES LESS THAN (TO_DATE('01/09/2001',
'DD/MM/YYYY')) TABLESPACE users,
PARTITION invoices_q4 VALUES LESS THAN (MAXVALUE) TABLESPACE users);

Note that the partition range values must be specified. The GLOBAL keyword means
that Oracle can not assume the partition key is the same as the underlying table.

Global Non-Prefixed Indexes


Oracle does not support Global Non Prefixed indexes.

Partitioning Existing Tables


The ALTER TABLE ... EXCHANGE PARTITION ... syntax can be used to partition an
existing table, as shown by the following example. First we must create a nonpartitioned table to act as our starting point.

CREATE TABLE my_table (


id

NUMBER,

description VARCHAR2(50)
);

INSERT INTO my_table (id, description) VALUES (1, 'One');


INSERT INTO my_table (id, description) VALUES (2, 'Two');
INSERT INTO my_table (id, description) VALUES (3, 'Three');
INSERT INTO my_table (id, description) VALUES (4, 'Four');
COMMIT;

Next we create a new partitioned table with a single partition to act as our destination
table.
CREATE TABLE my_table_2 (
id

NUMBER,

description VARCHAR2(50)
)
PARTITION BY RANGE (id)
(PARTITION my_table_part VALUES LESS THAN (MAXVALUE));

Next we switch the original table segment with the partition segment.
ALTER TABLE my_table_2
EXCHANGE PARTITION my_table_part
WITH TABLE my_table
WITHOUT VALIDATION;

We can now drop the original table and rename the partitioned table.
DROP TABLE my_table;

RENAME my_table_2 TO my_table;

Finally we can split the partitioned table into multiple partitions as required and gather
new statistics.
ALTER TABLE my_table SPLIT PARTITION my_table_part AT (3)
INTO (PARTITION my_table_part_1,
PARTITION my_table_part_2);
EXEC DBMS_STATS.gather_table_stats(USER, 'MY_TABLE', cascade => TRUE);

The following query shows that the partitioning process is complete.


COLUMN high_value FORMAT A20
SELECT table_name,
partition_name,
high_value,
num_rows
FROM user_tab_partitions
ORDER BY table_name, partition_name;

TABLE_NAME
NUM_ROWS

PARTITION_NAME

HIGH_VALUE

------------------------------ ------------------------------ -------------------- ---------MY_TABLE

MY_TABLE_PART_1

MY_TABLE

MY_TABLE_PART_2

MAXVALUE

2 rows selected.

2
2

Partitioning an Existing Table using


DBMS_REDEFINITION
This article presents a simple method for partitioning an existing table using
the DBMS_REDEFINITION package, introduced in Oracle 9i. The contents of the article
should not be used as an indication of when and how to partition objects, it simply
shows the method of getting from A to B. Remember, in many cases incorrect
partitioning is worse than no partitioning!

Create a Sample Schema


First we create a sample schema as our starting point.
-- Create and populate a small lookup table.
CREATE TABLE lookup (
id

NUMBER(10),

description VARCHAR2(50)
);

ALTER TABLE lookup ADD (


CONSTRAINT lookup_pk 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 larger table that we will later partition.


CREATE TABLE big_table (
id

NUMBER(10),

created_date DATE,
lookup_id
data

NUMBER(10),
VARCHAR2(50)

);

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 big_table (id, created_date, lookup_id, data)
VALUES (i, l_create_date, l_lookup_id, 'This is some data for ' || i);
END LOOP;
COMMIT;
END;

-- Apply some constraints to the table.


ALTER TABLE big_table ADD (
CONSTRAINT big_table_pk PRIMARY KEY (id)
);

CREATE INDEX bita_created_date_i ON big_table(created_date);

CREATE INDEX bita_look_fk_i ON big_table(lookup_id);

ALTER TABLE big_table ADD (


CONSTRAINT bita_look_fk
FOREIGN KEY (lookup_id)
REFERENCES lookup(id)
);

-- Gather statistics on the schema objects


EXEC DBMS_STATS.gather_table_stats(USER, 'LOOKUP', cascade => TRUE);
EXEC DBMS_STATS.gather_table_stats(USER, 'BIG_TABLE', cascade => TRUE);

Create a Partitioned Interim Table


Next we create a new table with the appropriate partition structure to act as an interim
table.
-- Create partitioned table.
CREATE TABLE big_table2 (
id

NUMBER(10),

created_date DATE,

lookup_id
data

NUMBER(10),
VARCHAR2(50)

)
PARTITION BY RANGE (created_date)
(PARTITION big_table_2003 VALUES LESS THAN (TO_DATE('01/01/2004',
'DD/MM/YYYY')),
PARTITION big_table_2004 VALUES LESS THAN (TO_DATE('01/01/2005',
'DD/MM/YYYY')),
PARTITION big_table_2005 VALUES LESS THAN (MAXVALUE));

With this interim table in place we can start the online redefinition.

Start the Redefinition Process


First we check the redefinition is possible using the following command.
EXEC DBMS_REDEFINITION.can_redef_table(USER, 'BIG_TABLE');

If no errors are reported it is safe to start the redefinition using the following command.
-- Alter parallelism to desired level for large tables.
--ALTER SESSION FORCE PARALLEL DML PARALLEL 8;
--ALTER SESSION FORCE PARALLEL QUERY PARALLEL 8;

BEGIN
DBMS_REDEFINITION.start_redef_table(
uname

=> USER,

orig_table => 'BIG_TABLE',


int_table => 'BIG_TABLE2');
END;
/

Depending on the size of the table, this operation can take quite some time to complete.

Create Constraints and Indexes (Dependencies)


If there is delay between the completion of the previous operation and moving on to
finish the redefinition, it may be sensible to resynchronize the interim table before
building any constraints and indexes. The resynchronization of the interim table is
initiated using the following command.
-- Optionally synchronize new table with interim data before index creation
BEGIN
dbms_redefinition.sync_interim_table(
uname

=> USER,

orig_table => 'BIG_TABLE',


int_table => 'BIG_TABLE2');
END;
/

The dependent objects will need to be created against the new table. This is done using
the COPY_TABLE_DEPENDENTS procedure. You can decide which dependencies should
be copied.
SET SERVEROUTPUT ON
DECLARE
l_errors NUMBER;
BEGIN
DBMS_REDEFINITION.copy_table_dependents(
uname

=> USER,

orig_table

=> 'BIG_TABLE',

int_table

=> 'BIG_TABLE2',

copy_indexes

=> DBMS_REDEFINITION.cons_orig_params,

copy_triggers

=> TRUE,

copy_constraints => TRUE,


copy_privileges => TRUE,
ignore_errors

=> FALSE,

num_errors

=> l_errors,

copy_statistics => FALSE,


copy_mvlog

=> FALSE);

DBMS_OUTPUT.put_line('Errors=' || l_errors);
END;
/

The fact you are partitioning the table means you should probably consider the way you
are indexing the table. You may want to manually create the constraints and indexes
against the interim table using alternate names to prevent errors. The indexes should be
created with the appropriate partitioning scheme to suit their purpose.
-- Add new keys, FKs and triggers.
ALTER TABLE big_table2 ADD (
CONSTRAINT big_table_pk2 PRIMARY KEY (id)
);

CREATE INDEX bita_created_date_i2 ON big_table2(created_date) LOCAL;

CREATE INDEX bita_look_fk_i2 ON big_table2(lookup_id) LOCAL;

ALTER TABLE big_table2 ADD (


CONSTRAINT bita_look_fk2
FOREIGN KEY (lookup_id)
REFERENCES lookup(id)
);

-- Gather statistics on the new table.


EXEC DBMS_STATS.gather_table_stats(USER, 'BIG_TABLE2', cascade => TRUE);

Complete the Redefinition Process


Once the constraints and indexes have been created the redefinition can be completed
using the following command.
BEGIN
dbms_redefinition.finish_redef_table(
uname

=> USER,

orig_table => 'BIG_TABLE',


int_table => 'BIG_TABLE2');
END;
/

At this point the interim table has become the "real" table and their names have been
switched in the data dictionary. All that remains is to perform some cleanup operations.
-- Remove original table which now has the name of the interim table.
DROP TABLE big_table2;

-- Rename all the constraints and indexes to match the original names.
ALTER TABLE big_table RENAME CONSTRAINT big_table_pk2 TO big_table_pk;
ALTER TABLE big_table RENAME CONSTRAINT bita_look_fk2 TO bita_look_fk;
ALTER INDEX big_table_pk2 RENAME TO big_table_pk;
ALTER INDEX bita_look_fk_i2 RENAME TO bita_look_fk_i;
ALTER INDEX bita_created_date_i2 RENAME TO bita_created_date_i;

The following queries show that the partitioning was successful.

SELECT partitioned
FROM user_tables
WHERE table_name = 'BIG_TABLE';

PAR
--YES

1 row selected.

SELECT partition_name
FROM user_tab_partitions
WHERE table_name = 'BIG_TABLE';

PARTITION_NAME
-----------------------------BIG_TABLE_2003
BIG_TABLE_2004
BIG_TABLE_2005

3 rows selected.

Partitioning an Existing Table using EXCHANGE


PARTITION
This article presents a simple method for partitioning an existing table using
the EXCHANGE PARTITION syntax. The contents of the article should not be used as an
indication of when and how to partition objects, it simply shows the method of getting
from A to B. Remember, in many cases incorrect partitioning is worse than no
partitioning!

Create a Sample Schema


First we create a sample schema as our starting point.
-- Create and populate a small lookup table.
CREATE TABLE lookup (
id

NUMBER(10),

description VARCHAR2(50)
);

ALTER TABLE lookup ADD (


CONSTRAINT lookup_pk 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 larger table that we will later partition.


CREATE TABLE big_table (
id

NUMBER(10),

created_date DATE,
lookup_id
data
);

DECLARE

NUMBER(10),
VARCHAR2(50)

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 big_table (id, created_date, lookup_id, data)
VALUES (i, l_create_date, l_lookup_id, 'This is some data for ' || i);
END LOOP;
COMMIT;
END;
/

-- Apply some constraints to the table.


ALTER TABLE big_table ADD (
CONSTRAINT big_table_pk PRIMARY KEY (id)
);

CREATE INDEX bita_created_date_i ON big_table(created_date);

CREATE INDEX bita_look_fk_i ON big_table(lookup_id);

ALTER TABLE big_table ADD (


CONSTRAINT bita_look_fk
FOREIGN KEY (lookup_id)
REFERENCES lookup(id)
);

-- Gather statistics on the schema objects


EXEC DBMS_STATS.gather_table_stats(USER, 'LOOKUP', cascade => TRUE);
EXEC DBMS_STATS.gather_table_stats(USER, 'BIG_TABLE', cascade => TRUE);

Create a Partitioned Destination Table


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 partitioned table.
CREATE TABLE big_table2 (
id

NUMBER(10),

created_date DATE,
lookup_id
data

NUMBER(10),
VARCHAR2(50)

)
PARTITION BY RANGE (created_date)
(PARTITION big_table_2007 VALUES LESS THAN (MAXVALUE));

-- Add new keys, FKs and triggers.


ALTER TABLE big_table2 ADD (
CONSTRAINT big_table_pk2 PRIMARY KEY (id)

);

CREATE INDEX bita_created_date_i2 ON big_table2(created_date) LOCAL;

CREATE INDEX bita_look_fk_i2 ON big_table2(lookup_id) LOCAL;

ALTER TABLE big_table2 ADD (


CONSTRAINT bita_look_fk2
FOREIGN KEY (lookup_id)
REFERENCES lookup(id)
);

With this destination table in place we can start the conversion.

EXCHANGE PARTITION
We now switch the segments associated with the source table and the partition in the
destination table using the EXCHANGE PARTITION syntax.
ALTER TABLE big_table2
EXCHANGE PARTITION big_table_2007
WITH TABLE big_table
WITHOUT VALIDATION
UPDATE GLOBAL INDEXES;

The exchange operation should not be affected by the size of the segments involved.
Once this is complete we can drop the old table and rename the new table and all it's
constraints.
DROP TABLE big_table;
RENAME big_table2 TO big_table;

ALTER TABLE big_table RENAME CONSTRAINT big_table_pk2 TO big_table_pk;


ALTER TABLE big_table RENAME CONSTRAINT bita_look_fk2 TO bita_look_fk;
ALTER INDEX big_table_pk2 RENAME TO big_table_pk;
ALTER INDEX bita_look_fk_i2 RENAME TO bita_look_fk_i;
ALTER INDEX bita_created_date_i2 RENAME TO bita_created_date_i;

SPLIT PARTITION
Next, we split the single large partition into smaller partitions as required.
ALTER TABLE big_table
SPLIT PARTITION big_table_2007 AT (TO_DATE('31-DEC-2005 23:59:59', 'DDMON-YYYY HH24:MI:SS'))
INTO (PARTITION big_table_2005,
PARTITION big_table_2007)
UPDATE GLOBAL INDEXES;

ALTER TABLE big_table


SPLIT PARTITION big_table_2007 AT (TO_DATE('31-DEC-2006 23:59:59', 'DDMON-YYYY HH24:MI:SS'))
INTO (PARTITION big_table_2006,
PARTITION big_table_2007)
UPDATE GLOBAL INDEXES;

EXEC DBMS_STATS.gather_table_stats(USER, 'BIG_TABLE', cascade => TRUE);

The following queries show that the partitioning was successful.


SELECT partitioned
FROM user_tables

WHERE table_name = 'BIG_TABLE';

PAR
--YES

1 row selected.

SELECT partition_name, num_rows


FROM user_tab_partitions
WHERE table_name = 'BIG_TABLE';

PARTITION_NAME

NUM_ROWS

------------------------------ ---------BIG_TABLE_2005

335326

BIG_TABLE_2006

332730

BIG_TABLE_2007

334340

3 rows selected.

You might also like