Partition pruning is the optimizer’s ability to eliminate partitions from a query’s access path based on predicate analysis. When it works, a query against a 10 TB table might access only 50 GB of relevant partitions. When it fails, you get a full table scan across every partition — often with no visible indication in the execution plan that pruning is not occurring.

How Partition Pruning Works

The optimizer evaluates predicates in the WHERE clause against the partition key definition during the parse phase. If a predicate can be resolved to a specific partition range, those partitions are excluded from the access path.

Two types of pruning:

  • Static pruning: The predicate contains a literal value. The optimizer can eliminate partitions at parse time. The plan shows Pstart and Pstop as specific partition numbers.
  • Dynamic pruning: The predicate contains a bind variable or a subquery. The optimizer cannot resolve partitions at parse time; partition elimination happens at execution time. The plan shows KEY in the Pstart/Pstop columns.
-- Example: range-partitioned table by order_date
CREATE TABLE orders (
  order_id    NUMBER,
  order_date  DATE,
  customer_id NUMBER,
  amount      NUMBER
)
PARTITION BY RANGE (order_date) INTERVAL (NUMTOYMINTERVAL(1, 'MONTH'))
(
  PARTITION p_initial VALUES LESS THAN (DATE '2024-01-01')
);

-- Static pruning (literal date): optimizer knows at parse time
EXPLAIN PLAN FOR
SELECT order_id, amount
FROM orders
WHERE order_date >= DATE '2026-01-01'
  AND order_date <  DATE '2026-02-01';

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(FORMAT => 'PARTITION'));
----------------------------------------------------------------------
| Id | Operation             | Name   | Rows | Pstart | Pstop |
----------------------------------------------------------------------
|  0 | SELECT STATEMENT      |        |      |        |       |
|  1 |  PARTITION RANGE SINGLE |       |      |     37 |    37 |
|  2 |   TABLE ACCESS FULL   | ORDERS |  50K |     37 |    37 |
----------------------------------------------------------------------

Pstart=37, Pstop=37 — only one partition accessed. This is the expected result for an effectively pruned query.


When Pruning Fails: Common Traps

1. Implicit Type Conversion

This is the most common and most subtle pruning killer. When the partition key is DATE and the predicate uses a VARCHAR2, Oracle implicitly converts — but the conversion function wraps the column, preventing pruning.

-- WRONG: implicit conversion disables pruning
WHERE TO_CHAR(order_date, 'YYYY-MM') = '2026-01'

-- CORRECT: use native date arithmetic
WHERE order_date >= DATE '2026-01-01'
  AND order_date <  DATE '2026-02-01'

Check: if Pstart=1 and Pstop=LAST appear in the plan, all partitions are being scanned.

2. Function Wrapping the Partition Key

Any function wrapped around the partition column prevents the optimizer from mapping the predicate to partition boundaries.

-- WRONG: function prevents pruning
WHERE TRUNC(order_date, 'MM') = DATE '2026-01-01'

-- CORRECT: range predicate on the raw column
WHERE order_date >= DATE '2026-01-01'
  AND order_date <  DATE '2026-02-01'

Exception: function-based partitioning allows the partition key itself to be defined as a function — if the WHERE clause uses the same function, pruning can occur. But this is a design decision made at table creation, not a workaround.

3. OR Conditions on Multiple Partitions

OR conditions across different partition key values can still prune — but each branch is evaluated independently. Verify in the plan that each OR branch shows specific partition ranges.

4. Non-Partitioning-Key Predicates Only

If the WHERE clause has no predicate on the partition key, all partitions are scanned regardless of other predicates.

-- No order_date predicate → full partition scan despite customer_id filter
WHERE customer_id = 12345   -- customer_id is not the partition key

This is a design problem: if customer_id-based access is a primary access pattern, consider composite partitioning or a global index.


Verifying Pruning via Execution Plan

Always check the PARTITION RANGE or PARTITION HASH operation and the Pstart/Pstop columns:

