Skip to content

Tutorial: Oracle to PostgreSQL with ora2pg

Step-by-step walkthrough: install ora2pg, analyze an Oracle schema, convert to PostgreSQL, deploy to Azure Database for PostgreSQL Flexible Server, and validate the migration.


!!! info "Prerequisites" - Oracle Database 11g or later with read access - Azure Database for PostgreSQL Flexible Server provisioned - Linux workstation or VM (Ubuntu 20.04+ recommended) for ora2pg - Oracle Instant Client installed on the migration workstation - Perl DBI and DBD::Oracle modules - Network connectivity to both Oracle source and Azure PostgreSQL target

**Time estimate:** 3-4 hours for a small database (< 100 tables, < 10 GB)

Step 1: Set up the migration environment

1.1 Install Oracle Instant Client

# Download Oracle Instant Client for Linux
# https://www.oracle.com/database/technologies/instant-client/linux-x86-64-downloads.html

# Install basic + SDK packages
sudo mkdir -p /opt/oracle
cd /opt/oracle
unzip instantclient-basic-linux.x64-19.22.0.0.0dbru.zip
unzip instantclient-sdk-linux.x64-19.22.0.0.0dbru.zip

# Configure environment
echo 'export ORACLE_HOME=/opt/oracle/instantclient_19_22' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=$ORACLE_HOME:$LD_LIBRARY_PATH' >> ~/.bashrc
echo 'export PATH=$ORACLE_HOME:$PATH' >> ~/.bashrc
source ~/.bashrc

# Verify
sqlplus -V

1.2 Install Perl Oracle driver

# Install Perl and required modules
sudo apt-get update
sudo apt-get install -y perl libdbi-perl cpanminus libaio1

# Install DBD::Oracle
sudo cpanm DBD::Oracle

# Install DBD::Pg (for direct PostgreSQL connection)
sudo apt-get install -y libpq-dev
sudo cpanm DBD::Pg

# Verify
perl -e 'use DBD::Oracle; print "Oracle driver OK\n";'
perl -e 'use DBD::Pg; print "PostgreSQL driver OK\n";'

1.3 Install ora2pg

# Install from package manager
sudo apt-get install -y ora2pg

# Or install latest from source
git clone https://github.com/darold/ora2pg.git
cd ora2pg
perl Makefile.PL
make
sudo make install

# Verify
ora2pg --version
# Expected: Ora2Pg v24.x

Step 2: Configure ora2pg

2.1 Create project directory

mkdir -p /opt/migration/feddb
cd /opt/migration/feddb

# Initialize ora2pg project
ora2pg --project_base /opt/migration --init_project feddb

# This creates:
# /opt/migration/feddb/
# ├── config/
# │   └── ora2pg.conf
# ├── data/
# ├── reports/
# ├── schema/
# │   ├── tables/
# │   ├── views/
# │   ├── sequences/
# │   ├── triggers/
# │   ├── functions/
# │   ├── procedures/
# │   ├── packages/
# │   ├── types/
# │   └── grants/
# └── sources/

2.2 Edit configuration

cat > /opt/migration/feddb/config/ora2pg.conf << 'CONF'
#---------------------------------------------------------------------
# Oracle source connection
#---------------------------------------------------------------------
ORACLE_HOME     /opt/oracle/instantclient_19_22
ORACLE_DSN      dbi:Oracle:host=oracle-prod.agency.gov;sid=FEDDB;port=1521
ORACLE_USER     migration_reader
ORACLE_PWD      ***

#---------------------------------------------------------------------
# Schema to migrate
#---------------------------------------------------------------------
SCHEMA          APP_SCHEMA
EXPORT_SCHEMA   1

#---------------------------------------------------------------------
# PostgreSQL target connection
#---------------------------------------------------------------------
PG_DSN          dbi:Pg:dbname=feddb;host=pg-flex.postgres.database.azure.com;port=5432;sslmode=require
PG_USER         migration_admin
PG_PWD          ***

#---------------------------------------------------------------------
# Conversion options
#---------------------------------------------------------------------
# Map Oracle schema to PostgreSQL schema
PG_SCHEMA       app_schema

# Data type mapping overrides
DATA_TYPE       DATE:timestamp,LONG:text,LONG RAW:bytea,CLOB:text,NCLOB:text,BLOB:bytea,BFILE:bytea

# Convert Oracle NUMBER to appropriate PostgreSQL types
DEFAULT_NUMERIC numeric
PG_INTEGER_TYPE 1
PG_NUMERIC_TYPE numeric

