Administer Partitioned Tables and Indexes Using Appropriate Methods and Keys

1. Documentación en Tahiti -> Oracle Database, 11g Release 2 (11.2) -> Masters Book List -> VLDB and Partitioning Guide

Aparte de la dirección donde vienen varios capítulos relativos al particionamiento me gustaría destacar estos otros:

· Tipos de Particiones
· Recomendaciones para escoger una estrategía
· Varios ejemplos de creación de particiones

2. El Particionamiento en Oracle es un tema muy denso. Hay un manual completo dedicado a las particiones y sus ventajas dentro de las BBDD muy grandes (VLDB). En el examen se dedican tres objetivos a este tema por la cantidad de requisitos a cubrir. Estos son los tres objetivos que cubren las particiones en el examen:

[ ] Administer partitioned tables and indexes using appropriate methods and keys (este objetivo)
[ ] Perform partition maintenance operations
[ ] Maintain indexes on a partitioned table

Este objetivo concreto trata de cubrir las diferentes opciones que tenemos para crear tablas e índices particionados. Además, debemos saber qué tipo de partición es la más adecuada para nuestro propósito, así como la clave o claves de partición que debemos utilizar.

3. Empezamos creando diferentes tipos de tablas particionadas. En los ejemplos que vamos a ir haciendo hay que prestar atención al tipo de particionamiento que hacemos y el por qué. Existen tres tipos de tablas particionadas: RANGE, HASH y LIST. Además, están se pueden combinar entre sí. En la documentación se nos explican varias razones por las cuales un método puede ser mejor en una u otra ocasión (Recommendations for Choosing a Partitioning Strategy)

-- Hacemos los ejemplos sobre el esquema SH
-- Creamos varios TABLESPACES para nuestras pruebas
CREATE TABLESPACE DW01 DATAFILE '/u01/app/oracle/oradata/OCM/dw0101.dbf' size 100M;
CREATE TABLESPACE DW02 DATAFILE '/u01/app/oracle/oradata/OCM/dw0201.dbf' size 100M;
CREATE TABLESPACE DW03 DATAFILE '/u02/app/oracle/oradata/OCM/dw0301.dbf' size 100M;
CREATE TABLESPACE DW04 DATAFILE '/u02/app/oracle/oradata/OCM/dw0401.dbf' size 100M;
CREATE TABLESPACE DW05 DATAFILE '/u01/app/oracle/oradata/OCM/dw0501.dbf' size 100M;
CREATE TABLESPACE DW06 DATAFILE '/u01/app/oracle/oradata/OCM/dw0601.dbf' size 100M;
CREATE TABLESPACE DW07 DATAFILE '/u02/app/oracle/oradata/OCM/dw0701.dbf' size 100M;
ALTER USER SH QUOTA UNLIMITED ON DW01;
ALTER USER SH QUOTA UNLIMITED ON DW02;
ALTER USER SH QUOTA UNLIMITED ON DW03;
ALTER USER SH QUOTA UNLIMITED ON DW04;
ALTER USER SH QUOTA UNLIMITED ON DW05;
ALTER USER SH QUOTA UNLIMITED ON DW06;
ALTER USER SH QUOTA UNLIMITED ON DW07;

-- Creamos una tabla particionada de tipo RANGE
--   · Este tipo de particionado es adecuado para guardar datos históricos (en este caso de ventas)
--   · Las busquedas que hagamos sobre "time_id" se beneficiarán del "Partition Pruning" (acceso sólo a ciertas particiones)
--   · El mantenimiento de esta tabla es sencillo, pudiendo crear nuevas particiones para nuevos datos y borrar las antiguas
--   · Hemos especificado un TBS distinto para cada partición, de esta forma podemos eliminar TBS facilmente
CREATE TABLE SALES_RANGE
  ( prod_id       NUMBER(6)
  , cust_id       NUMBER
  , time_id       DATE
  , channel_id    CHAR(1)
  , promo_id      NUMBER(6)
  , quantity_sold NUMBER(3)
  , amount_sold   NUMBER(10,2)
  )
 PARTITION BY RANGE (time_id)
 ( PARTITION sales_q1_2013 VALUES LESS THAN (TO_DATE('2013/04/01','YYYY/MM/DD')) TABLESPACE DW01,
   PARTITION sales_q2_2013 VALUES LESS THAN (TO_DATE('2013/07/01','YYYY/MM/DD')) TABLESPACE DW02,
   PARTITION sales_q3_2013 VALUES LESS THAN (TO_DATE('2013/10/01','YYYY/MM/DD')) TABLESPACE DW03,
   PARTITION sales_q4_2013 VALUES LESS THAN (TO_DATE('2014/01/01','YYYY/MM/DD')) TABLESPACE DW04
 );

