Partition by expression in Oracle AI Database 26ai

Oracle AI Database 26ai lets you place an expression directly in the partitioning clause. You no longer need to expose a virtual column only to use its result as the partition key.

Supported partitioning methods

Partition by expression works with the basic RANGE, LIST and HASH methods. Oracle creates and manages a hidden virtual column internally for each expression.

Test environment

All examples were verified using SQL*Plus on:

Oracle AI Database 26ai Free Release 23.26.3.0.0
Version 23.26.3.0.0

RANGE partitioning by expression

This table is partitioned by the month calculated from order_date:

CREATE TABLE expr_sales
(
  order_id   NUMBER PRIMARY KEY,
  order_date DATE NOT NULL,
  amount     NUMBER(10,2) NOT NULL
)
PARTITION BY RANGE (TRUNC(order_date, 'MM'))
(
  PARTITION p_2026_01 VALUES LESS THAN (DATE '2026-02-01'),
  PARTITION p_2026_02 VALUES LESS THAN (DATE '2026-03-01'),
  PARTITION p_2026_03 VALUES LESS THAN (DATE '2026-04-01'),
  PARTITION p_future  VALUES LESS THAN (MAXVALUE)
);
INSERT INTO expr_sales VALUES (1, DATE '2026-01-10', 120);
INSERT INTO expr_sales VALUES (2, DATE '2026-01-25', 80);
INSERT INTO expr_sales VALUES (3, DATE '2026-02-05', 200);
INSERT INTO expr_sales VALUES (4, DATE '2026-03-18', 150);
INSERT INTO expr_sales VALUES (5, DATE '2026-05-01', 300);
COMMIT;

Check the physical partition used by every row:

SELECT s.order_id,
       TO_CHAR(s.order_date, 'YYYY-MM-DD') order_date,
       s.amount,
       o.subobject_name partition_name
FROM expr_sales s
JOIN user_objects o
  ON o.data_object_id = DBMS_ROWID.ROWID_OBJECT(s.rowid)
ORDER BY s.order_id;

Result:

ORDER_ID ORDER_DATE AMOUNT PARTITION_NAME
-------- ---------- ------ --------------
       1 2026-01-10    120 P_2026_01
       2 2026-01-25     80 P_2026_01
       3 2026-02-05    200 P_2026_02
       4 2026-03-18    150 P_2026_03
       5 2026-05-01    300 P_FUTURE

Use the same expression in the predicate to enable partition pruning:

EXPLAIN PLAN FOR
SELECT SUM(amount)
FROM expr_sales
WHERE TRUNC(order_date, 'MM') = DATE '2026-02-01';

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(
  NULL, NULL, 'BASIC +PARTITION'
));

The tested plan contains:

| Operation              | Pstart | Pstop |
|------------------------|--------|-------|
| PARTITION RANGE SINGLE |      2 |     2 |
| TABLE ACCESS FULL      |      2 |     2 |

LIST partitioning by expression

The expression normalizes region codes, so values such as pl and PL use the same partition:

CREATE TABLE expr_customers
(
  customer_id   NUMBER PRIMARY KEY,
  region_code   VARCHAR2(2) NOT NULL,
  customer_name VARCHAR2(30) NOT NULL
)
PARTITION BY LIST (UPPER(region_code))
(
  PARTITION p_emea     VALUES ('PL', 'DE', 'FR'),
  PARTITION p_americas VALUES ('US', 'CA'),
  PARTITION p_other    VALUES (DEFAULT)
);
INSERT INTO expr_customers VALUES (1, 'pl', 'Anna');
INSERT INTO expr_customers VALUES (2, 'US', 'John');
INSERT INTO expr_customers VALUES (3, 'jp', 'Yuki');
COMMIT;

Check row placement:

SELECT c.customer_id,
       c.customer_name,
       c.region_code,
       o.subobject_name partition_name
