Chapter 14

Overview

Partitioning in Oracle was first introduced in Oracle 8.0. It is the ability to physically break a table or index into many smaller more manageable pieces. As far as the application accessing the database is concerned, there is logically only one table or one index. Physically, there may be many dozens of physical partitions that comprise the table or index. Each partition is an independent object that may be manipulated either by itself or as part of the larger object.

Partitioning is designed to facilitate the management of very large tables and indexes, by implementing the divide and conquer logic. For example, say you have a 10GB index in your database. If for some reason, you need to rebuild this index and it is not partitioned, you will have to rebuild the entire 10 GB index as a single unit of work. While it is true we could rebuild the index online, the amount of resources necessary to completely rebuild the entire 10 GB index is huge. We'll need at least 10 GB of free storage elsewhere to hold a copy of both indexes, we'll need a temporary transaction log table to record the changes made against the base table during the long time we spend rebuilding the index, and so on. On the other hand, if the index itself had been partitioned into ten, 1 GB partitions, we could rebuild each index partition individually, one by one. Now we need 10 percent of the free space we needed previously. Likewise, the index rebuild goes much faster (say ten times faster perhaps) and so the amount of transactional changes that need to be merged into the new index is much less, and so on.

In short, partitioning can make what would be otherwise daunting, or in some cases unfeasible, operations as easy as they are in a small database.

The Uses of Partitioning

There are three main reasons for using partitioning:

  • To increase availability

  • To ease administration burdens

  • To enhance DML and query performance

Increased Availability

Increased availability is derived from the fact that partitions are independent entities. The availability (or lack thereof) of a single partition in an object does not mean the object itself is unavailable. The optimizer is aware of the partitioning scheme you have implemented and will remove un-referenced partitions from the query plan accordingly. If a single partition is unavailable in a large object and your query can eliminate this partition from consideration, Oracle will successfully process the query for you. For example, we'll set up a hash-partitioned table with two partitions, each in a separate tablespace, and insert some data into it. For each row inserted into this table, the value of the EMPNO column is hashed to determine which partition (and hence tablespace in this case) the data will be placed into. Then, using the partition-extended tablename, we'll inspect the contents of each partition:

tkyte@TKYTE816> CREATE TABLE emp   2  ( empno   int,   3    ename   varchar2(20)   4  )   5  PARTITION BY HASH (empno)   6  ( partition part_1 tablespace p1,   7    partition part_2 tablespace p2   8  )   9  /      Table created.      tkyte@TKYTE816> insert into emp select empno, ename from scott.emp   2  /      14 rows created.      tkyte@TKYTE816> select * from emp partition(part_1);           EMPNO ENAME ---------- --------------------       7369 SMITH       7499 ALLEN       7654 MARTIN       7698 BLAKE       7782 CLARK       7839 KING       7876 ADAMS       7934 MILLER 8 rows selected.      tkyte@TKYTE816> select * from emp partition(part_2);           EMPNO ENAME ---------- --------------------       7521 WARD       7566 JONES       7788 SCOTT       7844 TURNER       7900 JAMES       7902 FORD 6 rows selected. 

Now, we'll make some of the data unavailable by taking one of the tablespaces offline. We will run a query that hits every partition to show that the query will fail. We will then show that a query that does not access the offline tablespace will function as normal C Oracle will eliminate the offline partition from consideration. I use a bind variable in this particular example just to demonstrate that even though Oracle does not know at query optimization time which partition will be accessed, it is able to perform this elimination nonetheless:

tkyte@TKYTE816> alter tablespace p1 offline;      Tablespace altered.      tkyte@TKYTE816> select * from emp   2  / select * from emp               * ERROR at line 1: ORA-00376: file 4 cannot be read at this time ORA-01110: data file 4: 'C:\ORACLE\ORADATA\TKYTE816\P1.DBF'      tkyte@TKYTE816> variable n number tkyte@TKYTE816> exec :n := 7844      PL/SQL procedure successfully completed.      tkyte@TKYTE816> select * from emp where empno = :n   2  /           EMPNO ENAME ---------- --------------------       7844 TURNER 

As you can see, we put one of the tablespaces offline, simulating a disk failure. The effect of this is that if we try to access the entire table, we of course cannot. However, if we attempt to access data that resides in the online partition, it is successful. When the optimizer can eliminate partitions from the plan, it will. This fact increases availability for those applications that use the partition key in their queries.

Partitions also increase availability due to the fact that downtime is reduced. If you have a 100 GB table for example, and it is partitioned into fifty 2 GB partitions, you can recover from errors that much faster. If one of the 2 GB partitions is damaged, the time to recover is now the time it takes to restore and recover a 2 GB partition, not a 100 GB table. So availability is increased in two ways; one is that many users may never even notice the data was unavailable due to partition elimination and the other is reduced downtime in the event of an error because of the significantly reduced amount of work that is performed.

Reduced Administrative Burden

The administrative burden relief is derived from the fact that performing operations on small objects is inherently easier, faster, and less resource intensive than performing the same operation on a large object. For example, if you discover that 50 percent of the rows in your table are 'chained' rows (see Chapter 6 on Tables for details on chained/migrated rows), and you would like to fix this, having a partitioned table will facilitate the operation. In order to 'fix' chained rows, you must typically rebuild the object C in this case a table. If you have one 100 GB table, you will need to perform this operation in one very large 'chunk' serially using ALTER TABLE MOVE. On the other hand, if you have 25 4GB partitions, you can rebuild each partition one by one. Alternatively, if you are doing this off-hours, you can even do the ALTER TABLE MOVE statements in parallel, in separate sessions, potentially reducing the amount of time it takes. If you are using locally partitioned indexes on this partitioned table then the index rebuilds will take significantly less time as well. Virtually everything you can do to a non-partitioned object, you can do to an individual partition of a partitioned object.

Another factor to consider, with regard to partitions and administration, is the use of 'sliding windows' of data in data warehousing and archiving. In many cases, you need to keep the last N units of time data online. For example, you need to keep the last twelve months online or the last five years. Without partitions, this was generally a massive INSERT followed by a massive DELETE. Lots of DML, lots of redo and rollback generated. Now with partitions we can simply:

So, we can now very easily support extremely large objects containing time-sensitive information. The old data can easily be removed from the partitioned table and simply dropped if you do not need it, or it can be archived off elsewhere. New data can be loaded into a separate table, so as to not affect the partitioned table until the loading, indexing, and so on, is complete. We will take a look at a complete example of a sliding window later.

Enhanced DML and Query Performance

The last general benefit of partitioning is in the area of enhanced query and DML performance. We'll take a look at each individually to see what benefits we might expect.

Enhanced DML performance refers to the potential to perform parallel DML (or PDML). During PDML Oracle uses many threads or processes to perform your INSERT, UPDATE, or DELETE instead of a single serial process. On a multi-CPU machine with plenty of I/O bandwidth, the potential speed-up may be large for mass DML operations. Unlike parallel query (the processing of a SELECT statement by many processes/threads), PDML requires partitioning (there is a special case of a parallel, direct path insert via the /*+ APPEND */ hint that does not require partitioning). If your tables are not partitioned, you cannot perform these operations in parallel. Oracle will assign a maximum degree of parallelism to the object, based on the number of physical partitions it has.