-- Consultamos ciertos atributos de la tabla que hemos creado con sus particiones
-- Las vistas más consultadas son estas:
--   · DBA_TAB_PARTITIONS
--   · DBA_PART_TABLES
--   · DBA_TABLES
--   · DBA_SEGMENTS
SELECT TABLE_NAME, PARTITION_NAME, TABLESPACE_NAME FROM USER_TAB_PARTITIONS;
SELECT TABLE_NAME, PARTITIONING_TYPE, PARTITION_COUNT FROM USER_PART_TABLES;

-- Una nueva funcionalidad que trae Oracle 11g se conoce como "Interval Partitioning"
-- Oracle crear automática las particiones cuando lo necesita en base a un "intervalo"
-- Nosotros seleccionamos que se cree una partición cada mes con la clausula "INTERVAL(NUMTOYMINTERVAL(1, 'MONTH')"
CREATE TABLE SALES_INTERVAL
    ( prod_id        NUMBER(6)
    , cust_id        NUMBER
    , time_id        DATE
    , channel_id     CHAR(1)
    , promo_id       NUMBER(6)
    , quantity_sold  NUMBER(3)
    , amount_sold    NUMBER(10,2)
    )
  PARTITION BY RANGE (time_id)
  INTERVAL(NUMTOYMINTERVAL(1, 'MONTH'))
    ( PARTITION p0 VALUES LESS THAN (TO_DATE('1-1-2011', 'DD-MM-YYYY')),
      PARTITION p1 VALUES LESS THAN (TO_DATE('1-1-2012', 'DD-MM-YYYY')),
      PARTITION p2 VALUES LESS THAN (TO_DATE('1-7-2012', 'DD-MM-YYYY')),
      PARTITION p3 VALUES LESS THAN (TO_DATE('1-1-2013', 'DD-MM-YYYY')) );

-- Comprobamos el número de particiones que tenemos
SELECT TABLE_NAME, PARTITION_NAME, TABLESPACE_NAME FROM USER_TAB_PARTITIONS WHERE TABLE_NAME='SALES_INTERVAL';

-- Insertamos varios registros en la tabla más alla del límite máximo de todas las particiones (1-1-2013)
INSERT INTO SH.SALES_INTERVAL VALUES (1, 1, TO_DATE('2-1-2013', 'DD-MM-YYYY'), 'A', 1, 1, 1);
INSERT INTO SH.SALES_INTERVAL VALUES (1, 1, TO_DATE('2-7-2013', 'DD-MM-YYYY'), 'A', 1, 1, 1);

-- Volvemos a revisar el número de particiones para descubrir que se han creado dos nuevas (Ej. SYS_P93 y SYS_P94)
-- Sólo es necesario crear dos particiones para los nuevos registros porque sólo hay dos meses "nuevos"
SELECT TABLE_NAME, PARTITION_NAME, HIGH_VALUE FROM USER_TAB_PARTITIONS WHERE TABLE_NAME='SALES_INTERVAL';

-- Obtenemos los registros de la nueva partición
SELECT * FROM INTERVAL_SALES PARTITION (SYS_P93);

-- El segundo tipo de particionado es HASH
--   · Utilizaremos HASH cuando no tenemos una división lógica o una clara visión de negocio de los datos
--   · Se suele utilizar para repartir los datos equitativamente entre varios particiones y/o dispositivos
--   · Es recomendable que la clave de partición sea UNIQUE o casi única
--   · En nuestro ejemplo indicamos que las particiones se asignen a los 4 TBS con algoritmo Round-Robin
CREATE TABLE EMP_HASH (
  EMPNO    NUMBER(4) NOT NULL,
  ENAME    CHAR(10),
  JOB      CHAR(9),
  MGR      NUMBER(4),
  HIREDATE DATE,
  SAL      NUMBER(7,2),
  COMM     NUMBER(7,2),
  DEPTNO   NUMBER(2),
  PROJNO   NUMBER,
  LOADSEQ  NUMBER)
PARTITION BY HASH (EMPNO)
PARTITIONS 4
STORE IN (DW01, DW02, DW03, DW04);

