Skip to content

Oracle to Azure Database for PostgreSQL Migration

When to choose PostgreSQL, how to use ora2pg for assessment and conversion, PL/SQL to PL/pgSQL conversion patterns, extension ecosystem, and Citus for horizontal scaling.


When to choose Azure PostgreSQL

Choose Azure Database for PostgreSQL Flexible Server when your organization has an open-source mandate or preference, cost sensitivity is a primary driver (zero license cost), the application can adapt to PL/pgSQL (syntactically closer to PL/SQL than T-SQL), or you need advanced spatial (PostGIS), vector search (pgvector), or horizontal scale-out (Citus) capabilities.


1. Azure Database for PostgreSQL for Oracle DBAs

PostgreSQL is the world's most advanced open-source relational database. Azure Database for PostgreSQL Flexible Server provides a fully managed deployment with enterprise features.

Oracle concept PostgreSQL equivalent
Oracle Instance PostgreSQL server (cluster)
Oracle Database Database
Schema Schema (same concept)
Tablespace Tablespace (similar, less commonly used)
PL/SQL PL/pgSQL (similar syntax)
Oracle packages Schemas + functions/procedures
SQL*Plus psql
RMAN Automated backups (Azure-managed)
Data Guard Zone-redundant HA + read replicas
RAC Citus extension (distributed)
Oracle Text Full-text search (tsvector/tsquery)
Oracle Spatial PostGIS extension
Oracle Advanced Queuing LISTEN/NOTIFY + Azure Service Bus
DBMS_SCHEDULER pg_cron extension
AWR / ASH pg_stat_statements + Query Performance Insight
Enterprise Manager Azure Portal + pgAdmin

2. ora2pg assessment and conversion

2.1 What is ora2pg

ora2pg is the primary open-source tool for Oracle-to-PostgreSQL migration. It performs:

  • Schema analysis with complexity scoring
  • Schema conversion (tables, views, sequences, indexes, constraints, triggers, procedures, functions, packages, types)
  • Data migration with parallel export/import
  • PL/SQL to PL/pgSQL automated conversion

2.2 Installing ora2pg

# On Ubuntu/Debian (recommended for migration workstation)
sudo apt-get update
sudo apt-get install -y ora2pg

# On RHEL/CentOS
sudo yum install -y ora2pg

# From source (latest version)
git clone https://github.com/darold/ora2pg.git
cd ora2pg
perl Makefile.PL
make && sudo make install

# Verify installation
ora2pg --version

2.3 Running an assessment

# Create ora2pg configuration
mkdir -p /opt/ora2pg/migration
cat > /opt/ora2pg/migration/ora2pg.conf << 'EOF'
ORACLE_HOME /usr/lib/oracle/19.0/client64
ORACLE_DSN  dbi:Oracle:host=oracle-prod.agency.gov;sid=FEDDB;port=1521
ORACLE_USER migration_reader
ORACLE_PWD  ***

# Assessment mode
EXPORT_SCHEMA 0
SCHEMA        APP_SCHEMA

# Output
OUTPUT_DIR    /opt/ora2pg/migration/output
DEBUG         0
EOF

# Run assessment report
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t SHOW_REPORT

# Generate detailed migration report
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t SHOW_REPORT --estimate_cost

2.4 ora2pg complexity scoring

ora2pg rates migration complexity from A (trivial) to C (complex):

Rating Description Typical effort per object Auto-conversion rate
A Simple objects, direct translation Minutes 90%+
B- Moderate complexity, some manual fixes Hours 70-90%
B+ Significant PL/SQL, Oracle-specific features Days 50-70%
C Complex packages, Oracle-specific features Weeks 30-50%

2.5 Schema conversion

# Convert all schema objects
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t TABLE -o tables.sql
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t VIEW -o views.sql
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t SEQUENCE -o sequences.sql
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t TRIGGER -o triggers.sql
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t FUNCTION -o functions.sql
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t PROCEDURE -o procedures.sql
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t PACKAGE -o packages.sql
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t TYPE -o types.sql
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t GRANT -o grants.sql