You should not look to PDML as a feature to speed up your OLTP based applications. There is often confusion with regards to this. Many times, I hear 'Parallel operations must be faster than serial operations C they just have to be'. This is not always the case. Some operations performed in parallel may be many times slower than the same thing done serially. There is a certain overhead associated with setting up parallel operations; more co-ordination that must take place. Additionally, parallel operations are not things you should consider doing in a mainly online, OLTP system C it just would not make sense. Parallel operations are designed to fully, and totally, maximize the utilization of a machine. They are designed so that a single user can completely use all of the disks, CPU, and memory on the machine. In a data warehouse C lots of data, few users C this is something you want to achieve. In an OLTP system (tons of users all doing short, fast transactions) giving a user the ability to fully take over the machine resources is not a very scalable solution.

This sounds contradictory C we use parallel query to scale up, how could it not be scalable? When applied to an OLTP system, the statement is quite accurate however. Parallel query is not something that scales up as the number of concurrent users scale up. Parallel query was designed to allow a single session to generate as much work as a hundred concurrent sessions would. In our OLTP system, we really do not want a single user to generate the work of a hundred users.

PDML is useful in a large data warehousing environment to facilitate bulk updates to massive amounts of data. The PDML operation is executed much in the way a distributed query would be executed by Oracle, with each partition acting like a separate database instance. Each partition is updated by a separate thread with its own transaction (and hence its own rollback segment hopefully) and after they are all done, the equivalent of a fast two phase commit is performed to commit the separate, independent transactions. Due to this architecture, there are certain limitations associated with PDML. For example, triggers are not supported during a PDML operation. This is a reasonable limitation in my opinion since triggers would tend to add a large amount of overhead to the update and you are using PDML in order to go fast C the two features don't go together. Also, there are certain declarative RI constraints that are not supported during the PDML, since each partition is done as a separate transaction in the equivalent of a separate session. Self-referential integrity is not supported for example. Consider the deadlocks and other locking issues that would occur if it were supported.

In the area of query performance, partitioning comes into play with two types of specialized operations:

Again, much like PDML, you should not look toward partitions as a way to massively improve performance in an OLTP system. Partition elimination is useful where you have full scans of large objects. Utilizing partition elimination, you can avoid full scanning large pieces of an object. That is where the increase in performance would be derived from this feature. However, in an OLTP environment you are not full scanning large objects (if you are, you have a serious design flaw). Even if you partition your indexes, the speed up you achieve by scanning a smaller index is miniscule C if you actually achieve a speed up at all. If some of your queries use an index and they cannot be used to eliminate all but one partition, you may find your queries actually run slower after partitioning since you now have 5, 10, or 20 small indexes to scan, instead of one larger index. We will investigate this in much more detail later when we look at the types of partitioned indexes available to us. There are opportunities to gain efficiency in an OLTP system with partitions C they may be used to increase concurrency by decreasing contention for example. They can be used to spread the modifications of a single table out over many physical partitions. Instead of having a single table segment with a single index segment C you might have 20 table partitions and 20 index partitions. It could be like having 20 tables instead of one C hence decreasing contention for this shared resource during modifications.

As for parallel operations, as stated previously, you do not want to be doing a parallel query in an OLTP system. You would reserve your use of parallel operations for the DBA to perform rebuilds, creating indexes, analyzing tables, and so on. The fact is that in an OLTP system your queries should already be characterized by very fast index accesses C partitioning will not speed that up very much, if at all. This does not mean 'avoid partitioning for OLTP', it means do not expect massive improvements in performance by simply adding partitioning. Your applications are not able to take advantage of the times where partitioning is able to enhance query performance in this case.

Now, in a data warehouse/decision support system, partitioning is not only a great administrative tool, but it is something to speed up processing. For example, you may have a large table, on which you need to perform an ad-hoc query. You always do the ad-hoc query by sales quarter C each sales quarter contains hundreds of thousands of records and you have millions of online records. So, you want to query a relatively small slice of the entire data set but it is not really feasible to index it based on the sales quarter. This index would point to hundreds of thousands of records and doing the index range scan in this way would be terrible (refer to Chapter 7 on Indexes for more details on this). A full table scan is called for to process many of your queries, but we end up having to scan millions of records, most of which won't apply to our query. Using an intelligent partitioning scheme, we can segregate the data by quarter so that when we go to query the data for any given quarter, we will end up full scanning just that quarter's data. This is the best of all possible solutions.

In addition, in a warehousing/decision support system environment, parallel query is used frequently. Here, operations such as parallel index range scans, or parallel fast full index scans are not only meaningful, but also beneficial to us. We want to maximize our use of all available resources, and parallel query is the way to do it. So, in this environment, partitioning stands a very good chance of speeding up processing.

If I were to put the benefits of partitioning in some sort of order, it would be:

  1. Increases availability of data C good for all system types

  2. Eases administration of large objects by removing large objects from the database C good for all system types

  3. Improves the performance of certain DML and queries C beneficial mainly in a large warehouse environment

  4. Reduces contention on high insert OLTP systems (such as an audit trail table) by spreading the inserts out across many separate partitions (spreads the hot spots around)

How Partitioning Works

In this section, we'll look at the partitioning schemes offered by Oracle 8i. There are three partitioning schemes for tables and two for indexes. Within the two schemes for index partitioning, there are various classes of partitioned indexes. We'll look at the benefits of each and at the differences between them. We'll also look at when to apply which schemes to different application types.

Table Partitioning Schemes

There are currently three methods by which you can partition tables in Oracle:

The following code and diagrams provide a visual demonstration of these different methods. Additionally, the CREATE TABLE statements are set up in such a way as to give an overview of the syntax of a partitioned table. The first type we will look at is a range partitioned table:

tkyte@TKYTE816> CREATE TABLE range_example   2  ( range_key_column date,   3    data             varchar2(20)   4  )   5  PARTITION BY RANGE (range_key_column)   6  ( PARTITION part_1 VALUES LESS THAN   7         (to_date('01-jan-1995','dd-mon-yyyy')),   8    PARTITION part_2 VALUES LESS THAN   9         (to_date('01-jan-1996','dd-mon-yyyy'))  10  )  11  /      Table created. 

The following diagram shows that Oracle will inspect the value of the RANGE_KEY_COLUMN and based on its value, insert it into one of the two partitions:

click to expand

You might wonder what would happen if the column used to determine the partition is modified. There are two cases to consider:

We can observe these behaviors easily. Below we'll insert a row into PART_1 of the above table. We will then update it to a value that allows it to remain in PART_1 - and that'll succeed. Next, we'll update the RANGE_KEY_COLUMN to a value that would cause it to belong in PART_2 - that will raise an error since we did not explicitly enable row movement. Lastly, we'll alter the table to support row movement and see the side effect of doing so:

tkyte@TKYTE816> insert into range_example   2  values ( to_date( '01-jan-1994', 'dd-mon-yyyy' ), 'application data' );      1 row created.      tkyte@TKYTE816> update range_example   2  set range_key_column = range_key_column+1   3  /      1 row updated. 