-- El último tipo de particiones es LIST
--   · Utilizaremos este tipo cuando queremos dividir las particiones en base a valores discretos
--   · En particiones de tipo LIST sólo podemos usar una clave de partición, al contrario que los otro métodos
CREATE TABLE ACCOUNTS_LIST
( id             NUMBER
, account_number NUMBER
, customer_id    NUMBER
, branch_id      NUMBER
, region         VARCHAR(2)
, status         VARCHAR2(1)
)
PARTITION BY LIST (region)
( PARTITION p_northwest VALUES ('OR', 'WA')
, PARTITION p_southwest VALUES ('AZ', 'UT', 'NM')
, PARTITION p_northeast VALUES ('NY', 'VM', 'NJ')
, PARTITION p_southeast VALUES ('FL', 'GA')
, PARTITION p_northcentral VALUES ('SD', 'WI')
, PARTITION p_southcentral VALUES ('OK', 'TX')
);

4. Hemos visto ejemplo de particionados únicos, pero también tenemos la posibilidad de combinar dos estrategías distintas. Se le conoce como COMPOSITE PARTITIONING o particionamiento compuesto. Estas son las distintas combinaciones que podemos tener:

· Range-Range
· Range-Hash
· Range-List
· List-Range
· List-Hash
· List-List

No vamos a probar todas las combinaciones ya que en la documentación están todas. Pero si quieres probar la más típicas.

-- Creamos una partición compuesta de tipo Range-Hash
--   · Indicado para datos históricos de gran volumen
--   · Se puede utilizar "Partition Pruning" a nivel de Range y "Partition-wise Joins" a nivel de Hash
CREATE TABLE SALES_RANGE_HASH
  ( prod_id       NUMBER(6)
  , cust_id       NUMBER
  , time_id       DATE
  , channel_id    CHAR(1)
  , promo_id      NUMBER(6)
  , quantity_sold NUMBER(3)
  , amount_sold   NUMBER(10,2)
  )
 PARTITION BY RANGE (time_id) SUBPARTITION BY HASH (cust_id)
  SUBPARTITIONS 8 STORE IN (DW01, DW02, DW03, DW04)
 ( PARTITION sales_q1_2006 VALUES LESS THAN (TO_DATE('01-APR-2006','dd-MON-yyyy'))
 , PARTITION sales_q2_2006 VALUES LESS THAN (TO_DATE('01-JUL-2006','dd-MON-yyyy'))
 , PARTITION sales_q3_2006 VALUES LESS THAN (TO_DATE('01-OCT-2006','dd-MON-yyyy'))
 , PARTITION sales_q4_2006 VALUES LESS THAN (TO_DATE('01-JAN-2007','dd-MON-yyyy'))
 );