FROM expr_customers c
JOIN user_objects o
  ON o.data_object_id = DBMS_ROWID.ROWID_OBJECT(c.rowid)
ORDER BY c.customer_id;

Result:

CUSTOMER_ID CUSTOMER_NAME REGION_CODE PARTITION_NAME
----------- ------------- ----------- --------------
          1 Anna          pl          P_EMEA
          2 John          US          P_AMERICAS
          3 Yuki          jp          P_OTHER

Check partition pruning:

EXPLAIN PLAN FOR
SELECT customer_name
FROM expr_customers
WHERE UPPER(region_code) = 'PL';

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(
  NULL, NULL, 'BASIC +PARTITION'
));

The tested plan contains:

| Operation             | Pstart | Pstop |
|-----------------------|--------|-------|
| PARTITION LIST SINGLE | KEY    | KEY   |
| TABLE ACCESS FULL     |      1 |     1 |

HASH partitioning by expression

This table distributes rows using the calculated net amount:

CREATE TABLE expr_items
(
  item_id  NUMBER PRIMARY KEY,
  price    NUMBER(10,2) NOT NULL,
  discount NUMBER(10,2) NOT NULL
)
PARTITION BY HASH (price - discount)
(
  PARTITION p_h1,
  PARTITION p_h2,
  PARTITION p_h3,
  PARTITION p_h4
);
INSERT INTO expr_items VALUES (1, 100, 10);
INSERT INTO expr_items VALUES (2, 200, 20);
INSERT INTO expr_items VALUES (3, 150, 0);
INSERT INTO expr_items VALUES (4, 80, 5);
COMMIT;

Check row placement:

SELECT i.item_id,
       i.price - i.discount net_amount,
       o.subobject_name partition_name
FROM expr_items i
JOIN user_objects o
  ON o.data_object_id = DBMS_ROWID.ROWID_OBJECT(i.rowid)
ORDER BY i.item_id;

Result from the tested database:

ITEM_ID NET_AMOUNT PARTITION_NAME
------- ---------- --------------
      1         90 P_H2
      2        180 P_H2
      3        150 P_H2
      4         75 P_H4

Hash assignment is calculated internally and can differ if the partition definition changes. Applications should not depend on a particular partition name.

Check partition pruning:

EXPLAIN PLAN FOR
SELECT item_id
FROM expr_items
WHERE price - discount = 90;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(
  NULL, NULL, 'BASIC +PARTITION'
));

The tested plan contains:

| Operation             | Pstart | Pstop |
|-----------------------|--------|-------|
| PARTITION HASH SINGLE |      2 |     2 |
| TABLE ACCESS FULL     |      2 |     2 |

Check the hidden virtual columns

SELECT table_name,
       column_name,
       hidden_column,
       virtual_column,
       data_default
FROM user_tab_cols
WHERE table_name IN
  ('EXPR_SALES', 'EXPR_CUSTOMERS', 'EXPR_ITEMS')
  AND hidden_column = 'YES'
ORDER BY table_name, column_id;

The generated ORA_PART_KEY_... column names are system-defined and can differ between databases.

When to use an explicit virtual column

Use partition by expression when the derived value is needed only for data placement. Use an explicit virtual column when applications also need that value for queries, reports or indexes.

Restrictions

  • The restrictions are the same as for virtual column-based partitioning.
  • External tables are not supported because they do not support virtual columns.
  • PL/SQL function calls cannot be used in a virtual-column partitioning key.
  • If an update can move a row between partitions, enable row movement first.

Cleanup

DROP TABLE expr_sales PURGE;
DROP TABLE expr_customers PURGE;
DROP TABLE expr_items PURGE;

Summary

Oracle 26ai supports expressions with RANGE, LIST and HASH partitioning. The syntax stays compact, while matching predicates can still prune the scan to a single partition.

More details are available in the Oracle VLDB and Partitioning Guide and the Oracle AI Database 26ai New Features Guide.

Have a fun 🙂

Tomasz

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.