# PL/SQL conversion
PLSQL_PGSQL     1
NULL_EQUAL_EMPTY 1

#---------------------------------------------------------------------
# Performance options
#---------------------------------------------------------------------
DATA_LIMIT      10000
JOBS            4
ORACLE_COPIES   4
PG_COPIES       4
LONGREADLEN     1048576

#---------------------------------------------------------------------
# Output
#---------------------------------------------------------------------
OUTPUT_DIR      /opt/migration/feddb/schema
FILE_PER_TABLE  1
FILE_PER_FUNCTION 1
CONF

Step 3: Run assessment report

3.1 Generate migration report

cd /opt/migration/feddb

# Generate assessment report
ora2pg -c config/ora2pg.conf -t SHOW_REPORT --estimate_cost \
    > reports/migration_assessment.txt

# View the report
cat reports/migration_assessment.txt

3.2 Interpreting the assessment

The report shows each object type with a migration cost estimate:

-----------------------------------------------
Migration level: B-5
-----------------------------------------------
Migration level: B-5 (moderate complexity)

Object type               Count  Cost   Details
---------------------------------------------------------
TABLE                     45     1.0    All tables convertible
VIEW                      12     1.5    3 views with Oracle-specific syntax
SEQUENCE                  15     0.5    Direct conversion
INDEX                     78     1.0    All standard indexes
CONSTRAINT                120    0.5    All constraints convertible
TRIGGER                   8      3.0    BEFORE triggers need conversion
FUNCTION                  25     2.0    Some DECODE/NVL patterns
PROCEDURE                 35     3.0    Complex PL/SQL in 8 procedures
PACKAGE                   5      5.0    Decompose to schemas + functions
TYPE                      3      2.0    Collection types need arrays
GRANT                     45     0.5    Role mapping needed
---------------------------------------------------------
Total estimated cost: 20.5 person-days
Migration level: B-5 (moderate, 15-30 days)

3.3 Complexity ratings

Level Description Typical effort
A-1 to A-3 Trivial to easy 1-5 days
B-4 to B-6 Moderate 5-30 days
B-7 to B-9 Difficult 30-60 days
C-10+ Very complex 60+ days

Step 4: Convert schema objects

4.1 Convert each object type

cd /opt/migration/feddb

# Convert in recommended order
ora2pg -c config/ora2pg.conf -t SEQUENCE -o schema/sequences/sequences.sql
ora2pg -c config/ora2pg.conf -t TABLE -o schema/tables/tables.sql
ora2pg -c config/ora2pg.conf -t VIEW -o schema/views/views.sql
ora2pg -c config/ora2pg.conf -t FUNCTION -o schema/functions/functions.sql
ora2pg -c config/ora2pg.conf -t PROCEDURE -o schema/procedures/procedures.sql
ora2pg -c config/ora2pg.conf -t PACKAGE -o schema/packages/packages.sql
ora2pg -c config/ora2pg.conf -t TRIGGER -o schema/triggers/triggers.sql
ora2pg -c config/ora2pg.conf -t TYPE -o schema/types/types.sql
ora2pg -c config/ora2pg.conf -t GRANT -o schema/grants/grants.sql

echo "Schema conversion complete. Review output files."

4.2 Review converted SQL

# Check for conversion warnings
grep -rn "TODO" schema/
grep -rn "FIXME" schema/
grep -rn "ora2pg" schema/  # ora2pg leaves comments for manual fixes

# Review a converted procedure
cat schema/procedures/procedures.sql | head -100

4.3 Manual fixes for common issues

-- Fix 1: Oracle SYSDATE -> PostgreSQL now()
-- ora2pg usually handles this, but verify:
-- Before: WHERE created_date > SYSDATE - 30
-- After:  WHERE created_date > now() - interval '30 days'

-- Fix 2: CONNECT BY -> recursive CTE
-- ora2pg may leave a TODO comment for complex hierarchical queries
-- See schema-migration.md for conversion patterns

-- Fix 3: Package global variables
-- Oracle packages with state variables need redesign
-- Option A: Use a configuration table
-- Option B: Use session variables (SET/SHOW)
-- Option C: Use function parameters instead of global state

-- Fix 4: AUTONOMOUS_TRANSACTION
-- See schema-migration.md for dblink pattern

Step 5: Create target database on Azure PostgreSQL

5.1 Provision Flexible Server (if not already done)

# Create resource group
az group create --name rg-oracle-migration --location eastus