-- El siguiente ejemplo más extenso no por ello más complicado se trata de una tabla particionada Range-List
--   · Fijaros que tenemos una tabla histórica de ventas particionada por trimestres
--   · Nos interesa, como requisito de negocio, didivir las particiones en areas geográficas concretas
CREATE TABLE SALES_RANGE_LIST
  (
    deptno number,
    item_no varchar2(20),
    txn_date date,
    txn_amount number,
    state varchar2(2)
  )
  PARTITION BY RANGE (txn_date)
    SUBPARTITION BY LIST (state)
      (PARTITION q1_1999 VALUES LESS THAN (TO_DATE('1-APR-1999','DD-MON-YYYY')) TABLESPACE DW01
         (SUBPARTITION q1_1999_northwest VALUES ('OR', 'WA'),
          SUBPARTITION q1_1999_southwest VALUES ('AZ', 'UT', 'NM'),
          SUBPARTITION q1_1999_northeast VALUES ('NY', 'VM', 'NJ'),
          SUBPARTITION q1_1999_southeast VALUES ('FL', 'GA'),
          SUBPARTITION q1_1999_northcentral VALUES ('SD', 'WI'),
          SUBPARTITION q1_1999_southcentral VALUES ('OK', 'TX')
         ),
       PARTITION q2_1999 VALUES LESS THAN ( TO_DATE('1-JUL-1999','DD-MON-YYYY')) TABLESPACE DW02
         (SUBPARTITION q2_1999_northwest VALUES ('OR', 'WA'),
          SUBPARTITION q2_1999_southwest VALUES ('AZ', 'UT', 'NM'),
          SUBPARTITION q2_1999_northeast VALUES ('NY', 'VM', 'NJ'),
          SUBPARTITION q2_1999_southeast VALUES ('FL', 'GA'),
          SUBPARTITION q2_1999_northcentral VALUES ('SD', 'WI'),
          SUBPARTITION q2_1999_southcentral VALUES ('OK', 'TX')
         ),
       PARTITION q3_1999 VALUES LESS THAN (TO_DATE('1-OCT-1999','DD-MON-YYYY')) TABLESPACE DW03
         (SUBPARTITION q3_1999_northwest VALUES ('OR', 'WA'),
          SUBPARTITION q3_1999_southwest VALUES ('AZ', 'UT', 'NM'),
          SUBPARTITION q3_1999_northeast VALUES ('NY', 'VM', 'NJ'),
          SUBPARTITION q3_1999_southeast VALUES ('FL', 'GA'),
          SUBPARTITION q3_1999_northcentral VALUES ('SD', 'WI'),
          SUBPARTITION q3_1999_southcentral VALUES ('OK', 'TX')
         ),
       PARTITION q4_1999 VALUES LESS THAN ( TO_DATE('1-JAN-2000','DD-MON-YYYY')) TABLESPACE DW04
         (SUBPARTITION q4_1999_northwest VALUES ('OR', 'WA'),
          SUBPARTITION q4_1999_southwest VALUES ('AZ', 'UT', 'NM'),
          SUBPARTITION q4_1999_northeast VALUES ('NY', 'VM', 'NJ'),
          SUBPARTITION q4_1999_southeast VALUES ('FL', 'GA'),
          SUBPARTITION q4_1999_northcentral VALUES ('SD', 'WI'),
          SUBPARTITION q4_1999_southcentral VALUES ('OK', 'TX')
         )
      );
-- Podemos listar todas las subparticiones que hemos creado (24)
SELECT
  TABLE_NAME, PARTITION_NAME, SUBPARTITION_NAME, TABLESPACE_NAME
FROM
  USER_TAB_SUBPARTITIONS
WHERE
  TABLE_NAME='SALES_RANGE_LIST'
ORDER BY 2,3;

-- Como habéis visto, la definición del ejemplo anterior repite continuamente el mismo grupo de subparticiones
-- Podemos crear una tabla particionado definiendo una plantilla para las subparticiones (así simplificamos el DDL)
CREATE TABLE SALES_RANGE_LIST_TEMPLATE
            ( deptno number, item_no varchar2(20),
              txn_date date, txn_amount number, state varchar2(2))
   PARTITION BY RANGE (txn_date)
   SUBPARTITION BY LIST (state)
   SUBPARTITION TEMPLATE
      (SUBPARTITION northwest VALUES ('OR', 'WA') TABLESPACE DW01,
       SUBPARTITION southwest VALUES ('AZ', 'UT', 'NM') TABLESPACE DW02,
       SUBPARTITION northeast VALUES ('NY', 'VM', 'NJ') TABLESPACE DW03,
       SUBPARTITION southeast VALUES ('FL', 'GA') TABLESPACE DW04,
       SUBPARTITION northcentral VALUES ('SD', 'WI') TABLESPACE DW05,
       SUBPARTITION southcentral VALUES ('OK', 'TX') TABLESPACE DW06,
       SUBPARTITION others VALUES (DEFAULT) TABLESPACE DW07
      )
  (PARTITION q1_1999 VALUES LESS THAN ( TO_DATE('01-APR-1999','DD-MON-YYYY')),
   PARTITION q2_1999 VALUES LESS THAN ( TO_DATE('01-JUL-1999','DD-MON-YYYY')),
   PARTITION q3_1999 VALUES LESS THAN ( TO_DATE('01-OCT-1999','DD-MON-YYYY')),
   PARTITION q4_1999 VALUES LESS THAN ( TO_DATE('1-JAN-2000','DD-MON-YYYY'))
  );
-- Comprobamos las particiones que hemos creado
SELECT
  TABLE_NAME, PARTITION_NAME, SUBPARTITION_NAME, TABLESPACE_NAME
FROM
  USER_TAB_SUBPARTITIONS
WHERE
  TABLE_NAME='SALES_RANGE_LIST_TEMPLATE'
ORDER BY 2,4;

5. Hasta ahora hemos creado varias tablas particionadas pero siempre hemos utilizado una única clave o columna para dividir las particiones, pero podemos utilizar más de una.