# Or convert everything at once
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t ALL -o full_schema.sql

3. PL/SQL to PL/pgSQL conversion patterns

PL/pgSQL is syntactically closer to PL/SQL than T-SQL, making PostgreSQL a natural migration target for PL/SQL-heavy codebases.

3.1 Function conversion

-- Oracle PL/SQL
CREATE OR REPLACE FUNCTION get_employee_salary(
    p_emp_id IN NUMBER
) RETURN NUMBER IS
    v_salary NUMBER(10,2);
BEGIN
    SELECT salary INTO v_salary
    FROM employees
    WHERE employee_id = p_emp_id;

    RETURN v_salary;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RETURN NULL;
    WHEN TOO_MANY_ROWS THEN
        RAISE_APPLICATION_ERROR(-20001, 'Multiple employees found');
END get_employee_salary;
/
-- PL/pgSQL (Azure PostgreSQL)
CREATE OR REPLACE FUNCTION get_employee_salary(
    p_emp_id integer
) RETURNS numeric AS $$
DECLARE
    v_salary numeric(10,2);
BEGIN
    SELECT salary INTO STRICT v_salary
    FROM employees
    WHERE employee_id = p_emp_id;

    RETURN v_salary;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RETURN NULL;
    WHEN TOO_MANY_ROWS THEN
        RAISE EXCEPTION 'Multiple employees found' USING ERRCODE = 'P0001';
END;
$$ LANGUAGE plpgsql;

Key differences:

PL/SQL PL/pgSQL Notes
RETURN type (function signature) RETURNS type Plural form
IS / AS AS $$ ... $$ LANGUAGE plpgsql Dollar-quoted body
NUMBER(p,s) numeric(p,s) Data type name
VARCHAR2(n) varchar(n) Data type name
IN / OUT parameter modes Same (IN/OUT/INOUT) Same concept
RAISE_APPLICATION_ERROR RAISE EXCEPTION Error handling
DBMS_OUTPUT.PUT_LINE RAISE NOTICE Debug output
SQL%ROWCOUNT GET DIAGNOSTICS row_count = ROW_COUNT or FOUND Row count after DML
EXECUTE IMMEDIATE EXECUTE Dynamic SQL

3.2 Package conversion

Oracle packages have no direct PostgreSQL equivalent. Convert to schemas with individual functions/procedures.

-- Oracle package spec
CREATE OR REPLACE PACKAGE hr_pkg AS
    c_max_salary CONSTANT NUMBER := 500000;

    FUNCTION validate_salary(p_salary NUMBER) RETURN BOOLEAN;
    PROCEDURE give_raise(p_emp_id NUMBER, p_pct NUMBER);
END hr_pkg;
/

CREATE OR REPLACE PACKAGE BODY hr_pkg AS
    FUNCTION validate_salary(p_salary NUMBER) RETURN BOOLEAN IS
    BEGIN
        RETURN p_salary > 0 AND p_salary <= c_max_salary;
    END;

    PROCEDURE give_raise(p_emp_id NUMBER, p_pct NUMBER) IS
        v_new_salary NUMBER;
    BEGIN
        SELECT salary * (1 + p_pct/100) INTO v_new_salary
        FROM employees WHERE employee_id = p_emp_id;

        IF NOT validate_salary(v_new_salary) THEN
            RAISE_APPLICATION_ERROR(-20002, 'Salary exceeds maximum');
        END IF;

        UPDATE employees SET salary = v_new_salary
        WHERE employee_id = p_emp_id;
    END;
END hr_pkg;
/
-- PL/pgSQL equivalent
CREATE SCHEMA IF NOT EXISTS hr_pkg;

-- Package constants become a configuration table or function
CREATE OR REPLACE FUNCTION hr_pkg.max_salary()
RETURNS numeric AS $$
BEGIN
    RETURN 500000;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

