Document ID: MDB-WP-2026-08-EXADATA-OSS | Version: 1.0 | Published: 24 August 2026 | Prepared by: MinervaDB Inc. Database Architecture Practice | Classification: Public whitepaper
Oracle Exadata cost optimization has two honest paths. The first is to keep Exadata and cut what you pay for it: prune unused options, enable fewer cores, and negotiate before the 19c Extended Support uplift lands. The second, and the one this MinervaDB whitepaper engineers in detail, is a staged data transformation onto a fully open source data infrastructure stack — PostgreSQL 18 for OLTP, ClickHouse 26.3 LTS for analytics, Apache Kafka 4.3 with Debezium 3.6 for change data capture, and Valkey 9.1 for caching — that removes the recurring license and support line entirely while holding performance, scalability, availability, and reliability at Exadata-class SLOs.
The finding, stated up front: on a representative Exadata X11M quarter-rack estate, the annual software support bill alone (licenses at 22% of net) exceeds the five-year hardware plus subscription-support cost of the replacement open source stack, and the performance characteristics that justify Exadata — Smart Scan, Storage Indexes, RAC, Hybrid Columnar Compression — each have a measured open source equivalent when the workload is routed to the right engine rather than forced through one. The rest of this paper shows the arithmetic, the architecture, the code, and the failure modes.
1. Where Oracle Exadata spend actually goes
Most Exadata cost optimization conversations start with the rack. That is the wrong place to look. The rack is a one-time capital line; the recurring line is software licensing and its 22% annual support, and on Exadata that line is amplified by three mechanisms: every enabled database-server core is licensed at the Oracle core factor, the storage tier carries its own per-disk software license, and the features that make Exadata fast (RAC, Partitioning, Advanced Compression, In-Memory) are separately licensed options rather than part of Enterprise Edition.
The list prices below are taken from Oracle's published Technology Global Price List and Engineered Systems Price List (June/August 2026 editions). Real contracts carry discounts, frequently in the 40–70% range; the ratios between line items are what matter for the model, and the support percentage applies to net, not list.
| Line item (per processor license unless noted) | List price (USD) | Annual support (22%) | Notes |
|---|---|---|---|
| Oracle Database Enterprise Edition | $47,500 | $10,450 | Mandatory on Exadata |
| Real Application Clusters | $23,000 | $5,060 | Instance-failure RTO and consolidation |
| Partitioning | $11,500 | $2,530 | Almost universally in use on Exadata estates |
| Advanced Compression | $11,500 | $2,530 | HCC itself is Exadata-included; OLTP compression is not |
| Diagnostics Pack + Tuning Pack | $12,500 | $2,750 | Required to legally query AWR/ASH |
| Active Data Guard | $11,500 | $2,530 | Readable standby |
| Database In-Memory | $23,000 | $5,060 | Optional; common on HTAP estates |
| Exadata Storage Server Software (per disk drive) | $10,000 | $2,200 | Storage tier, independent of core licensing |
| Exadata Database Machine X11M quarter rack (hardware) | $314,681 | ~$62,936 (systems + OS) | 2 database servers, 3 storage servers |
Apply the arithmetic to a quarter rack. Each X11M database server ships with two 96-core AMD EPYC processors; with capacity-on-demand a typical estate enables 64 cores per server, so 128 enabled cores × Oracle's 0.5 core factor for AMD EPYC = 64 processor licenses (verify against the current Oracle Processor Core Factor Table at engagement time).
A "standard Exadata option set" of EE + RAC + Partitioning + Advanced Compression + Diagnostics/Tuning is $106,000 per processor at list, so 64 processors is $6.78M list and $1.49M per year in support. Add 36 storage-server disk licenses ($360,000 list, $79,200 per year support) and hardware support, and the recurring line on an illustrative quarter rack lands around $1.63M per year at list, roughly $0.65–0.98M per year after a typical 40–60% discount. Over five years that recurring line, not the rack, is the number to optimize.
Two further pressures make 2026–2027 the decision window. Oracle Database 21c reaches end of support on 31 July 2027 with no Extended Support, and 19c Premier Support ends 31 December 2029 with Extended Support carrying a +10% year-one and +20% subsequent-year uplift on the support fee (see the Oracle Database lifecycle tracker; re-verify dates against MOS note 742060.1 before quoting them in a contract). Estates that stay on Exadata will pay more to stand still.
2. Oracle Exadata cost optimization in place (what to do first)
MinervaDB is vendor-neutral, and that cuts both ways: not every Exadata estate should leave. Before any migration business case, run the four measures below. They are reversible, they require no application change, and on several engagements they have cut the recurring line by 25–40% (illustrative range; your figure comes from your own DBA_FEATURE_USAGE_STATISTICS).
2.1 Measure option usage before you pay for it
Licensing follows usage, and Oracle's own catalog tells you what is used. This is the first query on every MinervaDB Oracle takeover, and it is the evidence for pruning options at renewal. Note the licensing gate: querying DBA_HIST_* or V$ACTIVE_SESSION_HISTORY on an estate without Diagnostics Pack is itself a license event, so check CONTROL_MANAGEMENT_PACK_ACCESS first.
-- 1. Licensing gate: what packs is this instance allowed to use?
SHOW PARAMETER control_management_pack_access;
-- 2. Which separately licensed options are actually in use?
SELECT
u.name AS feature_name,
u.detected_usages,
u.currently_used,
TO_CHAR(u.first_usage_date, 'YYYY-MM-DD') AS first_used,
TO_CHAR(u.last_usage_date, 'YYYY-MM-DD') AS last_used
FROM dba_feature_usage_statistics u
WHERE u.name IN (
'Real Application Clusters (RAC)',
'Partitioning (user)',
'Advanced Compression',
'HeapCompression',
'Hybrid Columnar Compression',
'In-Memory Column Store',
'Active Data Guard - Real-Time Query on Physical Standby',
'Oracle Multitenant',
'Automatic Workload Repository',
'SQL Tuning Advisor',
'Exadata'
)
AND u.dbid = (SELECT dbid FROM v$database)
ORDER BY u.currently_used DESC, u.detected_usages DESC;
-- 3. Enabled cores that drive the processor-license count
SELECT
cpu_count_current,
cpu_core_count_current,
cpu_socket_count_current
FROM v$license;
An option showing currently_used = FALSE and last_used older than the current support term is a renewal negotiation item. In-Memory and Active Data Guard are the most common finds; Tuning Pack is the second.
2.2 Reduce enabled cores with capacity-on-demand
Exadata licenses follow enabled cores, not installed cores. Right-size from DBA_HIST_SYSMETRIC_SUMMARY (Host CPU Utilization (%), p95 over 90 days, only if Diagnostics Pack is licensed — otherwise STATSPACK or OS-level sar data) and reduce enabled cores through OEDA/dbmcli during a maintenance window. Every two cores removed on AMD EPYC saves one processor license of support every year. Blast radius: CPU headroom during peak; rollback: re-enable cores (a reboot of the database server, not a license event, but confirm with your Oracle account team in writing before changing core counts).
2.3 Offload analytics before you offload anything else
The most expensive SQL on most Exadata estates is reporting: long-running aggregations that make Exadata's Smart Scan look heroic precisely because they are being run on a row-store OLTP engine. Moving those queries to ClickHouse via CDC (Section 5.2) shrinks the Exadata CPU footprint, which in turn reduces enabled cores, which reduces support. This is also the first, lowest-risk phase of the full migration, so nothing done here is wasted if the estate later leaves Exadata entirely.
2.4 Time the negotiation to the lifecycle
Do not renew a multi-year support term that runs across the 19c Extended Support boundary without pricing the uplift in. A credible, funded open source migration plan is the most effective negotiating asset an Oracle customer has; the sections below are that plan.
3. Target open source data infrastructure stack
Exadata is one engine asked to do three jobs: transactional OLTP, analytical reporting, and hot-key lookups that in practice live in the buffer cache and result cache. The open source design principle is the opposite: route each workload class to the engine built for it, connect them with change data capture, and operate all of it with the same observability and SRE discipline. Every component below is 100% open source, community-supported, and runs on commodity x86/ARM hardware or any cloud.
3.1 Workload-to-engine mapping
| Exadata capability | What it actually does for the workload | Open source equivalent | Evidence to compare |
|---|---|---|---|
| Smart Scan / Storage Indexes | Pushes predicate filtering and column projection to storage so large scans read less I/O | ClickHouse MergeTree: columnar storage, primary-key index granules, skip indexes, PREWHERE; PostgreSQL 18 asynchronous I/O (io_method = io_uring) and parallel sequential scans for the OLTP-side reporting that remains | cell physical IO bytes saved by storage index vs system.query_log.read_bytes / read_rows |
| Hybrid Columnar Compression | 10–15× compression on cold, read-mostly data | ClickHouse codecs (ZSTD, Delta, DoubleDelta, Gorilla, T64) routinely reach comparable ratios on time-series and fact data; PostgreSQL TOAST lz4 for large values | DBA_TABLES.COMPRESS_FOR and segment sizes vs system.parts.data_compressed_bytes / data_uncompressed_bytes |
| RAC | Instance-failure RTO in seconds; horizontal read scaling within one shared-storage database | Patroni quorum failover (RTO measured in tens of seconds, Section 7); read scaling via hot standbys behind PgBouncer; ClickHouse replicas for analytic reads | GV$INSTANCE failover drill timings vs Patroni switchover/failover drill timings |
| In-Memory Column Store | Vectorised aggregation on hot tables | ClickHouse is vectorised end to end; Valkey serves the hot-key subset | AWR SQL ordered by elapsed time vs system.query_log p99 by normalized_query_hash |
| Data Guard / Active Data Guard | Physical standby, readable | PostgreSQL streaming replication (sync + async) with hot standby; pgBackRest PITR; ClickHouse cross-DC replicas | V$DATAGUARD_STATS apply lag vs pg_stat_replication.replay_lag |
| Result cache / KEEP pool | Sub-millisecond repeated reads | Valkey 9.1 cluster, write-through from the CDC stream, hash-field TTLs | AWR "Result Cache" section vs INFO commandstats, keyspace hit ratio |
The mapping is honest about one thing: PostgreSQL alone does not replace Exadata for an estate that mixes heavy analytics with OLTP. The analytics tier is what makes the performance claim hold, and it is why the architecture is a stack rather than a database swap.
4. Options considered and rejected
A migration recommendation is only credible if the alternatives were evaluated on their merits. These were, and each is a legitimate choice for a different constraint set.
Oracle Autonomous Database / Exadata Database Service on OCI or Database@Azure/AWS/Google. Genuine strengths: RAC and Data Guard semantics preserved, patching automated, and the @-cloud variants burn down hyperscaler commitments. Rejected for the cost objective because the license line moves rather than disappears (BYOL or License Included, ECPU-metered), Autonomous removes SYSDBA, RMAN, and OS access, and the exit path from Autonomous is logical-only (Data Pump or GoldenGate). For an estate whose strategic direction is off-Oracle, every Oracle cloud service deepens the moat.
EDB Postgres Advanced Server (EPAS) with Oracle-compatibility mode. Genuine strengths: packages, PL/SQL dialect, OCI-compatible connector — the shortest path when the PL/SQL estate is large and the rewrite budget is the binding constraint. Rejected as the default because it exchanges Oracle lock-in for EDB lock-in and reintroduces a per-core subscription. MinervaDB supports EPAS estates without ideology; we recommend it only against a named PL/SQL-volume requirement and we say so in writing.
PostgreSQL-only with Citus columnar or TimescaleDB for analytics. Genuine strengths: one engine to operate, one skill set, transactional consistency across OLTP and reporting. Rejected for estates with true Exadata-class scan volumes because row-store parallelism and columnar access methods in PostgreSQL do not match a purpose-built vectorised MergeTree engine on the billion-row aggregations that justified Exadata in the first place. It remains the right answer for smaller estates whose "analytics" is a few hundred GB of reporting.
Managed cloud DBaaS (Amazon RDS/Aurora, Azure Database for PostgreSQL, Cloud SQL/AlloyDB) as the landing zone. Genuine strengths: operational automation, PostgreSQL 18 availability within days of community GA. Not rejected — the stack in Figure 1 runs on any of them — but the whitepaper models self-managed on commodity hardware or IaaS because that is the configuration with no vendor line at all.
The trade-offs are documented in MinervaDB's PostgreSQL cloud guide for AWS, GCP, and Azure.
Staying on Exadata with the Section 2 measures. The right answer for estates with deep RAC-dependent ISV applications under change freeze, or where PL/SQL volume exceeds roughly 500k lines with no rewrite budget. Those estates should optimize in place and revisit at the 19c Extended Support boundary.
5. Performance parity engineering
"Without compromising on performance" is a measurable claim or it is marketing, and it is the test every Oracle Exadata cost optimization plan must pass. The measurement frame we use on every Exadata exit is the same: capture the top-N SQL by elapsed time and I/O from AWR (or STATSPACK on unlicensed estates) before migration, map each statement to its target engine, and compare against pg_stat_statements and system.query_log after migration under production-shaped load. Parity is declared per statement class, never in aggregate.
5.1 OLTP on PostgreSQL 18: partitioning, plans, and I/O
Oracle Partitioning is a paid option; PostgreSQL declarative partitioning is not, and since PostgreSQL 17 it supports partition-wise joins and aggregates, identity columns and exclusion constraints on partitioned tables, and partition pruning at execution time. The pattern below converts a typical Oracle range-partitioned order table, retains the Oracle-side NUMBER precision decisions deliberately, and adds pg_partman for the maintenance that Oracle interval partitioning did implicitly.
CREATE TABLE sales.orders (
order_id BIGINT GENERATED ALWAYS AS IDENTITY,
customer_id BIGINT NOT NULL,
order_ts TIMESTAMP(0) NOT NULL, -- Oracle DATE carries time: map to timestamp(0), never date
status VARCHAR(16) NOT NULL,
amount NUMERIC(14,2) NOT NULL, -- NUMBER(14,2) → numeric(14,2); bare NUMBER hot columns → bigint where domain allows
region_code CHAR(3) NOT NULL,
CONSTRAINT pk_orders PRIMARY KEY (order_id, order_ts)
) PARTITION BY RANGE (order_ts);
-- pg_partman manages monthly partitions and pre-creates 3 months ahead
SELECT partman.create_parent(
p_parent_table => 'sales.orders',
p_control => 'order_ts',
p_interval => '1 month',
p_premake => 3
);
CREATE INDEX ix_orders_customer_ts
ON sales.orders (customer_id, order_ts DESC);
-- Empty string ≠ NULL in PostgreSQL. Oracle code that relied on '' IS NULL must be
-- audited; this CHECK makes the migration assumption explicit and testable.
ALTER TABLE sales.orders
ADD CONSTRAINT ck_orders_status_not_blank CHECK (status <> '');
Plan reasoning, not just DDL: the hot OLTP query "recent orders for a customer" must prune to one or two partitions and walk ix_orders_customer_ts. Verify it with EXPLAIN (ANALYZE, BUFFERS) and look for Partitions removed or a plan that lists only the matching child indexes; a plan that appends every partition is a missing pruning predicate on order_ts, the most common week-one regression on Oracle migrations.
EXPLAIN (ANALYZE, BUFFERS, SETTINGS) SELECT order_id, order_ts, status, amount FROM sales.orders WHERE customer_id = 8812931 AND order_ts >= now() - INTERVAL '45 days' ORDER BY order_ts DESC LIMIT 50; -- Expected shape (trimmed): -- Limit (actual time=0.041..0.118 rows=50 loops=1) -- -> Merge Append -- -> Index Scan Backward using orders_p2026_08_customer_id_order_ts_idx on orders_p2026_08 -- Index Cond: ((customer_id = 8812931) AND (order_ts >= ...)) -- -> Index Scan Backward using orders_p2026_07_customer_id_order_ts_idx on orders_p2026_07 -- Buffers: shared hit=14 -- Two partitions touched, 14 buffer hits, no heap fetches beyond the index — this is the target shape.
PostgreSQL 18 changes the I/O story that Exadata customers care about most. The new asynchronous I/O subsystem (io_method = worker by default, io_uring on Linux where enabled at build time) lets sequential scans, bitmap heap scans, and VACUUM issue reads ahead of consumption, and the release notes document the supporting changes: skip-scan on B-tree indexes, parallel GIN builds, and pg_stat_io byte-level accounting (PostgreSQL 18 release notes).
The configuration deltas from default that we apply on an Exadata-replacement OLTP node are listed with their reload/restart requirement, per house convention. Proposed values are for a 64 vCPU / 512 GB / NVMe node.
| Parameter | Default → proposed (unit) | Applies via | Justifying metric |
|---|---|---|---|
shared_buffers | 128MB → 128GB (bytes) | restart | pg_stat_io hit ratio; pg_buffercache usage counts |
effective_cache_size | 4GB → 384GB (bytes) | reload | OS page cache size; planner cost accuracy |
io_method | worker → io_uring (enum) | restart | pg_stat_io read latency under seq-scan load (PG 18+, Linux build with liburing) |
io_workers | 3 → 8 (count) | restart | Only when io_method = worker; pg_stat_io backend type io worker |
max_parallel_workers_per_gather | 2 → 8 (count) | reload | EXPLAIN ANALYZE "Workers Launched" on reporting queries that stay on PG |
wal_compression | off → zstd (enum) | reload | pg_stat_wal.wal_bytes; replication bandwidth |
synchronous_commit | on → on, with a sync standby in synchronous_standby_names (enum) | reload | RPO = 0 requirement; pg_stat_replication.sync_state |
autovacuum_vacuum_cost_limit | -1 (200) → 2000 (cost units) | reload | pg_stat_user_tables.n_dead_tup trend; bloat under Oracle-style update-heavy load |
track_io_timing | off → on (bool) | reload | Required for pg_stat_statements I/O time columns |
Test every value in staging under replayed production load before applying to production, and keep a robust DR posture (verified pgBackRest restore) before any restart-class change.
5.2 Analytics on ClickHouse: the Smart Scan replacement
The reporting queries that made Exadata Smart Scan indispensable have a consistent shape: filter by time and a low-cardinality dimension, aggregate measures, group by a handful of columns. On a columnar MergeTree table with the right ORDER BY, ClickHouse reads only the granules and columns the query touches, which is the same I/O-avoidance idea Smart Scan implements in storage cells, applied at the storage format instead of the storage hardware. Engine declarations carry full parameter lists — house rule, and the difference between a table that replicates and one that silently does not.
CREATE TABLE sales.orders_local ON CLUSTER 'analytics'
(
order_id UInt64,
customer_id UInt64,
order_ts DateTime('UTC') CODEC(DoubleDelta, ZSTD(3)),
status LowCardinality(String),
amount Decimal(14, 2) CODEC(T64, ZSTD(3)),
region_code LowCardinality(FixedString(3)),
_version UInt64, -- Debezium source.lsn / ts_ms, drives ReplacingMergeTree dedup
_deleted UInt8 DEFAULT 0,
INDEX ix_customer customer_id TYPE bloom_filter(0.01) GRANULARITY 4
)
ENGINE = ReplicatedReplacingMergeTree(
'/clickhouse/tables/{shard}/sales/orders_local',
'{replica}',
_version
)
PARTITION BY toYYYYMM(order_ts)
ORDER BY (region_code, status, order_ts, order_id)
TTL order_ts + INTERVAL 18 MONTH TO VOLUME 'cold_s3'
SETTINGS
index_granularity = 8192,
storage_policy = 'tiered',
min_bytes_for_wide_part = 10485760,
ttl_only_drop_parts = 1;
CREATE TABLE sales.orders ON CLUSTER 'analytics'
AS sales.orders_local
ENGINE = Distributed('analytics', 'sales', 'orders_local', cityHash64(customer_id));
-- Kafka engine consumer for the Debezium topic (JSON unwrapped by the ExtractNewRecordState SMT)
CREATE TABLE sales.orders_kafka ON CLUSTER 'analytics'
(
order_id UInt64, customer_id UInt64, order_ts DateTime('UTC'),
status String, amount Decimal(14,2), region_code String,
__lsn UInt64, __deleted String
)
ENGINE = Kafka
SETTINGS
kafka_broker_list = 'kafka-1:9092,kafka-2:9092,kafka-3:9092',
kafka_topic_list = 'pg.sales.orders',
kafka_group_name = 'clickhouse-sales-orders',
kafka_format = 'JSONEachRow',
kafka_num_consumers = 4,
kafka_max_block_size = 65536,
kafka_handle_error_mode = 'stream';
CREATE MATERIALIZED VIEW sales.orders_mv ON CLUSTER 'analytics'
TO sales.orders_local AS
SELECT
order_id, customer_id, order_ts, status, amount,
toFixedString(region_code, 3) AS region_code,
__lsn AS _version,
if(__deleted = 'true', 1, 0) AS _deleted
FROM sales.orders_kafka;
Plan reasoning for the reporting query: with ORDER BY (region_code, status, order_ts, order_id), a query filtering on region and a month range touches only the matching partition and the primary-key granules for that region prefix. EXPLAIN indexes = 1 shows the pruning; system.query_log shows the outcome. On ClickHouse 26.3 LTS the evidence pair is EXPLAIN PIPELINE plus system.trace_log; 26.7 adds EXPLAIN ANALYZE.
EXPLAIN indexes = 1
SELECT
region_code,
status,
count() AS orders,
sum(amount) AS revenue
FROM sales.orders FINAL
WHERE region_code = 'APJ'
AND order_ts >= toDateTime('2026-07-01 00:00:00', 'UTC')
AND order_ts < toDateTime('2026-08-01 00:00:00', 'UTC')
AND _deleted = 0
GROUP BY region_code, status;
-- Look for: MinMax → Partition → PrimaryKey with "Selected Granules" far below "Initial Granules".
SELECT
normalized_query_hash,
count() AS runs,
quantile(0.99)(query_duration_ms) AS p99_ms,
formatReadableSize(avg(read_bytes)) AS avg_read,
avg(read_rows) AS avg_rows
FROM system.query_log
WHERE type = 'QueryFinish'
AND event_time >= now() - INTERVAL 1 DAY
AND has(tables, 'sales.orders_local')
GROUP BY normalized_query_hash
ORDER BY p99_ms DESC
LIMIT 20;
Two ClickHouse 26.3 LTS specifics matter for an Exadata replacement. First, async_insert became enabled by default in 26.3; on a CDC-fed estate pin async_insert = 0 in the ingestion profile before cutover and re-enable deliberately under Keeper observation, because the change alters part-count and Keeper load characteristics. Second, use FINAL or argMax patterns on ReplacingMergeTree only where the reporting SLA tolerates it; for dashboards, a scheduled OPTIMIZE ... FINAL on closed partitions or a projection is cheaper. The ClickHouse engineering for this tier is delivered by our sister company, ChistaDATA, and scoped through MinervaDB's ClickHouse consulting practice.
5.3 Hot keys on Valkey 9.1
Oracle's result cache and KEEP buffer pool absorb the repeated point reads (session state, entitlement lookups, reference data) that would otherwise be latency outliers. Valkey 9.1 — BSD-3-licensed, Linux Foundation governed, with hash-field expiration and multi-database cluster mode since 9.0 (Valkey 9 release blog) — takes that role with write-through invalidation from the CDC stream, so the cache never serves a value newer data has superseded.
import json, os
from confluent_kafka import Consumer
from valkey.cluster import ValkeyCluster
vk = ValkeyCluster(
host=os.environ["VALKEY_HOST"], port=6379,
password=os.environ["VALKEY_PASSWORD"], ssl=True,
)
consumer = Consumer({
"bootstrap.servers": os.environ["KAFKA_BOOTSTRAP"],
"group.id": "valkey-entitlement-cache",
"auto.offset.reset": "earliest",
"enable.auto.commit": False,
})
consumer.subscribe(["pg.sales.customer_entitlements"])
while True:
msg = consumer.poll(1.0)
if msg is None or msg.error():
continue
row = json.loads(msg.value())
key = f"ent:{row['customer_id']}"
if row.get("__deleted") == "true":
vk.delete(key)
else:
# Hash with per-field TTL (Valkey 9.0+): plan fields expire independently
vk.hset(key, mapping={"tier": row["tier"], "limits": row["limits_json"]})
vk.hexpire(key, 86400, "limits")
consumer.commit(msg)
6. Scalability without RAC
RAC scales one database across nodes over shared storage. The open source stack scales each tier by the mechanism that suits its access pattern, and the honest statement is that write scaling for a single PostgreSQL primary is vertical (PostgreSQL 18 on a 2-socket 192-core node comfortably exceeds the enabled-core footprint most Exadata quarter racks license) while read scaling and analytic scaling are horizontal.
Read scaling for Oracle Exadata cost optimization targets: hot standbys behind PgBouncer with hot_standby_feedback = on and a read-routing pool; replication lag from pg_stat_replication is the SLO. Analytic scaling: add ClickHouse shards, rebalance with Distributed table weights, and keep system.parts per-partition counts inside the merge budget; parallel replicas (enable_parallel_replicas) fan a single heavy query across replicas of one shard.
Write scaling beyond a single primary, if the workload truly demands it after measurement (pg_stat_database.xact_commit rate against saturation evidence in pg_stat_activity wait events), is Citus sharding on a tenant or hash key — a step we scope only after vertical headroom is measured, because it changes the data model.
Connection scaling is solved at the pooler: Oracle's dedicated-server assumptions meet PostgreSQL's process-per-connection model, and PgBouncer sizing from measured concurrency is part of every migration design, not an afterthought.
[databases] sales_rw = host=pg-primary.internal port=5432 dbname=sales pool_size=64 sales_ro = host=pg-replicas.internal port=5432 dbname=sales pool_size=128 [pgbouncer] listen_addr = 0.0.0.0 listen_port = 6432 auth_type = scram-sha-256 auth_file = /etc/pgbouncer/userlist.txt pool_mode = transaction max_client_conn = 10000 default_pool_size = 64 reserve_pool_size = 16 server_idle_timeout = 300 max_prepared_statements = 200 ; 1.24+ enables prepared statements in transaction mode server_tls_sslmode = verify-full server_tls_ca_file = /etc/ssl/certs/internal-ca.pem stats_period = 60
7. Availability and reliability engineering
Availability numbers are engineered, not quoted, and Oracle Exadata cost optimization is meaningless if the replacement stack cannot match the protection tier. Exadata estates typically run RAC for instance failure plus Data Guard for site failure; the open source stack reaches the same protection tiers with Patroni for automated failover, synchronous replication for RPO = 0, pgBackRest for point-in-time recovery, and ReplicatedMergeTree with a dedicated Keeper ensemble for the analytics tier. The arithmetic below is what we put in front of a customer before claiming any nines.
| Failure mode | Detection | Recovery action | Illustrative RTO budget | RPO | Evidence source |
|---|---|---|---|---|---|
| PostgreSQL primary crash | Patroni ttl 30 s / loop_wait 10 s | Quorum failover to sync standby; PgBouncer re-resolves via Patroni REST | ≤ 45 s (drill-measured, quarterly) | 0 (sync standby) | patronictl history, pg_stat_replication |
| AZ loss | etcd quorum + Patroni | Failover to surviving AZ; async standby promoted to sync | ≤ 60 s | 0 | Drill log; pg_last_wal_receive_lsn() on survivors |
| Logical corruption / bad deploy | Application / monitoring | pgBackRest PITR to a timestamp; ClickHouse partition restore | Minutes to hours by data size (measured monthly) | Seconds (WAL archive interval) | pgbackrest info, restore drill timings |
| ClickHouse replica loss | Keeper session expiry | Queries route to surviving replica; replacement replica re-fetches parts | 0 for reads; rebuild in background | 0 | system.replicas, system.replication_queue |
| Kafka broker loss | KRaft controller | ISR shrinks; producers with acks=all continue on min.insync=2 | 0 | 0 | UnderReplicatedPartitions metric |
| Region loss | Manual / runbook gate | Promote DR PostgreSQL; ClickHouse cross-region replicas serve reads | ≤ 15 min (runbook-drilled) | ≤ async lag (measured) | Quarterly DR drill report |
scope: sales-pg18
namespace: /minervadb/
name: pg-az-a
restapi:
listen: 0.0.0.0:8008
connect_address: pg-az-a.internal:8008
etcd3:
hosts: etcd-1.internal:2379,etcd-2.internal:2379,etcd-3.internal:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576 # bytes; async standby is never promoted beyond this
synchronous_mode: quorum # Patroni 4.x quorum-based synchronous replication
synchronous_node_count: 1
failsafe_mode: true # keep primary up if DCS is unreachable but members are
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
wal_level: replica
max_wal_senders: 16
max_replication_slots: 16
hot_standby: "on"
hot_standby_feedback: "on"
wal_keep_size: 8GB
archive_mode: "on"
archive_command: "pgbackrest --stanza=sales archive-push %p"
restore_command: "pgbackrest --stanza=sales archive-get %f %p"
postgresql:
listen: 0.0.0.0:5432
connect_address: pg-az-a.internal:5432
data_dir: /pgdata/18/main
bin_dir: /usr/lib/postgresql/18/bin
authentication:
replication:
username: replicator
password: ${PG_REPL_PASSWORD}
superuser:
username: postgres
password: ${PG_SUPER_PASSWORD}
create_replica_methods:
- pgbackrest
- basebackup
pgbackrest:
command: /usr/bin/pgbackrest --stanza=sales --delta restore
keep_data: true
no_params: true
tags:
nofailover: false
noloadbalance: false
sync_priority: 100 # prefer this node as the synchronous standby when it is a replica
[global]
repo1-type=s3
repo1-s3-bucket=${PGBACKREST_BUCKET}
repo1-s3-endpoint=s3.ap-south-1.amazonaws.com
repo1-s3-region=ap-south-1
repo1-path=/pgbackrest
repo1-cipher-type=aes-256-cbc
repo1-cipher-pass=${PGBACKREST_CIPHER_PASS}
repo1-retention-full=4
repo1-retention-diff=14
repo1-retention-archive-type=full
repo1-bundle=y
repo1-block=y
process-max=8
compress-type=zst
compress-level=3
archive-async=y
spool-path=/var/spool/pgbackrest
log-level-console=info
[sales]
pg1-path=/pgdata/18/main
pg1-port=5432
pg1-user=postgres
# 2.59.0 restricts root execution to `restore` by default — run backups as the postgres user.
Reliability discipline is where Exadata-class expectations are actually met: monthly pgbackrest restore --type=time validation on an isolated host, quarterly Patroni switchover drills with timings recorded, quarterly ClickHouse partition-restore drills via clickhouse-backup, and a written escalation matrix. MinervaDB's 24×7 emergency DBA coverage operates on S1 15-minute response, and the drills are what make that response meaningful.
8. The migration program: assess, convert, replicate, cut over
8.1 Assessment: measure the estate, never guess it
Oracle Exadata cost optimization by migration starts here: effort is measured from the Oracle catalog. The output is an object-count matrix by complexity class and conversion route (automatic / assisted / manual rewrite), and it is the single artifact that turns a whitepaper into a funded program.
-- Object inventory by type
SELECT owner, object_type, COUNT(*) AS objects
FROM dba_objects
WHERE owner IN (${APP_SCHEMAS})
GROUP BY owner, object_type
ORDER BY objects DESC;
-- PL/SQL volume by unit (lines of code drive conversion effort, not table count)
SELECT owner, name, type, COUNT(*) AS loc
FROM dba_source
WHERE owner IN (${APP_SCHEMAS})
GROUP BY owner, name, type
ORDER BY loc DESC;
-- Constructs with no direct community-PostgreSQL equivalent (each is a line item)
SELECT owner, name, type, COUNT(*) AS hits, 'AUTONOMOUS_TRANSACTION' AS construct
FROM dba_source
WHERE owner IN (${APP_SCHEMAS})
AND UPPER(text) LIKE '%PRAGMA AUTONOMOUS_TRANSACTION%'
GROUP BY owner, name, type
UNION ALL
SELECT owner, name, type, COUNT(*), 'BULK_COLLECT'
FROM dba_source
WHERE owner IN (${APP_SCHEMAS})
AND UPPER(text) LIKE '%BULK COLLECT%'
GROUP BY owner, name, type
UNION ALL
SELECT owner, name, type, COUNT(*), 'CONNECT_BY'
FROM dba_source
WHERE owner IN (${APP_SCHEMAS})
AND UPPER(text) LIKE '%CONNECT BY%'
GROUP BY owner, name, type
ORDER BY hits DESC;
-- Feature dependencies: DB links, AQ, VPD, MV fast refresh
SELECT 'DB_LINK' AS dependency, COUNT(*) AS n FROM dba_db_links
UNION ALL SELECT 'AQ_QUEUE', COUNT(*) FROM dba_queues WHERE owner IN (${APP_SCHEMAS})
UNION ALL SELECT 'VPD_POLICY', COUNT(*) FROM dba_policies WHERE object_owner IN (${APP_SCHEMAS})
UNION ALL SELECT 'MV_FAST_REFRESH', COUNT(*) FROM dba_mviews WHERE owner IN (${APP_SCHEMAS}) AND refresh_method = 'FAST';
8.2 Conversion with ora2pg and orafce
Ora2Pg produces the assessment report (with its own cost-unit estimate per object) and performs schema, data, and a first-pass PL/SQL conversion; orafce supplies the Oracle-compatible functions (NVL, DECODE, ADD_MONTHS, DBMS_OUTPUT, and others) that keep converted code readable. Packages become schemas plus functions; package state becomes session GUCs or a state table; autonomous transactions become dblink or a background-worker pattern, chosen per call site.
ORACLE_DSN dbi:Oracle:host=exa-scan.internal;sid=SALESPDB;port=1521
ORACLE_USER ${ORA_MIG_USER}
ORACLE_PWD ${ORA_MIG_PASSWORD}
SCHEMA SALES
PG_VERSION 18
TYPE TABLE,VIEW,SEQUENCE,TRIGGER,FUNCTION,PROCEDURE,PACKAGE,TYPE,PARTITION,MVIEW
EXPORT_SCHEMA 1
DATA_TYPE DATE:timestamp(0),LONG:text,LONG RAW:bytea,CLOB:text,NCLOB:text,BLOB:bytea,BFILE:bytea,RAW:bytea,ROWID:oid,FLOAT:double precision,DEC:decimal,DECIMAL:decimal,DOUBLE PRECISION:double precision,INT:numeric,INTEGER:numeric,REAL:real,SMALLINT:smallint,BINARY_FLOAT:double precision,BINARY_DOUBLE:double precision,TIMESTAMP:timestamp,XMLTYPE:xml,BINARY_INTEGER:integer,PLS_INTEGER:integer,TIMESTAMP WITH TIME ZONE:timestamp with time zone,TIMESTAMP WITH LOCAL TIME ZONE:timestamp with time zone
PG_NUMERIC_TYPE 1
PG_INTEGER_TYPE 1
DEFAULT_NUMERIC bigint
USE_ORAFCE 1
PLSQL_PGSQL 1
NULL_EQUAL_EMPTY 0 ; force the '' vs NULL audit instead of masking it
ESTIMATE_COST 1
COST_UNIT_VALUE 5
PARALLEL_TABLES 8
JOBS 8
ORACLE_COPIES 8
DATA_LIMIT 20000
FILE_PER_TABLE 1
OUTPUT_DIR /migration/sales
ora2pg -c /migration/ora2pg.conf -t SHOW_REPORT --estimate_cost --dump_as_html > /migration/sales/assessment.html ora2pg -c /migration/ora2pg.conf -t TABLE -o schema_tables.sql ora2pg -c /migration/ora2pg.conf -t PACKAGE -o packages.sql ora2pg -c /migration/ora2pg.conf -t FUNCTION -o functions.sql # Data: COPY pipeline, 8 parallel jobs, indexes and FKs applied after load ora2pg -c /migration/ora2pg.conf -t COPY -j 8 -J 8
8.3 CDC with Debezium: parallel run and rehearsable cutover
The cutover is where an Oracle Exadata cost optimization program is won or lost, so it must be rehearsable and reversible. Debezium 3.6's Oracle connector (documentation) captures from LogMiner without a GoldenGate license, or from OpenLogReplicator for lower source overhead on high-redo estates; XStream requires a GoldenGate license and is not used.
The stream lands in Kafka 4.3 (KRaft-only since 4.0) and is applied to PostgreSQL by the JDBC sink connector during the parallel run. After cutover, the same topology is reversed — Debezium's PostgreSQL connector on pgoutput feeding a JDBC sink into Oracle — and kept warm as the rollback path until sign-off.
{
"name": "ora-sales-source",
"config": {
"connector.class": "io.debezium.connector.oracle.OracleConnector",
"tasks.max": "1",
"database.hostname": "exa-scan.internal",
"database.port": "1521",
"database.user": "${ORA_CDC_USER}",
"database.password": "${ORA_CDC_PASSWORD}",
"database.dbname": "SALESCDB",
"database.pdb.name": "SALESPDB",
"topic.prefix": "ora",
"table.include.list": "SALES.ORDERS,SALES.ORDER_ITEMS,SALES.CUSTOMER_ENTITLEMENTS",
"database.connection.adapter": "logminer",
"log.mining.strategy": "online_catalog",
"log.mining.batch.size.default": "20000",
"log.mining.transaction.retention.ms": "3600000",
"lob.enabled": "false",
"decimal.handling.mode": "precise",
"time.precision.mode": "adaptive",
"snapshot.mode": "initial",
"schema.history.internal.kafka.bootstrap.servers": "kafka-1:9092,kafka-2:9092,kafka-3:9092",
"schema.history.internal.kafka.topic": "schema-history.ora.sales",
"heartbeat.interval.ms": "10000",
"transforms": "unwrap",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
"transforms.unwrap.delete.handling.mode": "rewrite",
"transforms.unwrap.add.fields": "op,source.scn,source.ts_ms",
"key.converter": "io.apicurio.registry.utils.converter.AvroConverter",
"value.converter": "io.apicurio.registry.utils.converter.AvroConverter",
"key.converter.apicurio.registry.url": "http://apicurio.internal:8080/apis/registry/v2",
"value.converter.apicurio.registry.url": "http://apicurio.internal:8080/apis/registry/v2"
}
}
Source-side prerequisites are the part Oracle DBAs must own: ARCHIVELOG mode, minimal supplemental logging at database level plus ALL COLUMNS on captured tables, and a CDC user granted the LogMiner privileges the Debezium documentation lists. LogMiner load on the source is measurable in V$SESSION and, on licensed estates, in ASH; on a high-redo estate that overhead is the reason to evaluate OpenLogReplicator.
8.4 Cutover runbook (excerpt) with verification and rollback
The full cutover runbook is a versioned MinervaDB deliverable (MDB-RUN-*) with purpose, scope, prerequisites, roles, stepwise commands, expected output, verification after each phase, rollback, and an escalation matrix. The core sequence, with its verification queries, is reproduced here because it is the part readers ask for.
-- GATE 1 (Oracle side, writes already stopped at the application tier): confirm no in-flight transactions
SELECT COUNT(*) AS active_txns
FROM v$transaction;
-- expected: 0
-- GATE 2 (Kafka Connect): confirm the source connector has no lag (consumer group for the sink)
-- kafka-consumer-groups.sh --bootstrap-server kafka-1:9092 --describe --group connect-pg-sales-sink
-- expected: LAG column = 0 on every partition
-- GATE 3 (PostgreSQL side): per-table reconciliation against Oracle counts captured at GATE 1
SELECT
'orders' AS table_name,
COUNT(*) AS row_count,
md5(string_agg(order_id::text || ':' || amount::text, ',' ORDER BY order_id)) AS content_hash
FROM sales.orders
WHERE order_ts >= DATE '2026-01-01'; -- hot-window hash; full-table hashes run in the parallel-run reports
-- GATE 4: promote PostgreSQL to system of record (reverse CDC connector started, applications re-pointed)
-- Verification after cutover:
SELECT slot_name, active, confirmed_flush_lsn,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS reverse_cdc_lag
FROM pg_replication_slots
WHERE slot_name = 'debezium_reverse';
-- expected: active = t, lag in low MB and falling
-- ROLLBACK (any time before sign-off): stop application writes to PostgreSQL, wait for reverse_cdc_lag = 0 bytes,
-- re-point connection strings to the Oracle service, confirm V$TRANSACTION shows application activity, resume.
-- No object is dropped, truncated, or detached at any point in this runbook without a written confirmation gate.
9. Five-year TCO model (illustrative)
Every figure in this table is illustrative and built from the list prices in Section 1 with a stated discount assumption, so that a reader can substitute their own contract values. It deliberately excludes people cost on both sides — an Exadata estate and an open source stack both need a database engineering function, and the honest difference is skills mix, not headcount. It also excludes the one-time migration program, which is estate-specific and comes out of the Section 8.1 assessment.
| Cost line (5 years, USD, illustrative) | Exadata X11M quarter rack, 64 processor licenses, 50% discount | Open source stack (self-managed, commodity/IaaS) |
|---|---|---|
| Hardware / infrastructure | $157k rack (net) + $315k hardware support | 3× PostgreSQL nodes (64 vCPU/512 GB/NVMe), 4× ClickHouse nodes, 3× Keeper, 3× Kafka, 6× Valkey, 3× etcd, 2× PgBouncer/Connect: ≈ $450k–$700k purchased, or equivalent reserved IaaS |
| Database software licenses (net, one-time) | $3.39M (EE + RAC + Partitioning + Adv. Compression + Diag/Tuning) + $180k storage software | $0 — PostgreSQL License, Apache 2.0 (ClickHouse, Kafka, Debezium), BSD-3 (Valkey), MIT (Patroni, pgBackRest) |
| Annual software support (22% of net), ×5 | $3.93M (rising +10%/+20% under 19c Extended Support from 2030) | $0 vendor line; optional subscription support from an independent provider such as MinervaDB, priced per engagement |
| Object storage for backups and cold tiers | ZDLRA or third-party; estate-specific | ≈ $60k–$120k (S3-class, 5 years, 100–200 TB) |
| Five-year total (excluding people and migration) | ≈ $7.9M+ | ≈ $0.5M–$0.8M infrastructure + support subscription |
The order-of-magnitude gap survives any reasonable discount assumption because the open source column has no line that scales with cores.
What the gap buys is not free: it is spent on the migration program, on the skills to operate three engines instead of one, and on the Data SRE discipline that keeps the SLOs honest. For most estates in the 10–100 TB range, the support line alone repays that investment inside the first support renewal cycle — but that claim is only true for your estate once the Section 8.1 assessment and a load-replay test have been run, which is why MinervaDB does not publish a customer number without them.
10. Risks, honest edges, and where this does not apply
PL/SQL volume is the schedule driver. Data size determines the bulk-load window; procedural code determines the program length. Estates above roughly 500k lines of PL/SQL with packages, autonomous transactions, and REF CURSOR-heavy APIs should evaluate EPAS with eyes open, or phase the exit application by application.
Semantics that silently change. Empty string versus NULL, DATE with time component, NUMBER without precision, sequence CACHE behaviour, and case-sensitive identifiers are the correctness classes that survive a passing conversion and fail in production. Each has a mandatory test in the MinervaDB migration test suite; none is optional.
Optimizer differences are the week-two incident source. There are no hints in community PostgreSQL (pg_hint_plan only under governance), cardinality estimation differs, and Oracle's adaptive plans have no equivalent. Capture AWR top SQL before migration, compare against pg_stat_statements after, and budget tuning time in the stabilization phase.
ISV applications. Packaged applications certified only on Oracle (many ERP, core banking, and telecom billing platforms) cannot be migrated by the database team; the decision belongs to the application roadmap. The Section 2 measures still apply.
Materialized-view fast refresh ON COMMIT, Advanced Queuing, and fine-grained VPD map to application-level patterns (incremental views, pgmq or Kafka, row-level security) rather than one-to-one features; each is a design decision, not a conversion.
What we did not test for this paper. No benchmark numbers are published here because a benchmark without your workload shape, hardware, and configuration deltas is noise. Where a MinervaDB engagement includes a load-replay comparison, the methodology (hardware, versions, config deltas, dataset, run count, median and spread) is published before the results, per house rule.
Version boundaries. Claims above are pinned to PostgreSQL 18 (asynchronous I/O, skip scan), ClickHouse 26.3 LTS (async_insert default change; 25.8 LTS leaves support on 29 August 2026), Kafka 4.x (KRaft-only), Debezium 3.6, Valkey 9.x, and Oracle 19c/26ai lifecycle dates as of August 2026. Re-verify before relying on any of them in a contract.
11. FAQ: Oracle Exadata cost optimization and open source migration
Is Oracle Exadata cost optimization possible without leaving Oracle?
Yes. Measure option usage in DBA_FEATURE_USAGE_STATISTICS, reduce enabled cores with capacity-on-demand, offload analytics to ClickHouse via CDC, and negotiate ahead of the 19c Extended Support uplift. Those four measures are reversible and typically remove 25–40% of the recurring line (illustrative; your figure comes from your own catalog).
Can PostgreSQL really match Exadata performance?
For OLTP, PostgreSQL 18 on a modern two-socket NVMe node matches or exceeds the enabled-core footprint most quarter-rack estates license, and the plan-level evidence is EXPLAIN (ANALYZE, BUFFERS) against the same statements. For Exadata-class analytics, PostgreSQL alone is not the answer; ClickHouse is, and that is why the target is a stack. Parity is declared per statement class from pg_stat_statements and system.query_log, never in aggregate.
How do we replace RAC?
RAC solves instance-failure RTO and read scaling. Patroni quorum failover with a synchronous standby delivers drill-measured RTO in the tens of seconds at RPO = 0; hot standbys behind PgBouncer deliver read scaling. Genuine write-anywhere requirements are rare and are evaluated separately, with the conflict analysis done before any active-active design is proposed.
Do we need Oracle GoldenGate for the migration?
No. Debezium's Oracle connector captures from LogMiner without a GoldenGate license; OpenLogReplicator is the lower-overhead alternative on high-redo estates. GoldenGate remains a valid choice where the customer already licenses it.
What is the rollback plan if cutover fails?
Reverse CDC from PostgreSQL to Oracle runs from the moment of cutover until written sign-off. Rollback is: stop application writes, drain reverse lag to zero (measured in pg_replication_slots), re-point connection strings to Oracle. Nothing is dropped, truncated, or decommissioned before sign-off.
How long does an Exadata to open source migration take?
Illustratively 30–42 weeks for a 20 TB estate with moderate PL/SQL, per Figure 2. The schedule scales with procedural code volume rather than data size; the assessment in Section 8.1 replaces that illustration with a measured estimate.
Who supports the open source stack in production?
Community PostgreSQL, ClickHouse, Kafka, Debezium, and Valkey are supported by their projects; enterprise-grade 24×7 support with response SLAs comes from an independent provider. MinervaDB operates on S1 15 minutes / S2 12 hours / S3 24 hours / S4 48 hours, with ClickHouse engineering delivered through ChistaDATA.
Next steps
An Oracle Exadata cost optimization program starts with evidence, not a proposal. MinervaDB's Exadata assessment delivers, within four weeks, the feature-usage baseline and enabled-core analysis for in-place savings, the object and PL/SQL complexity matrix, the AWR or STATSPACK top-SQL baseline mapped to target engines, and a versioned migration plan with rollback gates — the MDB-MIG deliverable that turns this whitepaper into your program. Talk to the MinervaDB data modernization practice, review our PostgreSQL consulting and PostgreSQL remote DBA services, or book an architecture consultation.
Standing caveat for all guidance in this document: test every change in a staging environment that mirrors production before applying it, and maintain a verified disaster-recovery posture (tested restores, not just backups) throughout any migration.
References
- Oracle Corporation, Oracle Technology Global Price List and Oracle Engineered Systems Price List; Exadata X11M announcement; Exadata documentation.
- PostgreSQL Global Development Group, PostgreSQL 18 released; release notes; logical replication; pg_stat_statements; versioning policy.
- ClickHouse, which version to use in production; Replicated table engines; Kafka table engine; 2026 changelog.
- Debezium, releases and Oracle connector; Apache Kafka, 4.3.1 release.
- Patroni, pgBackRest, PgBouncer, Ora2Pg, orafce, OpenLogReplicator, Valkey 9.
- Oracle Database lifecycle dates: endoflife.date (re-verify against MOS 742060.1).
- Related MinervaDB reading: PostgreSQL migration rollback strategy; PostgreSQL on AWS, GCP, and Azure.
Revision history: v1.0, 24 August 2026 — initial public release. Prepared by MinervaDB Database Architecture Practice; reviewed by MinervaDB PostgreSQL and Oracle practice leads; approved by Shiv Iyer, Founder & CEO, MinervaDB Inc.