Partitions in the Real World
Have you ever noticed that most articles about Oracle features use totally trivial examples to demonstrate the points they are trying to make? Have you also noticed that it is only when you try to do something realistic with a feature that the issues start to show up?
This article presents a few of the issues surrounding the realistic use of the
exchange partition feature. The version of Oracle used throughout this article is
9.2.0.3
– other versions may exhibit different behaviour.
What is partition exchange ?
One of the great features about partitioning, and most specifically range-based partitioning, is the ability to load in new data quickly and easily with minimal impact on the current users. The manuals indicate that the critical command to achieve this is as follows: alter table pt_tab1 exchange partition p_9999 with table new_data
-- including indexes -- optional
-- without validation -- optional
;
This command “hacks” into the data dictionary and swaps over the definitions of the named partition and the new_data 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.
Or so the story goes.
Typically an article to demonstrate this point about partition exchange will create a table with a handful of partitions and a couple of hundred rows per partition, quite possibly without indexes, and almost certainly without any related tables.
Typically, if you've paid for the partitioning option, you will have very large tables, several indexes, and things like related tables with foreign keys defined. So what happens in real life when you do a partition exchange.
What does partition exchange cost ?
Let' start with the really easy case - swapping a 1,000,000 row table with an empty partition. Let's start with some SQL to create a partitioned table and a non-partitioned table - we will be re-using this code, with some modifications, throughout the rest of the article. We will also need a tablespace with about 200MB of free space. create table parent ( id number(12,6), v1 varchar2(10), padding varchar2(100)
) partition by range(id) ( partition p1000 values less than (1000), partition p3000 values less than (3000),
); partition p5000 values less than (5000)
create table parent_ex ( id number(12,6), v1 varchar2(10), padding varchar2(100)
) nologging -- just to save a bit of time
; insert /*+ append ordered full(s1) use_nl(s2) */ into parent_ex select
3000 + trunc((rownum-1)/500,6), from to_char(rownum), rpad('x',100) sys.source$ s1, sys.source$ s2
-- you’ll need the privilege where rownum <= 1000000;
And now, let's swap the table with partition P5000 - which is where the data belongs.
But let's switch on timing to see how long it takes. alter table parent exchange partition p5000 with table parent_ex;
Elapsed: 00:00:17.06
What happened to the 'very fast' transfer - repeat the test with sql_trace switched on and you will find the following SQL in the trace file - Oracle is checking if there are any rows in the parent_ex table that don't belong in the specified partition of the parent table. select 1 from "PARENT_EX" where TBL$OR$IDX$PART$NUM("PARENT",0,0,65535,"ID") != :1
This takes a full tablescan, and calls a function for every single row in the load table - imagine the effect on a real system with large data sets, and a busy I/O subsystem.
But never fear, that's what the without validation clause is for. Repeat the experiment, but put this clause at the end of the exchange command. alter table parent exchange partition p5000 with table parent_ex without validation;
Elapsed: 00:00:00.00
Hurrah ! it works much faster. Hang on though, real databases tend to have things like indexes, and primary key, or unique key, constraints. So let's repeat the exercise, but add a primary key constraint to the main table and the exchange table before doing the exchange (Note the version 9 syntax for specifying the index completely when adding the constraint - I've also chosen to enforce a unique/pk constraint through a non-unique index). In this case, we will probably want to include indexes in the exchange so that the exchanged data appears in the main table with a usable index partition. alter table parent add constraint p_pk primary key(id)
using index (create index p_pk on parent(id) nologging local); alter table parent_ex add constraint px_pk primary key(id) using index (create index px_pk on parent_ex(id) nologging); alter table parent exchange partition p5000 with table parent_ex including indexes without validation;
Elapsed: 00:00:28.05
Now what's happened ? A (not so quick) repeat of the experiment with sql_trace enabled produces the following SQL. Oracle is checking that the exchange won't cause a uniqueness problem. The query is searching the entire parent (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 - which means there is only one legal partition that a row can be in, and we have already promised (through the without validation clause) that all the rows belong where we are putting them.
It is true, of course, that there may be illegal rows in other partitions that should be in the partition we are loading – but if so, I don’t think I want to pay the penalty for finding them right now.
The check for nulls seems a bit strange too, as in this case we have a primary key constraint - which implies a not null constraint. Presumably this is here to allow the same code to be reused for unique constraints as well as primary key constraints.
Note particularly the minus and intersect operators - these require a sort of the entire data set - and I got off cheaply with my 28 seconds because (a) I had no data in the original parent table, and (b) the data in the parent_ex table was already sorted. select "ID" from "TEST_USER"."PARENT_EX" where not( "ID" is null) intersect select "ID" from ( select "ID" from "TEST_USER"."PARENT" minus select "ID" from "TEST_USER"."PARENT" partition (P5000)
) B where not( "ID" is null)
Can we work around this massive cost. Yes, all we have to do is set the constraints on both tables to novalidate status before we do the exchange. alter table parent_ex modify primary key novalidate; alter table parent modify primary key novalidate;
The general purpose of the novalidate option on a constraint is to tell Oracle that it should not check pre-existing data to see that it obeys the constraint. New data, or changes that you make to old data, will be checked though, as the constraint is still enabled .
But there is a very subtle hidden trap here. There is a special optimizer algorithm that prices primary key, and unique key constraints, at a lower price than normal unless the constraint is deferrable or (as we have just set it) in the novalidate state.
If we switch our constraints between validate and novalidate to allow us to exchange partitions cheaply, we might just find the occasional query changing its execution path as we do so !
So perhaps the best bet is to leave the constraints in the validate state, and not go for the 'cheap' without validation option on exchange. The single pass through the incoming data may be much more acceptable than the alternatives.
Do you have a relational database ?
But there's always worse news around the corner. In this case, you might actually have two partitioned tables with some sort of relationship between them (a feature that I have observed in many systems). How does this affect partition manipulation.
) create table child ( id_p number(12,6), seq_c v1 number(10), varchar2(10), padding varchar2(100), constraint c_fk_p foreign key (id_p) references parent, constraint c_pk primary key (id_p, seq_c) using index local partition by range(id_p) ( partition p1000 values less than (1000),
); partition p3000 values less than (3000), partition p5000 values less than (5000)
Note, by the way, how this child table (with a foreign key constraint) is designed to match the parent table on its partition boundaries - a design that helps us achieve a special partition-wise join optimisation.
When you start experimenting with parent/child relationships, you find that you are severely restricted on partition exchange, unless you start setting constraints to the novalidate state all over the place.
And then it gets worse! Should you ever decide to drop some old partitions, the command is easy: alter table child drop partition p1000; alter table parent drop partition p1000;
If you try the above on the test case that we have been using so far, you will find that the commands work quickly and efficiently. Unfortunately, the test case is a very special case: the partitions in question have never held any data. There are actually three problems to consider when dropping pairs of parent/child partitions.
First issue - if you try to drop a parent/child pair of partitions and the parent has ever held any data, then the attempt to drop (or truncate) the parent partition will result in
Oracle error:
ORA-02266: unique/primary keys in table referenced by enabled foreign keys
You have to disable the foreign key constraint to drop the parent partition - even when you've dropped the 'obvious' matching child partition. This is, of course, perfectly reasonable, but seems intuitively 'unfair'.
Second issue - when you drop a partition, every partition above it has to be renumbered (internally) in the data dictionary. Imagine a partitioned table with 3,000 partitions and two locally partitioned indexes. When you drop the bottom partition,
Oracle renumbers 9,000 data dictionary rows - and it does this one row at a time.
One drop command results in 9,000 separate updates . It's not quick.
Finally - as you issue the first drop (against the child table, presumably), the tables cease to be partitioned identically - all related SQL cursors are invalidated (this invalidation occurs on any partition maintenance operation) and re-optimised, and the optimiser will not consider partition-wise joins until the matching parent is dropped. You have to time your drops carefully.
Workarounds:
In your own special cases, you may find acceptable solutions, or even work-arounds to problems. For example, one (apparently viable) work-around to the problem of disabling foreign key constraints relies on the fact that you can drop a parent partition if it has never held any data. If you play about with this idea, you will find that the following strategy for a parent/child drop works with just novalidate constraints:
create empty parent_clone with indexes
drop child partition
exchange parent partition with parent_clone without validation including indexes
drop parent partition - which is now acceptable since it has never held data
drop parent_clone – which is where all the data is
I am not sure that this is entirely safe, though. What if the failure to raise Oracle error
ORA-02266 in this special case is a bug ? What do you do if Oracle Corp. slips in a bug fix on the next patch set ?
Conclusion
It is almost inevitable that new features cost you in CPU, I/O and developer time.
Make sure you test a feature realistically before you starting coding it into your system.
There may be side effects that you won't notice if all you test is that the syntax works.
Once you know what a feature costs, you can make a rational decision about how it should be used in your application.
Stop Press
Shortly before sending this article in to DBAZine for publication, I installed an upgrade to 9.2.0.4, and found a note in the bug patch list pointing out that a bug that
‘may cause exchange on tables with unique constraints to be slow’ had been fixed.
The fix was to remove the constraint check described in this article.
Jonathan Lewis is a freelance consultant with more than 17 years experience of
Oracle. He specialises in physical database design and the strategic use of the
Oracle database engine and is often available at short notice for brief consulting assignments. He is author of 'Practical Oracle 8I - Designing Efficient Databases' published by Addison-Wesley. Further details of pulished papers, presentations, seminars and tutorials can be found at http://www.jlcomp.demon.co.uk, which also hosts The Co-operative Oracle Users' FAQ for the Oracle-related Usenet newsgroups.