CREATE OR REPLACE FUNCTION hr_pkg.validate_salary(p_salary numeric)
RETURNS boolean AS $$
BEGIN
    RETURN p_salary > 0 AND p_salary <= hr_pkg.max_salary();
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE PROCEDURE hr_pkg.give_raise(
    p_emp_id integer,
    p_pct numeric
) AS $$
DECLARE
    v_new_salary numeric;
BEGIN
    SELECT salary * (1 + p_pct/100.0) INTO v_new_salary
    FROM employees WHERE employee_id = p_emp_id;

    IF NOT hr_pkg.validate_salary(v_new_salary) THEN
        RAISE EXCEPTION 'Salary exceeds maximum' USING ERRCODE = 'P0002';
    END IF;

    UPDATE employees SET salary = v_new_salary
    WHERE employee_id = p_emp_id;
END;
$$ LANGUAGE plpgsql;

3.3 Collection types

-- Oracle: Nested table type
CREATE OR REPLACE TYPE number_list AS TABLE OF NUMBER;
/

CREATE OR REPLACE FUNCTION sum_numbers(p_numbers number_list)
RETURN NUMBER IS
    v_sum NUMBER := 0;
BEGIN
    FOR i IN 1..p_numbers.COUNT LOOP
        v_sum := v_sum + p_numbers(i);
    END LOOP;
    RETURN v_sum;
END;
/
-- PL/pgSQL: Use arrays
CREATE OR REPLACE FUNCTION sum_numbers(p_numbers numeric[])
RETURNS numeric AS $$
DECLARE
    v_sum numeric := 0;
    v_num numeric;
BEGIN
    FOREACH v_num IN ARRAY p_numbers LOOP
        v_sum := v_sum + v_num;
    END LOOP;
    RETURN v_sum;
END;
$$ LANGUAGE plpgsql;

-- Or simpler with built-in aggregate:
-- SELECT sum(unnest) FROM unnest(p_numbers);

4. Extension ecosystem

PostgreSQL's extension ecosystem provides capabilities that Oracle charges separately for or does not offer.

Capability PostgreSQL extension Oracle equivalent Oracle cost
Spatial / GIS PostGIS Oracle Spatial $17,500/processor
Full-text search Built-in (tsvector) Oracle Text Included (EE)
Vector search / AI pgvector Oracle AI Vector Search (23ai) Included (23ai only)
Time-series TimescaleDB Manual partitioning Partitioning option ($11,500/proc)
Job scheduling pg_cron DBMS_SCHEDULER Included
Horizontal scale-out Citus RAC $23,000/processor
Columnar storage citus_columnar In-Memory option $23,000/processor
Graph Apache AGE Graph (23c+) Included (23c only)
Foreign data access postgres_fdw, oracle_fdw Database Links Included
Audit logging pgAudit Fine-Grained Auditing Included (EE)
Password management Custom pg_hba.conf + Entra ID Oracle profiles Included

4.1 Enabling extensions on Azure PostgreSQL

-- Enable extensions (Azure PostgreSQL Flexible Server)
-- Some extensions are pre-installed, others need allow-listing in Azure Portal

-- Check available extensions
SELECT * FROM pg_available_extensions ORDER BY name;

-- Enable PostGIS
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;

-- Enable pgvector for AI/RAG patterns
CREATE EXTENSION IF NOT EXISTS vector;

-- Enable pg_cron for scheduling
CREATE EXTENSION IF NOT EXISTS pg_cron;

-- Enable pgAudit for audit logging
-- (Must be enabled in server parameters first via Azure Portal)
CREATE EXTENSION IF NOT EXISTS pgaudit;

-- Enable Citus for distributed tables
-- (Available on Citus-enabled server configuration)
CREATE EXTENSION IF NOT EXISTS citus;

5. Citus for horizontal scaling

For Oracle RAC workloads that need horizontal scale-out, Citus on Azure PostgreSQL provides distributed table capabilities.