# Create PostgreSQL Flexible Server
az postgres flexible-server create \
    --resource-group rg-oracle-migration \
    --name pg-flex-feddb \
    --location eastus \
    --admin-user migration_admin \
    --admin-password "***" \
    --sku-name Standard_D4ds_v5 \
    --tier GeneralPurpose \
    --storage-size 256 \
    --version 16 \
    --high-availability ZoneRedundant \
    --public-access None

# Configure private endpoint (if using VNet)
az network private-endpoint create \
    --resource-group rg-oracle-migration \
    --name pe-pg-flex-feddb \
    --vnet-name vnet-prod \
    --subnet snet-data \
    --private-connection-resource-id $(az postgres flexible-server show \
        --resource-group rg-oracle-migration --name pg-flex-feddb --query id -o tsv) \
    --group-id postgresqlServer \
    --connection-name pg-connection

# Enable required extensions
az postgres flexible-server parameter set \
    --resource-group rg-oracle-migration \
    --server-name pg-flex-feddb \
    --name azure.extensions \
    --value "pgcrypto,pg_cron,pgaudit,postgis,uuid-ossp"

5.2 Create database and schema

# Connect to PostgreSQL
psql "host=pg-flex-feddb.postgres.database.azure.com \
      dbname=postgres \
      user=migration_admin \
      sslmode=require"

# Create target database
CREATE DATABASE feddb WITH ENCODING 'UTF8' LC_COLLATE 'en_US.UTF-8';

# Connect to new database
\c feddb

# Create schema
CREATE SCHEMA IF NOT EXISTS app_schema;

Step 6: Deploy schema to Azure PostgreSQL

6.1 Deploy in order

# Deploy schema objects in dependency order
psql "host=pg-flex-feddb.postgres.database.azure.com dbname=feddb user=migration_admin sslmode=require" \
    -f schema/types/types.sql

psql "host=pg-flex-feddb.postgres.database.azure.com dbname=feddb user=migration_admin sslmode=require" \
    -f schema/sequences/sequences.sql

psql "host=pg-flex-feddb.postgres.database.azure.com dbname=feddb user=migration_admin sslmode=require" \
    -f schema/tables/tables.sql

psql "host=pg-flex-feddb.postgres.database.azure.com dbname=feddb user=migration_admin sslmode=require" \
    -f schema/views/views.sql

psql "host=pg-flex-feddb.postgres.database.azure.com dbname=feddb user=migration_admin sslmode=require" \
    -f schema/functions/functions.sql

psql "host=pg-flex-feddb.postgres.database.azure.com dbname=feddb user=migration_admin sslmode=require" \
    -f schema/procedures/procedures.sql

psql "host=pg-flex-feddb.postgres.database.azure.com dbname=feddb user=migration_admin sslmode=require" \
    -f schema/packages/packages.sql

psql "host=pg-flex-feddb.postgres.database.azure.com dbname=feddb user=migration_admin sslmode=require" \
    -f schema/triggers/triggers.sql

6.2 Verify schema deployment

-- Check object counts
SELECT
    CASE c.relkind
        WHEN 'r' THEN 'TABLE'
        WHEN 'v' THEN 'VIEW'
        WHEN 'S' THEN 'SEQUENCE'
        WHEN 'i' THEN 'INDEX'
    END AS object_type,
    COUNT(*) AS count
FROM pg_class c
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE n.nspname = 'app_schema'
  AND c.relkind IN ('r', 'v', 'S', 'i')
GROUP BY c.relkind
ORDER BY object_type;

-- Check functions and procedures
SELECT routine_name, routine_type
FROM information_schema.routines
WHERE routine_schema = 'app_schema'
ORDER BY routine_type, routine_name;

-- Check triggers
SELECT trigger_name, event_object_table, action_timing, event_manipulation
FROM information_schema.triggers
WHERE trigger_schema = 'app_schema'
ORDER BY event_object_table, trigger_name;

Step 7: Migrate data

7.1 Migrate data with ora2pg

# Migrate data using COPY mode (fastest)
ora2pg -c config/ora2pg.conf -t COPY -j 4 -o data/data_load.sql

# Monitor progress
# ora2pg shows per-table progress:
# [1/45] Exporting table EMPLOYEES (25,000 rows)...
# [2/45] Exporting table DEPARTMENTS (50 rows)...
# ...

# For very large tables, use direct PostgreSQL COPY
ora2pg -c config/ora2pg.conf -t COPY -j 4 --pg_dsn "dbi:Pg:dbname=feddb;host=pg-flex-feddb.postgres.database.azure.com;port=5432;sslmode=require"

7.2 Alternative: CSV export + COPY import