-- Creamos una tabla particionada sobre varias claves
--   · El segundo campo "month" se evalua cuando el primero no es suficiente para determinar la partición
CREATE TABLE SALES_MULTI (
   year          NUMBER,
   month         NUMBER,
   day           NUMBER,
   amount_sold   NUMBER)
PARTITION BY RANGE (year,month)
  (PARTITION before2001 VALUES LESS THAN (2001,1),
   PARTITION q1_2001    VALUES LESS THAN (2001,4),
   PARTITION q2_2001    VALUES LESS THAN (2001,7),
   PARTITION q3_2001    VALUES LESS THAN (2001,10),
   PARTITION q4_2001    VALUES LESS THAN (2002,1),
   PARTITION future     VALUES LESS THAN (MAXVALUE,0));

6. En el siguiente ejemplo quiero combinar varias características ya que no merece la pena verlas por separado.

-- Creamos una tabla con varios aspectos distintivos
--   · Habilitamos la compresión a nivel de tabla, por lo que todas particiones se crearán con COMPRESS
--   · Habilitamos movimiento de filas (ENABLE ROW MOVEMENT) para migrar las filas cuando se modifique la clave
--   · Podemos usar columnas virtuales como la clave de la partición (total_amount)
--   · Con "SEGMENT CREATION IMMEDIATE" forzamos la creación de los segmentos aunque no insertemos registros
--   · Habilitamos PX sobra la tabla (PARALLEL)
--   · Deshabilitamos la generación de REDO (NOLOGGING)
CREATE TABLE SALES_COMBO
  ( prod_id       NUMBER(6) NOT NULL
  , cust_id       NUMBER NOT NULL
  , time_id       DATE NOT NULL
  , channel_id    CHAR(1) NOT NULL
  , promo_id      NUMBER(6) NOT NULL
  , quantity_sold NUMBER(3) NOT NULL
  , amount_sold   NUMBER(10,2) NOT NULL
  , total_amount AS (quantity_sold * amount_sold)
  )
 SEGMENT CREATION IMMEDIATE
 COMPRESS
 PARTITION BY RANGE (time_id) INTERVAL (NUMTOYMINTERVAL(1,'MONTH'))
 SUBPARTITION BY RANGE(total_amount)
 SUBPARTITION TEMPLATE
   ( SUBPARTITION p_small VALUES LESS THAN (1000)
   , SUBPARTITION p_medium VALUES LESS THAN (5000)
   , SUBPARTITION p_large VALUES LESS THAN (10000)
   , SUBPARTITION p_extreme VALUES LESS THAN (MAXVALUE)
   )
 (PARTITION sales_before_2007 VALUES LESS THAN
        (TO_DATE('01-JAN-2007','dd-MON-yyyy'))
)
ENABLE ROW MOVEMENT
PARALLEL NOLOGGING;

7. Al igual que tenemos tablas particionadas, también podemos particionar los índices. Cuando creamos un índice sobre una tabla particionada puede ser de uno de estos tipos:

· Local Partitioned: Cada clave de una partición del índice apunta a una única partición de la tabla del índice
· Global Partitioned: Cada clave de una mima partición pueden a apuntar a varias particiones distinas de la tabla
· Global Nonpartitioned: Tenemos índice no particionado sobre una tabla particionada

Dentro de los índices particionados localmente, tenemos dos tipos:

· Local prefixed: El índice se particiona sobre un prefijo de las columnas del índice
· Local nonprefixed: El índice no está particionado sobre un prefijo de las columnas del índice

-- Creamos un índice global particionado sobre la tabla SALES_RANGE (tipo RANGE)
CREATE INDEX amount_sold_ix ON sales_range(amount_sold)
   GLOBAL PARTITION BY RANGE(amount_sold)
      ( PARTITION p_100 VALUES LESS THAN (100)
      , PARTITION p_1000 VALUES LESS THAN (1000)
      , PARTITION p_10000 VALUES LESS THAN (10000)
      , PARTITION p_100000 VALUES LESS THAN (100000)
      , PARTITION p_1000000 VALUES LESS THAN (1000000)
      , PARTITION p_greater_than_1000000 VALUES LESS THAN (maxvalue)
      );

-- La creación de un índice LOCAL es mucho más sencilla, ya que las particiones son las mismas que en la tabla
-- Las particiones se crean EMPNO pero el índice está creado sobre DEPTNO => El índice es LOCAL NONPREFIXED
     · El índice está particionado sobre EMPNO (aunque no lo indiquemos) porque lo obtiene de la tabla particionada