5.1 Citus architecture

                   ┌─────────────────┐
                   │   Coordinator   │
                   │   (Routes SQL)  │
                   └────────┬────────┘
            ┌───────────────┼───────────────┐
            │               │               │
    ┌───────┴──────┐ ┌──────┴───────┐ ┌─────┴──────┐
    │  Worker 1    │ │  Worker 2    │ │  Worker 3  │
    │  (Shards)    │ │  (Shards)    │ │  (Shards)  │
    └──────────────┘ └──────────────┘ └────────────┘

5.2 Converting Oracle RAC workloads to Citus

-- Distribute a high-volume table across workers
SELECT create_distributed_table('transactions', 'tenant_id');

-- Reference tables (small lookup tables replicated to all workers)
SELECT create_reference_table('transaction_types');
SELECT create_reference_table('currencies');

-- Queries run in parallel across all workers
SELECT tenant_id, COUNT(*), SUM(amount)
FROM transactions
WHERE transaction_date >= '2024-01-01'
GROUP BY tenant_id;
-- Citus automatically parallelizes and aggregates

6. Data migration with ora2pg

# Configure data migration in ora2pg.conf
# Add to existing config:
cat >> /opt/ora2pg/migration/ora2pg.conf << 'EOF'

# Data migration settings
PG_DSN     dbi:Pg:dbname=feddb;host=pg-flex.postgres.database.azure.com;port=5432
PG_USER    migration_admin
PG_PWD     ***

# Performance tuning
DATA_LIMIT     10000
JOBS           4
ORACLE_COPIES  4
PG_COPIES      4
DROP_FKEY      1
TRUNCATE_TABLE 1
EOF

# Migrate data (parallel)
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t COPY -j 4

# Or use INSERT mode for complex data types
ora2pg -c /opt/ora2pg/migration/ora2pg.conf -t INSERT -j 4

7. CSA-in-a-Box integration

7.1 ADF pipeline for PostgreSQL ingestion

Azure Database for PostgreSQL integrates with CSA-in-a-Box through Azure Data Factory pipelines.

{
    "name": "PostgreSQL_to_OneLake",
    "properties": {
        "activities": [
            {
                "name": "CopyPostgreSQLToLakehouse",
                "type": "Copy",
                "inputs": [
                    {
                        "referenceName": "AzurePostgreSQLSource",
                        "type": "DatasetReference"
                    }
                ],
                "outputs": [
                    {
                        "referenceName": "OneLakeDelta",
                        "type": "DatasetReference"
                    }
                ],
                "typeProperties": {
                    "source": {
                        "type": "AzurePostgreSqlSource",
                        "query": "SELECT * FROM app_schema.employees WHERE updated_at >= '@{pipeline().parameters.watermark}'"
                    },
                    "sink": {
                        "type": "LakehouseTableSink",
                        "tableActionOption": "Append"
                    }
                }
            }
        ]
    }
}

7.2 dbt source configuration

# domains/shared/dbt/models/sources.yml
sources:
    - name: oracle_migrated_pg
      description: "Oracle workloads migrated to Azure PostgreSQL"
      database: feddb
      schema: app_schema
      tables:
          - name: employees
            description: "Employee records (migrated from Oracle HR)"
            columns:
                - name: employee_id
                  tests: [not_null, unique]
          - name: departments
            description: "Department hierarchy (migrated from Oracle HR)"
          - name: transactions
            description: "Financial transactions (migrated from Oracle EBS)"

8. Post-migration validation

-- PostgreSQL: Validate row counts against Oracle source
SELECT schemaname, relname AS table_name,
       n_live_tup AS approximate_row_count
FROM pg_stat_user_tables
WHERE schemaname = 'app_schema'
ORDER BY relname;

-- Validate data types were correctly mapped
SELECT column_name, data_type, character_maximum_length,
       numeric_precision, numeric_scale
FROM information_schema.columns
WHERE table_schema = 'app_schema' AND table_name = 'employees'
ORDER BY ordinal_position;

-- Performance baseline
SELECT query, calls, mean_exec_time, total_exec_time,
       rows, shared_blks_hit, shared_blks_read
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

Maintainers: csa-inabox core team Last updated: 2026-04-30