As expected, this succeeds, the row remains in partition PART_1. Next, we'll observe the behavior when the update would cause the row to move:

tkyte@TKYTE816> update range_example   2  set range_key_column = range_key_column+366   3  / update range_example        * ERROR at line 1: ORA-14402: updating partition key column would cause a partition change 

That immediately raised an error. In Oracle 8.1.5 and later releases, we can enable row movement on this table to allow the row to move from partition to partition. This functionality is not available on Oracle 8.0 - you must delete the row and re-insert it in that release. You should be aware of a subtle side effect of doing this, however. It is one of two cases where the ROWID of a row will change due to an update (the other is an update to the primary key of an IOT - index organized table. The universal ROWID will change for that row too):

tkyte@TKYTE816> select rowid from range_example   2  /      ROWID ------------------ AAAHeRAAGAAAAAKAAA      tkyte@TKYTE816> alter table range_example enable row movement   2  /      Table altered.      tkyte@TKYTE816> update range_example   2  set range_key_column = range_key_column+366   3  /      1 row updated. tkyte@TKYTE816> select rowid from range_example   2  /      ROWID ------------------ AAAHeSAAGAAAABKAAA 

So, as long as you understand that the ROWID of the row will change on this update, enabling row movement will allow you to update partition keys.

The next example is that of a hash-partitioned table. Here Oracle will apply a hash function to the partition key to determine in which of the N partitions the data should be placed. Oracle recommends that N be a number that is a power of 2 (2, 4, 8, 16, and so on) to achieve the best overall distribution. Hash partitioning is designed to achieve a good spread of data across many different devices (disks). The hash key chosen for a table should be a column or set of columns that are as unique as possible in order to provide for a good spread of values. If you chose a column that has only four values and you use two partitions then they may very well all end up hashing to the same partition quite easily, bypassing the value of partitioning in the first place!

We will create a hash table with two partitions in this case:

tkyte@TKYTE816> CREATE TABLE hash_example   2  ( hash_key_column   date,   3    data              varchar2(20)   4  )   5  PARTITION BY HASH (hash_key_column)   6  ( partition part_1 tablespace p1,   7    partition part_2 tablespace p2   8  )   9  /      Table created. 

The diagram shows that Oracle will inspect the value in the hash_key_column, hash it, and determine which of the two partitions a given row will appear in:

click to expand

Lastly, we'll look at an example of composite partitioning, which is a mixture of range and hash. Here we are using a different set of columns to range partition by from the set of columns used to hash on. This is not mandatory - we could use the same set of columns for both:

tkyte@TKYTE816> CREATE TABLE composite_example   2  ( range_key_column   date,   3    hash_key_column    int,   4    data               varchar2(20)   5  )   6  PARTITION BY RANGE (range_key_column)   7  subpartition by hash(hash_key_column) subpartitions 2   8  (   9  PARTITION part_1  10       VALUES LESS THAN(to_date('01-jan-1995','dd-mon-yyyy'))  11       (subpartition part_1_sub_1,  12        subpartition part_1_sub_2  13       ),  14  PARTITION part_2  15      VALUES LESS THAN(to_date('01-jan-1996','dd-mon-yyyy'))  16      (subpartition part_2_sub_1,  17       subpartition part_2_sub_2  18      )  19  )  20  /      Table created. 

In composite partitioning, Oracle will first apply the range partitioning rules to figure out which range the data falls into. Then, it will apply the hash function to decide which physical partition the data should finally be placed into:

click to expand

In general, range partitioning is useful when you have data that is logically segregated by some value(s). Time-based data immediately comes to the forefront as a classic example. Partition by 'Sales Quarter'. Partition by 'Fiscal Year'. Partition by 'Month'. Range partitioning is able to take advantage of partition elimination in many cases, including use of exact equality and ranges - less than, greater than, between, and so on.

Hash partitioning is suitable for data that has no natural ranges by which you can partition. For example, if you had to load a table full of census-related data, there might not be an attribute by which it would make sense to range partition by. However, you would still like to take advantage of the administrative, performance, and availability enhancements offered by partitioning. Here, you would simply pick a unique or almost unique set of columns to hash on. This would achieve an even distribution of data across as many partitions as you like. Hash partitioned objects can take advantage of partition elimination when exact equality or IN ( value, value, ... ) is used but not when ranges of data are used.

Composite partitioning is useful when you have something logical by which you can range partition, but the resulting range partitions are still too large to manage effectively. You can apply the range partitioning and then further divide each range by a hash function. This will allow you to spread I/O requests out across many disks in any given large partition. Additionally, you may achieve partition elimination at three levels now. If you query on the range partition key, Oracle is able to eliminate any range partitions that do not meet your criteria. If you add the hash key to your query, Oracle can eliminate the other hash partitions within that range. If you just query on the hash key (not using the range partition key), Oracle will query only those hash sub-partitions that apply from each range partition.

It is recommended that if there is something by which it makes sense to range partition your data, you should use that over hash partitioning. Hash partitioning adds many of the salient benefits of partitioning, but is not as useful as range partitioning when it comes to partition elimination. Using hash partitions within range partitions is advisable when the resulting range partitions are too large to manage or when you want to utilize PDML or parallel index scanning against a single range partition.

Partitioning Indexes

Indexes, like tables, may be partitioned. There are two possible methods to partition indexes. You may either:

The following diagrams display the difference between a local and a global index:

click to expand

click to expand

In the case of a globally partitioned index, note that the number of index partitions may in fact be different than the number of table partitions.

Since global indexes may be partitioned by range only, you must use local indexes if you wish to have a hash or composite partitioned index. The local index will be partitioned using the same scheme as the underlying table.

Local Indexes

Local indexes are what most partition implementations have used in my experience. This is because most of the partitioning implementations I've seen have been data warehouses. In an OLTP system, global indexes would be more common. Local indexes have certain properties that make them the best choice for most data warehouse implementations. They support a more available environment (less down time) since problems will be isolated to one range or hash of data. A global index on the other hand, since it can point to many table partitions, may become a point of failure, rendering all partitions inaccessible to certain queries. Local indexes are more flexible when it comes to partition maintenance operations. If the DBA decides to move a table partition, only the associated local index partition needs to be rebuilt. With a global index all index partitions must be rebuilt. The same is true with 'sliding window' implementations where old data is aged out of the partition and new data is aged in. No local indexes will be in need of a rebuild but all global indexes will be. In some cases, Oracle can take advantage of the fact that the index is locally partitioned with the table, and will develop optimized query plans based on that. With global indexes, there is no such relationship between the index and table partitions. Local indexes also facilitate a partition point-in-time recovery operation. If a single partition needs to be recovered to an earlier point in time than the rest of the table for some reason, all locally partitioned indexes can be recovered to that same point in time. All global indexes would need to be rebuilt on this object.

Oracle makes a distinction between these two types of local indexes:

Both types of indexes are able take advantage of partition elimination, both can support uniqueness (as long as the non-prefixed index includes the partition key), and so on. The fact is that a query that uses a local prefixed index will always allow for index partition elimination, whereas a query that uses a local non-prefixed index might not. This is why local non-prefixed indexes are said to be 'slower'; they do not enforce partition elimination (but they do support it). Additionally, as we'll see below, the optimizer will treat local non-prefixed indexes differently from local prefixed indexes when performing certain operations. The Oracle documentation stresses that:

local prefixed indexes provide better performance than local non-prefixed indexes because they minimize the number of indexes probed

It should really read more like:

locally partitioned indexes that are used in QUERIES that reference the entire partition key in them provide better performance than for QUERIES that do not reference the partition key

There is nothing inherently better about a local prefixed index, as opposed to a local non-prefixed index, when that index is used as the initial path to the table in a query. What I mean by that is that if the query can start with SCAN AN INDEX as the first step, there isn't much difference between a prefixed and non-prefixed index. Below, when we are looking at using partitioned indexes in joins, we'll see the difference between a prefixed and non-prefixed index.

For the query that starts with an index access, it all really depends on the predicate in your query. A small example will help demonstrate this. We'll set up a table, PARTITIONED_TABLE, and create a local prefixed index LOCAL_PREFIXED on it. Additionally, we'll add a local non-prefixed index LOCAL_NONPREFIXED:

tkyte@TKYTE816> CREATE TABLE partitioned_table   2  ( a int,   3    b int   4  )   5  PARTITION BY RANGE (a)   6  (   7  PARTITION part_1 VALUES LESS THAN(2) ,   8  PARTITION part_2 VALUES LESS THAN(3)   9  )  10  /      Table created.      tkyte@TKYTE816> create index local_prefixed on partitioned_table (a,b) local;      Index created.      tkyte@TKYTE816> create index local_nonprefixed on partitioned_table (b) local;      Index created. 

Now, we'll insert some data into one partition and mark the indexes UNUSABLE:

tkyte@TKYTE816> insert into partitioned_table values ( 1, 1 );      1 row created.      tkyte@TKYTE816> alter index local_prefixed modify partition part_2 unusable;      Index altered.      tkyte@TKYTE816> alter index local_nonprefixed modify partition part_2 unusable;      Index altered. 

Setting these index partitions to UNUSABLE will prevent Oracle from accessing these specific index partitions. It will be as is they suffered 'media failure' - they are unavailable. So, now we'll query the table to see what index partitions are needed by different queries:

tkyte@TKYTE816> set autotrace on explain tkyte@TKYTE816> select * from partitioned_table where a = 1 and b = 1;               A          B ---------- ----------          1          1           Execution Plan ----------------------------------------------------------    0      SELECT STATEMENT Optimizer=CHOOSE (Cost=1 Card=1 Bytes=26)    1    0   INDEX (RANGE SCAN) OF 'LOCAL_PREFIXED' (NON-UNIQUE) (Cost=1 

So, the query that uses LOCAL_PREFIX succeeds. The optimizer was able to exclude PART_2 of LOCAL_PREFIX from consideration because we specified A=1 in the query. Partition elimination kicked in for us. For the second query:

tkyte@TKYTE816> select * from partitioned_table where b = 1; ERROR: ORA-01502: index 'TKYTE.LOCAL_NONPREFIXED' or partition of such index is in unusable state      no rows selected      Execution Plan ----------------------------------------------------------    0      SELECT STATEMENT Optimizer=CHOOSE (Cost=1 Card=2 Bytes=52)    1    0   PARTITION RANGE (ALL)    2    1     TABLE ACCESS (BY LOCAL INDEX ROWID) OF 'PARTITIONED_TABLE'    3    2       INDEX (RANGE SCAN) OF 'LOCAL_NONPREFIXED' (NON-UNIQUE) 

Here the optimizer was not able to remove PART_2 of LOCAL_NONPREFIXED from consideration. Herein lies a performance issue with local non-prefixed indexes. They do not make you use the partition key in the predicate as a prefixed index does. It is not that prefixed indexes are better, it is that in order to use them, you must use a query that allows for partition elimination.

If we drop LOCAL_PREFIXED index, and rerun the original successful query:

tkyte@TKYTE816> select * from partitioned_table where a = 1 and b = 1;               A          B ---------- ----------          1          1      Execution Plan ----------------------------------------------------------    0      SELECT STATEMENT Optimizer=CHOOSE (Cost=1 Card=1 Bytes=26)    1    0   TABLE ACCESS (BY LOCAL INDEX ROWID) OF 'PARTITIONED_TABLE'    2    1     INDEX (RANGE SCAN) OF 'LOCAL_NONPREFIXED' (NON-UNIQUE) (Cost=1 

Well, this might be a surprising result. This is almost the same exact plan for the query that just failed, but this time it works. That is because the optimizer is able to perform partition elimination even for non-prefixed local indexes (there is no PARTITION RANGE(ALL) step in this plan).

If you frequently query the above table with the queries:

select ... from partitioned_table where a = :a and b = :b; select ... from partitioned_table where b = :b; 

then, you might consider using a local non-prefixed index on (b,a) - that index would be useful for both of the above queries. The local prefixed index on (a,b) would only be useful for the first query.

However, when it comes to using the partitioned indexes in joins, the results may be different. In the above examples, Oracle was able to look at the predicate and at optimization time determine that it would be able to eliminate partitions (or not). This was clear from the predicate (even if the predicate used bind variables this would be true). When the index is used as the initial, primary access method, there are no real differences between prefixed and non-prefixed indexes. When we join to a local prefixed index, however, this changes. Consider a simple range partitioned table such as:

tkyte@TKYTE816> CREATE TABLE range_example   2  ( range_key_column date,   3    x                int,   4    data             varchar2(20)   5  )   6  PARTITION BY RANGE (range_key_column)   7  ( PARTITION part_1 VALUES LESS THAN   8         (to_date('01-jan-1995','dd-mon-yyyy')),   9    PARTITION part_2 VALUES LESS THAN  10         (to_date('01-jan-1996','dd-mon-yyyy'))  11  )  12  /      Table created.      tkyte@TKYTE816> alter table range_example   2  add constraint range_example_pk   3  primary key (range_key_column,x)   4  using index local   5  /      Table altered.      tkyte@TKYTE816> insert into range_example values ( to_date( '01-jan-1994' ), 1, 'xxx' );      1 row created.      tkyte@TKYTE816> insert into range_example values ( to_date( '01-jan-1995' ), 2, 'xxx' );      1 row created. 

This table will start out with a local prefixed primary key index. In order to see the difference between the prefixed and non-prefixed indexes, we'll need to create another table. We will use this table to drive a query against our RANGE_EXAMPLE table above. The TEST table will simply be used as the driving table in a query that will do a 'nested loops' type of join to the RANGE_EXAMPLE table:

tkyte@TKYTE816> create table test ( pk , range_key_column , x,   2                                 constraint test_pk primary key(pk) )   3  as   4  select rownum, range_key_column, x from range_example   5  / Table created.      tkyte@TKYTE816> set autotrace on explain      tkyte@TKYTE816> select * from test, range_example   2    where test.pk = 1   3    and test.range_key_column = range_example.range_key_column   4    and test.x = range_example.x   5  /              PK RANGE_KEY          X RANGE_KEY          X DATA ---------- --------- ---------- --------- ---------- --------------------          1 01-JAN-94          1 01-JAN-94          1 xxx           Execution Plan ----------------------------------------------------------    0      SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=1 Bytes=69)    1    0   NESTED LOOPS (Cost=2 Card=1 Bytes=69)    2    1     TABLE ACCESS (BY INDEX ROWID) OF 'TEST' (Cost=1 Card=1    3    2       INDEX (RANGE SCAN) OF 'TEST_PK' (UNIQUE) (Cost=1 Card=1)    4    1     PARTITION RANGE (ITERATOR)    5    4       TABLE ACCESS (BY LOCAL INDEX ROWID) OF 'RANGE_EXAMPLE'    6    5         INDEX (UNIQUE SCAN) OF 'RANGE_EXAMPLE_PK' (UNIQUE) 

The query plan of the above would be processed in the following manner:

  1. Use the index TEST_PK to find the rows in the table TEST that match the predicate test.pk = 1.

  2. Access the table TEST to pick up the remaining columns in TEST - the range_key_column and x specifically.

  3. Using these values we picked up in (2), use the RANGE_EXAMPLE_PK to find the rows in a single partition of RANGE_EXAMPLE.

  4. Access the single RANGE_EXAMPLE partition to pick up the data column.

That seems straightforward enough - but look what happens when we turn the index into a non-prefixed index by reversing range_key_column and x:

tkyte@TKYTE816> alter table range_example   2  drop constraint range_example_pk   3  /      Table altered.      tkyte@TKYTE816> alter table range_example   2  add constraint range_example_pk   3  primary key (x,range_key_column)   4  using index local   5  /      Table altered. tkyte@TKYTE816> select * from test, range_example   2    where test.pk = 1   3    and test.range_key_column = range_example.range_key_column   4    and test.x = range_example.x   5  /              PK RANGE_KEY          X RANGE_KEY          X DATA ---------- --------- ---------- --------- ---------- --------------------          1 01-JAN-94          1 01-JAN-94          1 xxx           Execution Plan ----------------------------------------------------------    0      SELECT STATEMENT Optimizer=CHOOSE (Cost=2 Card=1 Bytes=69)    1    0   NESTED LOOPS (Cost=2 Card=1 Bytes=69)    2    1     TABLE ACCESS (BY INDEX ROWID) OF 'TEST' (Cost=1 Card=1    3    2       INDEX (RANGE SCAN) OF 'TEST_PK' (UNIQUE) (Cost=1 Card=1)    4    1     PARTITION RANGE (ITERATOR)    5    4       TABLE ACCESS (FULL) OF 'RANGE_EXAMPLE' (Cost=1 Card=164 

All of a sudden, Oracle finds this new index to be too expensive to use. This is one case where the use of a prefixed index would provide a measurable benefit.

The bottom line here is that you should not be afraid of non-prefixed indexes or consider them as major performance inhibitors. If you have many queries that could benefit from a non-prefixed index as outlined above, then you should consider using it. The main concern is to ensure that your queries contain predicates that allow for index partition elimination whenever possible. The use of prefixed local indexes enforces that consideration. The use of non-prefixed indexes does not. Consider also, how the index will be used - if it will be used as the first step in a query plan, there are not many differences between the two types of indexes. If, on the other hand, the index is intended to be used primarily as an access method in a join, as above, consider prefixed indexes over non-prefixed indexes. If you can use the local prefixed index, try to use it.

Local Indexes and Uniqueness

In order to enforce uniqueness, and that includes a UNIQUE constraint or PRIMARY KEY constraints, your partitioning key must be included in the constraint itself. This is the largest impact of a local index, in my opinion. Oracle only enforces uniqueness within an index partition, never across partitions. What this implies, for example, is that you cannot range partition on a TIMESTAMP field, and have a primary key on the ID which is enforced using a locally partitioned index. Oracle will utilize a single global index to enforce uniqueness.

For example, if you execute the following CREATE TABLE statement in a schema that owns no other objects (so we can see exactly what objects are created by looking at every segment this user owns) we'll find:

tkyte@TKYTE816> CREATE TABLE partitioned   2  ( timestamp date,   3    id        int primary key   4  )   5  PARTITION BY RANGE (timestamp)   6  (   7  PARTITION part_1 VALUES LESS THAN   8  ( to_date('01-jan-2000','dd-mon-yyyy') ) ,   9  PARTITION part_2 VALUES LESS THAN  10  ( to_date('01-jan-2001','dd-mon-yyyy') )  11  )  12  /      Table created.      tkyte@TKYTE816> select segment_name, partition_name, segment_type   2    from user_segments;      SEGMENT_NAME    PARTITION_NAME  SEGMENT_TYPE --------------- --------------- ------------------ PARTITIONED     PART_2          TABLE PARTITION PARTITIONED     PART_1          TABLE PARTITION SYS_C003582                     INDEX 

The SYS_C003582 index is not partitioned - it cannot be. This means you lose many of the availability features of partitions in a data warehouse. If you perform partition operations, as you would in a data warehouse, you lose the ability to do them independently. That is, if you add a new partition of data, your single global index must be rebuilt - virtually every partition operation will require a rebuild. Contrast that with a local index only implementation whereby the local indexes never need to be rebuilt unless it is their partition that was operated on.

If you try to trick Oracle by realizing that a primary key can be enforced by a non-unique index as well as a unique index, you'll find that it will not work either:

tkyte@TKYTE816> CREATE TABLE partitioned   2  ( timestamp date,   3    id        int   4  )   5  PARTITION BY RANGE (timestamp)   6  (   7  PARTITION part_1 VALUES LESS THAN   8  ( to_date('01-jan-2000','dd-mon-yyyy') ) ,   9  PARTITION part_2 VALUES LESS THAN  10  ( to_date('01-jan-2001','dd-mon-yyyy') )  11  )  12  /      Table created.      tkyte@TKYTE816> create index partitioned_index   2  on partitioned(id)   3  LOCAL   4  /      Index created.      tkyte@TKYTE816> select segment_name, partition_name, segment_type   2    from user_segments;      SEGMENT_NAME      PARTITION_NAME  SEGMENT_TYPE ----------------- --------------- ------------------ PARTITIONED       PART_2          TABLE PARTITION PARTITIONED       PART_1          TABLE PARTITION PARTITIONED_INDEX PART_2          INDEX PARTITION PARTITIONED_INDEX PART_1          INDEX PARTITION      tkyte@TKYTE816> alter table partitioned   2  add constraint partitioned_pk   3  primary key(id)   4  / alter table partitioned * ERROR at line 1: ORA-01408: such column list already indexed 

Here, Oracle attempts to create a global index on ID but finds that it cannot since an index already exists. The above statements would work if the index we created was not partitioned - Oracle would have used that index to enforce the constraint.

The reason that uniqueness cannot be enforced, unless the partition key is part of the constraint, is twofold. Firstly, if Oracle allowed this it would void most all of the advantages of partitions. Availability and scalability would be lost, as each, and every, partition would always have to be available and scanned to do any inserts and updates. The more partitions you have, the less available the data is. The more partitions you have, the more index partitions you must scan, the less scalable partitions become. Instead of providing availability and scalability, this would actually decrease both.

Additionally, Oracle would have to effectively serialize inserts and updates to this table at the transaction level. This is because of the fact that if we add ID=1 to PART_1, Oracle would have to somehow prevent anyone else from adding ID=1 to PART_2. The only way to do this would be to prevent them from modifying index partition PART_2 since there isn't anything to really 'lock' in that partition.

In an OLTP system, unique constraints must be system enforced (enforced by Oracle) to ensure the integrity of data. This implies that the logical model of your system will have an impact on the physical design. Uniqueness constraints will either drive the underlying table partitioning scheme, driving the choice of the partition keys, or alternatively, these constraints will point you towards the use of global indexes instead. We'll take a look at global indexes in more depth now.

Global Indexes

Global indexes are partitioned using a scheme different from their underlying table. The table might be partitioned by the TIMESTAMP column into ten partitions, and a global index on that table could be partitioned into five partitions by the REGION column. Unlike local indexes, there is only one class of global index, and that is a prefixed global index. There is no support for a global index whose index key does not begin with the partition key.

Following on from our previous example, here is a quick example of the use of a global index. It shows that a global partitioned index can be used to enforce uniqueness for a primary key, so you can have partitioned indexes that enforce uniqueness, but do not include the partition key of TABLE. The following example creates a table partitioned by TIMESTAMP that has an index that is partitioned by ID:

tkyte@TKYTE816> CREATE TABLE partitioned   2  ( timestamp date,   3    id        int   4  )   5  PARTITION BY RANGE (timestamp)   6  (   7  PARTITION part_1 VALUES LESS THAN   8  ( to_date('01-jan-2000','dd-mon-yyyy') ) ,   9  PARTITION part_2 VALUES LESS THAN  10  ( to_date('01-jan-2001','dd-mon-yyyy') )  11  )  12  /      Table created.      tkyte@TKYTE816> create index partitioned_index   2  on partitioned(id)   3  GLOBAL   4  partition  by range(id)   5  (   6  partition part_1 values less than(1000),   7  partition part_2 values less than (MAXVALUE)   8  )   9  /      Index created. 

Note the use of MAXVALUE in this index. MAXVALUE can be used in any range-partitioned table as well as in the index. It represents an 'infinite upper bound' on the range. In our examples so far, we've used hard upper bounds on the ranges (values less than <some value>). However, a global index has a requirement that the highest partition (the last partition) must have a partition bound whose value is MAXVALUE. This ensures that all rows in the underlying table can be placed in the index.

Now, completing this example, we'll add our primary key to the table:

tkyte@TKYTE816> alter table partitioned add constraint   2  partitioned_pk   3  primary key(id)   4  /      Table altered. 

It is not evident from the above that Oracle is using the index we created to enforce the primary key (it is to me because I know that Oracle is using it) so we can prove it by a magic query against the 'real' data dictionary. You'll need an account that has SELECT granted on the base tables, or the SELECT ANY TABLE privilege, in order to execute this query:

tkyte@TKYTE816> select  t.name    table_name   2       ,  u.name    owner   3       ,  c.name    constraint_name   4       ,  i.name    index_name   5       ,   decode(bitand(i.flags, 4), 4, 'Yes',   6                decode( i.name, c.name, 'Possibly', 'No') ) generated   7   from sys.cdef$ cd   8      , sys.con$  c   9      , sys.obj$  t  10      , sys.obj$  i  11      , sys.user$ u  12  where cd.type#      between 2 and 3  13    and      cd.con#    = c.con#  14    and      cd.obj#    = t.obj#  15    and      cd.enabled = i.obj#  16    and       c.owner#       = u.user#  17    and      c.owner# = uid  18  /      TABLE_NAME   OWNER CONSTRAINT_NAME INDEX_NAME        GENERATE ------------ ----- --------------- ----------------- -------- PARTITIONED  TKYTE PARTITIONED_PK  PARTITIONED_INDEX No 

This query shows us the index used to enforce a given constraint, and tries to 'guess' as to whether the index name is a system generated one or not. In this case, it shows us that the index being used for our primary key is in fact the PARTITIONED_INDEX we just created (and the name was definitely not system generated).

To show that Oracle will not allow us to create a non-prefixed global index, we only need try:

tkyte@TKYTE816> create index partitioned_index2   2  on partitioned(timestamp,id)   3  GLOBAL   4  partition  by range(id)   5  (   6  partition part_1 values less than(1000),   7  partition part_2 values less than (MAXVALUE)   8  )   9  / partition  by range(id)                       * ERROR at line 4: ORA-14038: GLOBAL partitioned index must be prefixed 

The error message is pretty clear. The global index must be prefixed.

So, when would you use a global index? We'll take a look at two system types, data warehouse and OLTP, and see when they might apply.

Data Warehousing and Global Indexes

It is my opinion that these two things are mutually exclusive. A data warehouse implies certain things; large amounts of data coming in and going out, high probability of a failure of a disk somewhere, and so on. Any data warehouse that uses a 'sliding window' would want to avoid the use of global indexes. Here is an example of what I mean by a sliding window of data and the impact of a global index on it:

tkyte@TKYTE816> CREATE TABLE partitioned   2  ( timestamp date,   3    id        int   4  )   5  PARTITION BY RANGE (timestamp)   6  (   7  PARTITION fy_1999 VALUES LESS THAN   8  ( to_date('01-jan-2000','dd-mon-yyyy') ) ,   9  PARTITION fy_2000 VALUES LESS THAN  10  ( to_date('01-jan-2001','dd-mon-yyyy') ) ,  11  PARTITION the_rest VALUES LESS THAN  12  ( maxvalue )  13  )  14  /      Table created.      tkyte@TKYTE816> insert into partitioned partition(fy_1999)   2  select to_date('31-dec-1999')-mod(rownum,360), object_id   3  from all_objects   4  /      21914 rows created.      tkyte@TKYTE816> insert into partitioned partition(fy_2000)   2  select to_date('31-dec-2000')-mod(rownum,360), object_id   3  from all_objects   4  /      21914 rows created.      tkyte@TKYTE816> create index partitioned_idx_local   2  on partitioned(id)   3  LOCAL   4  /      Index created.      tkyte@TKYTE816> create index partitioned_idx_global   2  on partitioned(timestamp)   3  GLOBAL   4  /      Index created. 

So, this sets up our 'warehouse' table. The data is partitioned by fiscal year and we have the last two years worth of data online. This table has two indexes; one is LOCAL and the other GLOBAL. Notice that I left an empty partition THE_REST at the end of the table. This will facilitate sliding new data in quickly. Now, it is the end of the year and we would like to:

  1. Remove the oldest fiscal year data. We do not want to lose this data forever, we just want to age it out and archive it.

  2. Add the newest fiscal year data. It will take a while to load it, transform it, index it, and so on. We would like to do this work without impacting the availability of the current data, if at all possible.

The steps I might take would be:

tkyte@TKYTE816> create table fy_1999 ( timestamp date, id int );      Table created.      tkyte@TKYTE816> create index fy_1999_idx on fy_1999(id)   2  /      Index created.      tkyte@TKYTE816> create table fy_2001 ( timestamp date, id int );      Table created.      tkyte@TKYTE816> insert into fy_2001   2  select to_date('31-dec-2001')-mod(rownum,360), object_id   3  from all_objects   4  /      21922 rows created.      tkyte@TKYTE816> create index fy_2001_idx on fy_2001(id) nologging   2  /      Index created. 

What I've done here is to set up an empty 'shell' table and index for the oldest data. What we will do is turn the current full partition into an empty partition and create a 'full' table, with the FY_1999 data in it. Also, I've completed all of the work necessary to have the FY_2001 data ready to go. This would have involved verifying the data, transforming it - whatever complex tasks you need to undertake to get it ready.

Now we are ready to update the 'live' data:

tkyte@TKYTE816> alter table partitioned   2  exchange partition fy_1999   3  with table fy_1999   4  including indexes   5  without validation   6  /      Table altered.      tkyte@TKYTE816> alter table partitioned   2  drop partition fy_1999   3  /      Table altered. 

This is all you need to do to 'age' the old data out. We turned the partition into a full table and the empty table into a partition. This was a simple data dictionary update - no large amount of I/O took place, it just happened. We can now export that table (perhaps using a transportable tablespace) out of our database for archival purposes. We could re-attach it quickly if we ever need to.

Next, we want to slide in the new data:

tkyte@TKYTE816> alter table partitioned   2  split partition the_rest   3  at ( to_date('01-jan-2002','dd-mon-yyyy') )   4  into ( partition fy_2001, partition the_rest )   5  /      Table altered.      tkyte@TKYTE816> alter table partitioned   2  exchange partition fy_2001   3  with table fy_2001   4  including indexes   5  without validation   6  /      Table altered. 

Again, this was instantaneous - a simple data dictionary update. Splitting the empty partition takes very little real-time since there never was, and never will be data in it. That is why I placed an extra empty partition at the end of the table, to facilitate the split. Then, we exchange the newly created empty partition with the full table and the full table with an empty partition. The new data is online.

Looking at our indexes however, we'll find:

tkyte@TKYTE816> select index_name, status from user_indexes;      INDEX_NAME             STATUS ---------------------- ---------- FY_1999_IDX            VALID FY_2001_IDX            VALID PARTITIONED_IDX_GLOBAL UNUSABLE PARTITIONED_IDX_LOCAL  N/A 

The global index is of course unusable after this operation. Since each index partition can point to any table partition and we just took away a partition and added a partition, that index is invalid. It has entries that point into the partition we dropped. It has no entries that point into the partition we just added. Any query that would make use of this index will fail:

tkyte@TKYTE816> select count(*)   2  from partitioned   3  where timestamp between sysdate-50 and sysdate; select count(*) * ERROR at line 1: ORA-01502: index 'TKYTE.PARTITIONED_IDX_GLOBAL' or partition of such index is in unusable state 

We could set SKIP_UNUSABLE_INDEXES=TRUE, but then we lose the performance the index was giving us (this works in Oracle 8.1.5 and later versions; prior to that, SELECT statement would still try to use the UNUSABLE index). We need to rebuild this index in order to make the data truly usable again. The sliding window process, which so far has resulted in virtually no downtime, will now take a very long time to complete while we rebuild the global index. All of the data must be scanned and the entire index reconstructed from the table data. If the table is many hundreds of gigabytes in size, this will take considerable resources.

Any partition operation will cause this global index invalidation to occur. If you need to move a partition to another disk, your global indexes must be rebuilt (only the local index partitions on that partition would have to be rebuilt). If you find that you need to split a partition into two smaller partitions at some point in time, all global indexes must be rebuilt (only the pairs of individual local index partitions on those two new partitions would have to be rebuilt). And so on. For this reason, global indexes in a data warehousing environment should be avoided. Their use may dramatically affect many operations.

OLTP and Global Indexes

An OLTP system is characterized by the frequent occurence of many small, to very small, read and write transactions. In general, sliding windows of data are not something with which you are concerned. Fast access to the row, or rows, you need is paramount. Data integrity is vital. Availability is also very important.

Global indexes make sense in OLTP systems. Table data can only be partitioned by one key - one set of columns. However, you may need to access the data in many different ways. You might partition EMPLOYEE data by location. You still need fast access to EMPLOYEE data however by:

There is a need to access the EMPLOYEE data by many different keys in different places in your application, and speed is paramount. In a data warehouse, we would just use locally partitioned indexes on the above key values, and use parallel index range scans to collect the data fast. In these cases we don't necessarily need to utilize index partition elimination, but in an OLTP system, however, we do need to utilize it. Parallel query is not appropriate for these systems; we need to provide the indexes appropriately. Therefore, we will need to make use of global indexes on certain fields.

Therefore the goals we need to meet are:

Global indexes can do this for us in an OLTP system, since the characteristics of this system are very different from a data warehouse. We will probably not be doing sliding windows, we will not be splitting partitions (unless we have a scheduled downtime), we will not be moving data and so on. The operations we perform in a data warehouse are not done on a live OLTP system in general.

Here is a small example that shows how we can achieve the above three goals with global indexes. I am going to use simple, single partition global indexes, but the results would not be different with partitioned global indexes (except for the fact that availability and manageability would increase as we add index partitions):

tkyte@TKYTE816> create table emp   2  (EMPNO             NUMBER(4) NOT NULL,   3   ENAME             VARCHAR2(10),   4   JOB               VARCHAR2(9),   5   MGR               NUMBER(4),   6   HIREDATE          DATE,   7   SAL               NUMBER(7,2),   8   COMM              NUMBER(7,2),   9   DEPTNO            NUMBER(2) NOT NULL,  10   LOC               VARCHAR2(13) NOT NULL  11  )  12  partition by range(loc)  13  (  14  partition p1 values less than('C') tablespace p1,  15  partition p2 values less than('D') tablespace p2,  16  partition p3 values less than('N') tablespace p3,  17  partition p4 values less than('Z') tablespace p4  18  )  19  /      Table created.      tkyte@TKYTE816> alter table emp add constraint emp_pk   2  primary key(empno)   3  /      Table altered.      tkyte@TKYTE816> create index emp_job_idx on emp(job)   2  GLOBAL   3  /      Index created.      tkyte@TKYTE816> create index emp_dept_idx on emp(deptno)   2  GLOBAL   3  /      Index created.      tkyte@TKYTE816> insert into emp   2  select e.*, d.loc   3    from scott.emp e, scott.dept d   4   where e.deptno = d.deptno   5  /      14 rows created. 

So, we start with a table that is partitioned by location, LOC, according to our rules. There exists a global unique index on the EMPNO column as a side effect of the ALTER TABLE ADD CONSTRAINT. This shows we can support data integrity. Additionally, we've added two more global indexes on DEPTNO and JOB, to facilitate accessing records quickly by those attributes. Now we'll put some data in there and see what is in each partition:

tkyte@TKYTE816> select empno,job,loc from emp partition(p1);      no rows selected      tkyte@TKYTE816> select empno,job,loc from emp partition(p2);           EMPNO JOB       LOC ---------- --------- -------------       7900 CLERK     CHICAGO       7844 SALESMAN  CHICAGO       7698 MANAGER   CHICAGO       7654 SALESMAN  CHICAGO       7521 SALESMAN  CHICAGO       7499 SALESMAN  CHICAGO 6 rows selected.      tkyte@TKYTE816> select empno,job,loc from emp partition(p3);           EMPNO JOB       LOC ---------- --------- -------------       7902 ANALYST   DALLAS       7876 CLERK     DALLAS       7788 ANALYST   DALLAS       7566 MANAGER   DALLAS       7369 CLERK     DALLAS      tkyte@TKYTE816> select empno,job,loc from emp partition(p4);           EMPNO JOB       LOC ---------- --------- -------------       7934 CLERK     NEW YORK       7839 PRESIDENT NEW YORK       7782 MANAGER   NEW YORK 

This shows the distribution of data, by location, into the individual partitions. We can now run some queries to check out performance:

tkyte@TKYTE816> select empno,job,loc from emp where empno = 7782;           EMPNO JOB       LOC ---------- --------- -------------       7782 MANAGER   NEW YORK      Execution Plan ----------------------------------------------------------    0      SELECT STATEMENT Optimizer=CHOOSE (Cost=1 Card=4 Bytes=108)    1    0   TABLE ACCESS (BY GLOBAL INDEX ROWID) OF 'EMP' (Cost=1 Card    2    1     INDEX (RANGE SCAN) OF 'EMP_PK' (UNIQUE) (Cost=1 Card=4)           tkyte@TKYTE816> select empno,job,loc from emp where job = 'CLERK';           EMPNO JOB       LOC ---------- --------- -------------       7900 CLERK     CHICAGO       7876 CLERK     DALLAS       7369 CLERK     DALLAS       7934 CLERK     NEW YORK           Execution Plan ----------------------------------------------------------    0      SELECT STATEMENT Optimizer=CHOOSE (Cost=1 Card=4 Bytes=108)    1    0   TABLE ACCESS (BY GLOBAL INDEX ROWID) OF 'EMP' (Cost=1 Card    2    1     INDEX (RANGE SCAN) OF 'EMP_JOB_IDX' (NON-UNIQUE) (Cost=1 

And sure enough, our indexes are used and provide high speed OLTP access to the underlying data. If they were partitioned, they would have to be prefixed and would enforce index partition elimination; hence, they are scalable as well. Lastly, let's look at the area of availability. The Oracle documentation claims that globally partitioned indexes make for 'less available' data than locally partitioned indexes. I don't fully agree with this blanket characterization, I believe that in an OLTP system they are as highly available as a locally partitioned index. Consider:

tkyte@TKYTE816> alter tablespace p1 offline;      Tablespace altered.      tkyte@TKYTE816> alter tablespace p2 offline;      Tablespace altered.      tkyte@TKYTE816> alter tablespace p3 offline;      Tablespace altered.      tkyte@TKYTE816> select empno,job,loc from emp where empno = 7782;           EMPNO JOB       LOC ---------- --------- -------------       7782 MANAGER   NEW YORK           Execution Plan ----------------------------------------------------------    0      SELECT STATEMENT Optimizer=CHOOSE (Cost=1 Card=4 Bytes=108)    1    0   TABLE ACCESS (BY GLOBAL INDEX ROWID) OF 'EMP' (Cost=1 Card    2    1     INDEX (RANGE SCAN) OF 'EMP_PK' (UNIQUE) (Cost=1 Card=4) 

Here, even though most of the underlying data is unavailable in the table, we can still gain access to any bit of data available via that index. As long as the EMPNO we want is in a tablespace that is available, our GLOBAL index works for us. On the other hand, if we had been using the 'highly available' local index in the above case, we might have been prevented from accessing the data! This is a side effect of the fact that we partitioned on LOC but needed to query by EMPNO; we would have had to probe each local index partition and would have failed on the index partitions that were not available.

Other types of queries however will not (and cannot) function at this point in time:

tkyte@TKYTE816> select empno,job,loc from emp where job = 'CLERK'; select empno,job,loc from emp where job = 'CLERK'                           * ERROR at line 1: ORA-00376: file 13 cannot be read at this time ORA-01110: data file 13: 'C:\ORACLE\ORADATA\TKYTE816\P2.DBF' 

The CLERK data is in all of the partitions - the fact that three of the tablespaces are offline does affect us. This is unavoidable unless we had partitioned on JOB, but then we would have had the same issues with queries that needed data by LOC. Any time you need to access the data from many different 'keys', you will have this issue. Oracle will give you the data whenever it can.

Note, however, that if the query can be answered from the index, avoiding the TABLE ACCESS BY ROWID, the fact that the data is unavailable is not as meaningful:

tkyte@TKYTE816> select count(*) from emp where job = 'CLERK';        COUNT(*) ----------          4      Execution Plan ----------------------------------------------------------    0      SELECT STATEMENT Optimizer=CHOOSE (Cost=1 Card=1 Bytes=6)    1    0   SORT (AGGREGATE)    2    1     INDEX (RANGE SCAN) OF 'EMP_JOB_IDX' (NON-UNIQUE) (Cost=1 

Since Oracle didn't need the table in this case, the fact that most of the partitions were offline doesn't affect this query. As this type of optimization (answer the query using just the index) is common in an OLTP system, there will be many applications that are not affected by the data that is offline. All we need to do now is make the offline data available as fast as possible (restore it, recover it).

Summary

Partitioning is extremely useful in scaling up large database objects in the database. This scaling is visible from the perspective of performance scaling, availability scaling, as well as administrative scaling. All three are extremely important to different people. The DBA is concerned with administrative scaling. The owners of the system are concerned with availability. Downtime is lost money and anything that reduces downtime, or reduces the impact of downtime, boosts the payback for a system. The end users of the system are concerned with performance scaling - no one likes to use a slow system after all.

We also looked at the fact that in an OLTP system, partitions may not increase performance, especially if applied improperly. Partitions can increase performance of certain classes of queries but those queries are generally not applied in an OLTP system. This point is important to understand as many people associate partitioning with 'free performance increase'. This does not mean that partitions should not be used in OLTP systems, as they do provide many other salient benefits in this environment - just don't expect a massive increase in throughput. Expect reduced downtime. Expect the same good performance (it will not slow you down when applied appropriately). Expect easier manageability, which may lead to increased performance due to the fact that some maintenance operations are performed by the DBAs more frequently because they can be.

We investigated the various table-partitioning schemes offered by Oracle - range, hash, and composite - and talked about when they are most appropriately used. We spent the bulk of our time looking at partitioned indexes, examining the difference between prefixed and non-prefixed, and local and global indexes. We found that global indexes are perhaps not appropriate in most data warehouses whereas an OLTP system would most likely make use of them frequently.

Partitioning is an ever-growing feature within Oracle with many enhancements planned for the next release. Over time, I see this feature becoming more relevant to a broader audience as the size and scale of database applications grow. The Internet and its database hungry nature are leading to more and more extremely large collections of data and partitioning is a natural tool to help manage that problem.



Expert One on One Oracle
Full Frontal PR: Getting People Talking about You, Your Business, or Your Product
ISBN: 1590595254
EAN: 2147483647
Year: 2005
Pages: 41
Authors: Richard Laermer, Michael Prichinello
BUY ON AMAZON

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net