-- Runtime partition access (use ALLSTATS LAST for actual values)
SELECT *
FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST PARTITION'));
Pstart/Pstop Values Meaning
Specific numbers (e.g., 37, 37) Static pruning — specific partition(s)
KEY Dynamic pruning — resolved at execution
1 to LAST No pruning — full partition scan
KEY(SQ) Subquery-driven dynamic pruning

Composite Partitioning for Multi-Dimensional Access Patterns

When queries access data by two dimensions (e.g., date range AND region), composite partitioning provides pruning on both dimensions.

-- Range-Hash composite: prune by date at the partition level,
-- hash by region at the subpartition level
CREATE TABLE sales (
  sale_id   NUMBER,
  sale_date DATE,
  region_id NUMBER,
  amount    NUMBER
)
PARTITION BY RANGE (sale_date)
  SUBPARTITION BY HASH (region_id) SUBPARTITIONS 16
INTERVAL (NUMTOYMINTERVAL(1, 'MONTH'))
(
  PARTITION p_initial VALUES LESS THAN (DATE '2024-01-01')
);

A query with predicates on both sale_date and region_id prunes at both levels, potentially accessing a single subpartition of a single partition — a tiny fraction of the total data.


VLDB Design Patterns

Pattern 1: Monthly Interval Partitioning with Local Indexes

For time-series data (logs, transactions, IoT), interval partitioning combined with local indexes is the standard VLDB pattern:

-- Local index: one index segment per partition
CREATE INDEX idx_orders_customer ON orders (customer_id, order_date) LOCAL;

Local indexes enable partition-level index maintenance — when you drop an old partition, the corresponding index segment is dropped automatically. No global index rebuild required.

Pattern 2: Partition Exchange for ETL

Load data into a staging table, then exchange it into the target partitioned table in a single metadata operation (no data movement):

-- 1. Load data into staging table (identical structure to one partition)
INSERT /*+ APPEND */ INTO staging_orders
SELECT * FROM external_source WHERE order_date >= DATE '2026-02-01'
  AND order_date < DATE '2026-03-01';
COMMIT;

-- 2. Build indexes on staging table (matches the partitioned table's local index)
CREATE INDEX idx_stg_customer ON staging_orders (customer_id, order_date);

-- 3. Exchange: atomic swap into the partition (no data movement)
ALTER TABLE orders
EXCHANGE PARTITION FOR (DATE '2026-02-15')
WITH TABLE staging_orders
INCLUDING INDEXES
WITHOUT VALIDATION;

The EXCHANGE PARTITION operation is effectively instantaneous — it updates data dictionary entries, not data blocks.

Pattern 3: Partition Pruning with Parallel DML

Partition pruning and parallel DML combine efficiently for large batch operations:

-- Parallel delete of an old date range (prunes to relevant partitions)
ALTER SESSION ENABLE PARALLEL DML;

DELETE /*+ PARALLEL(4) */
FROM orders
WHERE order_date < DATE '2020-01-01';

-- Better: drop old partitions directly (no redo generated)
ALTER TABLE orders DROP PARTITION p_2019_12;

Dropping partitions is orders of magnitude faster than DELETE for large volumes — it is a metadata operation, not a row-by-row undo-generating DML.


Monitoring Partition Access Patterns

-- Which partitions are most accessed? (from AWR segment statistics)
SELECT object_name,
       object_type,
       partition_name,
       logical_reads_delta,
       physical_reads_delta
FROM dba_hist_seg_stat s
JOIN dba_hist_seg_stat_obj o ON s.obj# = o.obj# AND s.dbid = o.dbid
WHERE o.object_name = 'ORDERS'
  AND s.snap_id >= (SELECT MAX(snap_id) - 48 FROM dba_hist_snapshot)
ORDER BY logical_reads_delta DESC
FETCH FIRST 20 ROWS ONLY;

This reveals which partitions carry the hot workload — validating that your pruning strategy is directing queries to the right partitions, and identifying “cold” partitions that should be compressed or tiered to cheaper storage.