# For tables too large for ora2pg direct migration
# Step 1: Export from Oracle to CSV
sqlplus -S migration_reader/***@oracle-prod.agency.gov:1521/FEDDB << 'EOF'
SET COLSEP ','
SET PAGESIZE 0
SET LINESIZE 32767
SET FEEDBACK OFF
SET HEADING ON
SET TRIMSPOOL ON
SPOOL /opt/migration/feddb/data/transactions.csv
SELECT employee_id, department_id, salary, hire_date, name
FROM APP_SCHEMA.EMPLOYEES;
SPOOL OFF
EXIT;
EOF

# Step 2: Upload to Azure (if remote)
azcopy copy '/opt/migration/feddb/data/transactions.csv' \
    'https://stmigration.blob.core.windows.net/data/'

# Step 3: Import to PostgreSQL
psql "host=pg-flex-feddb.postgres.database.azure.com dbname=feddb user=migration_admin sslmode=require" \
    -c "\COPY app_schema.employees(employee_id, department_id, salary, hire_date, name) FROM '/opt/migration/feddb/data/transactions.csv' WITH CSV HEADER"

Step 8: Validate the migration

8.1 Row count comparison

# Create validation script
cat > /opt/migration/feddb/validate_counts.sh << 'SCRIPT'
#!/bin/bash
echo "=== Row Count Validation ==="
echo ""

TABLES="employees departments transactions audit_log documents"

for TABLE in $TABLES; do
    ORA_COUNT=$(sqlplus -S migration_reader/***@oracle-prod:1521/FEDDB << EOF
SET PAGESIZE 0 FEEDBACK OFF
SELECT COUNT(*) FROM APP_SCHEMA.$TABLE;
EXIT;
EOF
)
    PG_COUNT=$(psql -t -A \
        "host=pg-flex-feddb.postgres.database.azure.com dbname=feddb user=migration_admin sslmode=require" \
        -c "SELECT COUNT(*) FROM app_schema.$TABLE;")

    if [ "$ORA_COUNT" = "$PG_COUNT" ]; then
        echo "PASS: $TABLE - Oracle: $ORA_COUNT, PostgreSQL: $PG_COUNT"
    else
        echo "FAIL: $TABLE - Oracle: $ORA_COUNT, PostgreSQL: $PG_COUNT"
    fi
done
SCRIPT

chmod +x /opt/migration/feddb/validate_counts.sh
./validate_counts.sh

8.2 Data integrity checks

-- PostgreSQL: Verify data types
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;

-- Verify foreign key integrity
SELECT tc.table_name, tc.constraint_name,
       kcu.column_name, ccu.table_name AS foreign_table
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
    ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
    ON tc.constraint_name = ccu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
  AND tc.table_schema = 'app_schema';

8.3 Functional testing

-- Test converted functions
SELECT app_schema.get_employee_salary(1001);

-- Test converted procedures
CALL app_schema.update_salary(1001, 85000.00);

-- Verify trigger behavior
INSERT INTO app_schema.employees (name, department_id, salary)
VALUES ('Test User', 10, 50000);
-- Check that audit trigger fired
SELECT * FROM app_schema.audit_log ORDER BY log_time DESC LIMIT 1;

Step 9: Configure ADF pipeline for CSA-in-a-Box

# Create ADF linked service for PostgreSQL
# In Azure Portal: ADF > Manage > Linked Services > New
# Select "Azure Database for PostgreSQL"
# Configure connection to pg-flex-feddb

# Create pipeline to copy data to OneLake
# ADF > Author > New Pipeline > "PG_to_OneLake_Incremental"
# Add Copy Activity:
#   Source: Azure PostgreSQL (query with watermark)
#   Sink: Lakehouse Table (OneLake Delta)
# Schedule: Every 15 minutes for near-real-time

Troubleshooting

Issue Cause Resolution
DBD::Oracle install failed Missing Oracle client libraries Ensure ORACLE_HOME and LD_LIBRARY_PATH are set
ORA-12154: TNS could not resolve Oracle connectivity issue Verify tnsnames.ora or host:port/sid syntax
encoding mismatch Oracle charset vs PostgreSQL UTF-8 Set NLS_LANG=AMERICAN_AMERICA.AL32UTF8 in environment
function body syntax error PL/SQL not fully converted Review ora2pg TODO comments, apply manual fixes
COPY failed on large table Memory or network timeout Reduce DATA_LIMIT, increase LONGREADLEN
permission denied on extension Extension not allow-listed Enable in Azure Portal > Server Parameters > azure.extensions

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