Partition Exchange Loading using OWB10gR2
July 20th, 2006 by Mark Rittman
In this final posting in the series, I’ll look at how Oracle Warehouse Builder 10.2 can be used to perform Partition Exchange Loading into a target table.
Partition Exchange Loading (PEL) s a data warehousing technique that is useful when you are loading data into a large, partitioned table. By swapping the table that contains the data to be loaded with an empty partition in the partitioned table, the data appears in the target table instantaneously together with any indexes associated with it, giving you the ability to prepare, load and index data offline, and then have it instantly appear in the data warehouse. Oracle Warehouse Builder has supported partition exchange loading for some time now, and in this article I’ll go through how it works with version 10.2
For partition exchange to work, you need to satisfy certain conditions. Some of them are general to the Oracle database, and some are restrictions put in place by Warehouse Builder.
- The table to be loaded must be partitioned (obviously), and additionally for PEL to work in Warehouse Builder the table must be range partitioned on a date datatype column.
- The target table must only have local indexes
- The source table must have the same column definition as the target table (in the same order, with the same datatypes and lengths)
- The source table should have the same index definition as the target table, although as usually it won’t be partitioned the indexes here will be global
- The source table must have the same constraints as the target table.
In this example, I’ll base my target table on the UNITS_FACT table from the Global Sample Schema, but I’ll turn the MONTH_ID column, which in the sample schema is a number column, into a date datatype using an OWB mapping.

I first of all use the mapping to define the table, by joining a view over the UNITS_FACT table that only returns data from dates before 2004 to the DATE_DIM table to get hold of the transaction date in the required format, and then using this to define a new table which I then create and bind into the repository. Then, after creating and binding the table in the repository, I can then add the partition definition to the table.
The trick here is to make sure you range partition on a date column, and name the partitions to a set standard:
-
Ydddd if they are year partitions, i.e. Y2000, Y2001 etc
-
Ydddd_Qd for quarter partitions, i.e. Y2000_Q1
-
Ydddd_Qd_Mdd for month partitions, i..e Y2000_Q1_M03
-
Ydddd_Qd_Mdd_Ddd for days
-
Ydddd_Qd_Mdd_Ddd_Hdd for hours, or
-
Ydddd_Qd_Mdd_Ddd_Hdd_Mdd for minute partitions
In my case, I want to partition the table by year, and so I call the partitions Y1999, Y2000, Y2002 through to Y2005. When you use this naming convention, Warehouse Builder auto-fills the “values” column with the correct end date for the partition.

Note that you need to create in advance the partition that you’re going to load data into - the PEL process doesn’t create the partition itself. In my case, the mapping I’ve just used is going to load data up until 2003 into the table, and I’m going to create another mapping in a moment to partition exchange load additional data from 2004, so I create partitions for all years from 1999 up to 2005.
Next I add the index definition, creating a single local index covering all four key columns, which picks up the partitioning scheme from the parent table.

Now I add the primary key constraint, built on the same four columns in the same order as the index.

Then, I close the Data Object Editor and right-click on the table in the project explorer, to bring up the table configuration. Using the properties panel, I select “Use Indexes” against the primary key element to tell Oracle to use the existing unique index on the table rather than automatically create one when creating the primary key constraint.

Now that the target, partitioned table definition is complete, I run the mapping to bring across the initial set of data, which goes up to 2003.
Next I have to work on the source for the partition exchange load. In Warehouse Builder, there are two types of partition exchange load;
-
Direct, where the source table is directly swapped with the partition in the target table, leaving you with an empty source table afterwards, and
-
Indirect, where OWB creates an intermediate staging table from data in your source, and swaps that with the target partition.
Direct is faster, as it just requires OWB to issue some DDL, whilst indirect involves populating an intermediate table; the only reason you’d use the latter is if you haven’t got a source table to swap, i.e. what you’re PEL’ing is the results of a jointer, or a splitter, or something else that’s not a “table”. In our case, we’ll go for direct as then we’ll see the swapping in action and we’ll be able to tell it worked, as our source table will be empty at the end.
To do this, I need an intermediate staging table, which I call UNITS_FACT_STAGING. I create this in the same way as before, creating a mapping to take data from another view over the UNITS_FACT table in the Global Sample Schema, this time with a WHERE clause limiting data to just 2004. After binding and creating the table, I enter the Data Object Editor and define the index, which in this case is global as the table isn’t partitioned.

After that I define the primary key the same was as before, and select “Using Index” from the configuration page to tell Oracle to re-use the index. Then, I deploy the table, deploy the mapping and bring the data across. I’m all set now to put together the partition exchange load.
Before I do this, I go into SQL*Plus and run some SELECTs.
SQL> conn global_pel/password@ora10g Connected. SQL> select count(*) 2 from global.units_fact 3 / COUNT(*) ---------- 222589 SQL> select count(*) 2 from units_fact_partitioned 3 / COUNT(*) ---------- 196038 SQL> select count(*) 2 from units_fact_partitioned partition(Y2003) 3 / COUNT(*) ---------- 43800 SQL> select count(*) 2 from units_fact_partitioned partition(Y2004) 3 / COUNT(*) ---------- 0 SQL> select count(*) 2 from units_fact_staging 3 / COUNT(*) ---------- 26551
OK, that looks good. The target partitioned table currently is missing the data for 2004, which can in fact be found in the staging table. Now it’s time to put the PEL mapping together.
The mapping itself is very simple. I just drag the source and target tables onto a mapping canvas, and connect the columns I wish to map. In reality, as I’m looking to do a direct PEL, I only really need to map one column, as the data will be loaded “en-masse” by a DDL swap of the segments rather than at column level - the reason you’d map the rest is if you’re doing an indirect PEL and Warehouse Builder needs to create an intermediate table, or if you want there to be a fall-back in case PEL can’t happen. However, in the latter case, the only reason PEL won’t happen is because the conditions aren’t met - either there’s a global index on the target table, or the source table definition doesn’t match that of the target table - and in this case, I’d want to fix the problem in the first place, not have Warehouse Builder fail-over to a much slower method.