CREATE INDEX LOC_DEPT_IX ON EMP_HASH(DEPTNO) LOCAL;

-- Pongo un ejemplo de lo que sería un índice LOCAL PREFIXED (índice y particiones sobre EMPNO)
CREATE INDEX LOC_EMPNO_IX ON EMP_HASH(EMPNO) LOCAL;

Respecto a qué índice es más apropiado para qué situación, os recomiendo leer esta sección donde dan unas directrices bastante adecuadas para ello. Expongamos un caso concreto bastante sencillo. Supongamos que tenemos una tabla de registros de llamadas telefónicas. Este tipo de tablas suelen tener millones de registros y además suelen tener un requisito de negocio muy claro que es el número de meses de retención de estos registros. La estrategía habitual consiste en particionar la tabla por fecha y tener un índice local sobre la fecha, para poder mantener la tabla con bastante facilidad y no tener que recrear el índice cada vez que purgamos particiones viejas. Además, podemos tener índices globales sobre otros campos en función de los requisitos de negocio que manejemos.

8. Una funcionalidad nueva de Oracle 11g se llama Reference Partitioning. Nos permite clonar el tipo de particionado de una tabla referencia por una Foreign Key. Es mucho más sencillo explicarlo con un ejemplo que con texto.

-- Creamos varios tablespace para almacenar las tablas particionadas de prueba
CREATE TABLESPACE TBS1 DATAFILE SIZE 20M;
CREATE TABLESPACE TBS2 DATAFILE SIZE 20M;
CREATE TABLESPACE TBS3 DATAFILE SIZE 20M;
CREATE TABLESPACE TBS4 DATAFILE SIZE 20M;

-- Creamos una tabla de productos (muy similar a la tabla OE.PRODUCTS)
CREATE TABLE HASH_PRODUCTS
    ( PRODUCT_ID          NUMBER(6)   PRIMARY KEY
    , PRODUCT_NAME        VARCHAR2(50)
    , PRODUCT_DESCRIPTION VARCHAR2(2000)
    , CATEGORY_ID         NUMBER(2)
    , WEIGHT_CLASS        NUMBER(1)
    , WARRANTY_PERIOD     INTERVAL YEAR TO MONTH
    , SUPPLIER_ID         NUMBER(6)
    , PRODUCT_STATUS      VARCHAR2(20)
    , LIST_PRICE          NUMBER(8,2)
    , MIN_PRICE           NUMBER(8,2)
    , CATALOG_URL         VARCHAR2(50)
    , CONSTRAINT          PRODUCT_STATUS_LOV_DEMO
                          CHECK (PRODUCT_STATUS IN ('ORDERABLE'
                                                  ,'PLANNED'
                                                  ,'UNDER DEVELOPMENT'
                                                  ,'OBSOLETE')
 ) )
 PARTITION BY HASH (PRODUCT_ID)
 PARTITIONS 4
 STORE IN (TBS1, TBS2, TBS3, TBS4);

-- Aquí es donde creamos una tabla con Reference Partitioning
-- Indicamos que el particionado de la clave debe ser el mismo que tiene HASH_PRODUCTS sobre el campo PRODUCT_ID
CREATE TABLE PART_ORDER_ITEMS (
    ORDER_ID        NUMBER(12) PRIMARY KEY,
    LINE_ITEM_ID    NUMBER(3),
    PRODUCT_ID      NUMBER(6) NOT NULL,
    UNIT_PRICE      NUMBER(8,2),
    QUANTITY        NUMBER(8),
    CONSTRAINT PRODUCT_ID_FK
    FOREIGN KEY (PRODUCT_ID) REFERENCES HASH_PRODUCTS(PRODUCT_ID))
 PARTITION BY REFERENCE (PRODUCT_ID_FK);

-- Limpiamos el entorno
DROP TABLE PART_ORDER_ITEMS;
DROP TABLE HASH_PRODUCTS;
DROP TABLESPACE TBS1 INCLUDING CONTENTS AND DATAFILES;
DROP TABLESPACE TBS2 INCLUDING CONTENTS AND DATAFILES;
DROP TABLESPACE TBS3 INCLUDING CONTENTS AND DATAFILES;
DROP TABLESPACE TBS4 INCLUDING CONTENTS AND DATAFILES;