Anyway, once the objects are mapped on the canvas, I need to close the mapping and right-click on it in the Project Explorer to configure it. This is where you tell Warehouse Builder to load using PEL.

Now, it’s a case of deploying the mapping, running it, and checking the results. Going back to SQL*Plus, I run SELECTs on the target and source tables:
SQL> select count(*) 2 from units_fact_partitioned partition(Y2004) 3 / COUNT(*) ---------- 26551 SQL> select count(*) 2 from units_fact_staging 3 / COUNT(*) ---------- 0
It’s worked. Warehouse Builder has swapped the UNITS_FACT_STAGING table and it’s index with the empty Y2004 partition in the UNITS_FACT_PARTITIONED table and it’s local index partition, instantly loading the partitioned table and making it ready for use.

December 11th, 2006 at 9:02 pm
I can’t do your example because at the end when I validate the mapping display a message like ‘VLD-2789 Direct PEL is not possible for target because its source is not local’. Can you help me?
December 16th, 2006 at 1:36 pm
Hi JJ,
This isn’t an error that I’ve come across before, but I suspect it’s because you can’t do direct PEL when the source and target tables aren’t on the same database, i.e. both segments need to be in the same database before you can ’swap’ them, which sounds logical. This FAQ on Warehouse Builder 9.2 suggests this is the case:
http://www.oracle.com/technology/products/warehouse/htdocs/ORACLE92_WAREHOUSE_BUILDER_FAQ1.htm
The workaround for this would be to either use indirect PEL (i.e. Warehouse Builder creates a temporary table on the same database as the partitioned table to be loaded, and swaps that temporary table instead), or create your source table on the same database as the target partitioned table, and direct PEL that instead.
regards
Mark
May 28th, 2008 at 3:49 pm
Hi Mark,
Hope you’re still looking at comments on old posts like this! Anyway, I’m dealing with a slightly unusual problem in that I have a fact table I would like to load using PEL, but I also want to use the fact table itself as a dimension for another fact table. Therefore I want the first fact table to have a primary key so that it can be referenced by foreign key of the other fact table, useful for example for query rewrite. As a fact table, the table in question has a unique surrogate key column suitable for a primary key, but I cannot create a local index on it because a local unique index must include the partitioning key as well. If I were doing PEL manually rather than through OWB I could get round this by defining the index as global and using the ‘update global indexes’ clause on ‘exchange partition’ (though performance-wise this might not be ideal). But if I do this in OWB then as far as I can see the ‘update global indexes’ option is not available, and so I have two options: 1) Bite the bullet and add the partitioning key to the index; not desirable as this results in a relatively inefficient 2-column index, and I also have to propogate the partioning key value to the other fact table. 2) Work round it by defining the index as non-unique, and creating the primary key constraint as ‘disable novalidate’, or by using the ‘using index’ clause (as I understand it constraints only need to be declarative to support query rewrite - and I don’t actually want primary or foreign key constraints to be applied during my ETL process anyway). Neither approach seems ideal, though I am leaning towards option 1, mainly because option 2 really does seem like a ‘workaround’ rather than a ’solution’ - having said that I can’t think of any reason why it wouldn’t work. What advice would you offer on this?
Rob
May 29th, 2008 at 3:23 pm
@Rob
It’s never too late to post things like this!
It is uncommon to want to make a fact a also a dimension - I guess that this ‘dimension’ just has one level and one hierarchy. Sometimes other ways of modelling this sort of relationship may be more appropriate, can you give an idea of the type of thing you are trying to model (without naming names!). In the past when I needed to model a similar thing (well it sounds similar) but needing an index on some of the data I used a partitioned Index Organized Table as sort of skinny table consisting of just the columns I needed
You are right, constraints only need be declared for the CBO to rewrite queries - they need not physically exist at all: RELY DISABLE NOVALIDATE will not even create an index to enforce the constraint. This could be a good option if you do not need to index the “dimensional fact”
Finally, it is possible to mix your PL/SQL packages (say for complex partition handling) with OWB packages - I have done so for one of my customers where the OWB version used (9.2 at the time) did not support our requirements
June 4th, 2008 at 2:02 pm
Hi Peter,
This case is from retail. The ‘dimension fact’ represents a ’shopping basket’ and has its own distinct metrics as well as a number of attributes. The other fact is for line items, and the reason for referencing the shopping basket from this is to gain access to the basket level attributes, as well as being able to derive information such as ‘number of baskets containing product A and product B’. At the moment we’re proceeding with option 1 - the 2-column index. Seems cleanest, and hasn’t caused us any problems so far!
Thanks,
Rob