Migrating Oracle Databases to Oracle Database@Azure: Strategy, Architecture, and Best Practices

Migrating Oracle Databases to Oracle Database@Azure: Strategy, Architecture, and Best Practices


A practical, code-first walkthrough of five migration methods — RMAN, Data Pump, GoldenGate, Zero Downtime Migration, and Data Guard.


📅July 25, 2026 🏷️ Migration, RMAN, Data Pump, GoldenGate, ZDM, Data Guard⏱ 30 min read


Article Overview

This is the hands-on companion to the strategy discussions. It assumes you have already decided a database belongs on Oracle Database@Azure and now have to actually move it. Five methods are covered with working commands: RMAN restore for full-database moves, Data Pump for schema-level and cross-version moves, GoldenGate for near-zero-downtime replication, Zero Downtime Migration for automated orchestration, and Data Guard for a rehearsed physical cutover. Each section gives you the real commands, the parameter files, and the gotchas — not a description of what the tool does. A short decision section up front helps you pick; everything after it is execution.

⚠️ Read before you copy-paste

Every command below is illustrative. Names, paths, OCIDs, connect strings, service names, and sizes are placeholders — swap in your own. Test the whole flow on a non-production copy first, and check current tool versions and syntax against Oracle documentation, because option names change between releases. Nothing here should touch a production system on its first run.

1.Pick a Method in 60 Seconds

Five tools, and the choice is mostly settled by three variables: how much downtime you can tolerate, whether the source and target Oracle versions match, and how big the database is.

MethodDowntimeCross-versionBest fitAutomation
RMANHours (offline restore)Same version onlyFull DB, same release, maintenance window availableScripted, manual
Data PumpHours to minutesYes — 11g up to currentSchema subsets, cross-version, cross-charset, reorg during moveScripted, manual
GoldenGateNear zero (seconds)Yes — wide rangeMinimal-downtime, cross-version, phased or bidirectionalConfig-driven
ZDMNear zero (online) or a window (offline)Same release for physical; wider for logicalThe default — automates RMAN/Data Guard/GoldenGate under one commandFully automated
Data GuardMinutes (switchover)Same version onlySame-release move you want rehearsed and reversibleScripted, manual

Rule of thumb: reach for ZDM first. Use RMAN or Data Guard when you want manual control over a same-version move, Data Pump when versions differ or you only need part of the database, and GoldenGate when the downtime budget is measured in seconds.

2.Prerequisites and Target Prep

Everything in this article assumes the same starting line. Get these done once and every method below becomes easier.

2.1  Network path

The source database (on-premises or on VMs) needs a private path to the VM cluster running inside your Azure delegated subnet. That usually means ExpressRoute or a site-to-site VPN into the VNet, with routing to the delegated subnet and security rules permitting the listener port (1521, or 2484 for TCPS). Confirm reachability before anything else.

source $ verify-network.sh
# From the source host, prove you can reach the target SCAN listener
nc -zv erpdb-scan.example.oraclevcn.com 1521

# Resolve the target private DNS name (must resolve to the delegated-subnet IPs)
nslookup erpdb-scan.example.oraclevcn.com

# End-to-end TNS check once tnsnames is in place
tnsping ERPPROD_AZURE

2.2  The target database

On the Exadata VM cluster, create the container and pluggable database that will receive the data — through the OCI console, OCI CLI, or the database tooling. For physical methods (RMAN, Data Guard, ZDM physical) the target release must match the source exactly. Capture the essentials the moment it exists.

target $ capture-target-facts.sql
-- Run on the target CDB after provisioning
SELECT name, cdb, database_role, open_mode FROM v$database;
SELECT version_full FROM v$instance;              -- must match source for physical moves
SELECT name FROM v$pdbs;
SELECT property_value FROM database_properties
 WHERE property_name = 'NLS_CHARACTERSET';         -- watch this for logical moves
ARCHIVE LOG LIST;                                   -- archivelog mode is required for online methods

2.3  A pre-flight checklist worth running

CheckWhy it matters
Source and target character setsA mismatch forces conversion and can expand data; it blocks a clean logical import if incompatible
Source and target versions (physical)Physical methods demand identical releases; a patch-level gap will stop the job
EndiannessExadata is little-endian; a big-endian source (older Solaris/AIX) rules out simple RMAN restore
Database size and network throughputDetermines whether an offline copy fits your window, or you need an online method
Invalid objects and unsupported typesRun a compatibility pass before, not during, the migration
TDE / encryption on the targetOracle Database@Azure targets typically require encrypted tablespaces — plan keys early
Time sync and DNSData Guard and GoldenGate are unforgiving about clock skew and name resolution

✅ Set a common environment block

Save yourself repetition — the examples below assume variables like these are already exported on each host.

both $ env.sh
# Source side
export SRC_ORACLE_SID=ERPPROD
export SRC_TNS=ERPPROD
# Target side (Oracle Database@Azure VM cluster)
export TGT_TNS=ERPPROD_AZURE
export TGT_PDB=PDB1
export TGT_SCAN=erpdb-scan.example.oraclevcn.com
# Shared working area for dumps/backups reachable by both
export STAGE=/u02/migstage

3.Method 1 — RMAN

Best when the source and target are the same Oracle release, the database is a candidate to move whole, and you have a maintenance window. Two variants are shown: a backup-based restore (simple, needs staging space) and an active duplicate over the network (no intermediate backup).

3.1  Variant A — backup, ship, restore

STEP 1 Take a full backup on the source, including archivelogs and the controlfile.

source $ rman_backup.rman
rman target /

RUN {
  ALLOCATE CHANNEL c1 DEVICE TYPE DISK FORMAT '/u02/migstage/%U';
  ALLOCATE CHANNEL c2 DEVICE TYPE DISK FORMAT '/u02/migstage/%U';
  BACKUP AS COMPRESSED BACKUPSET DATABASE
         PLUS ARCHIVELOG;
  BACKUP CURRENT CONTROLFILE FORMAT '/u02/migstage/ctl_%U';
  BACKUP SPFILE FORMAT '/u02/migstage/spfile_%U';
}
-- Note the last SCN so you know how current the restore is
LIST BACKUP SUMMARY;

STEP 2 Move the backup pieces to a location the target can read — over the private link, or via an object-store bucket the VM cluster can reach. Then restore on the target.

target $ rman_restore.rman
# On the target VM-cluster node, with the SID set to the new instance
rman target /

STARTUP NOMOUNT PFILE='/u02/migstage/initERPPROD.ora';

RESTORE CONTROLFILE FROM '/u02/migstage/ctl_...';
ALTER DATABASE MOUNT;

# Point RMAN at the staged pieces
CATALOG START WITH '/u02/migstage/' NOPROMPT;

RUN {
  SET UNTIL SEQUENCE 45210 THREAD 1;   # one past the last archived log you shipped
  RESTORE DATABASE;
  RECOVER DATABASE;
}

ALTER DATABASE OPEN RESETLOGS;

3.2  Variant B — active duplicate over the network

No staging area. RMAN copies directly from a running source (or its standby) to the target using DUPLICATE ... FROM ACTIVE DATABASE. Requires TNS entries both ways and a password file.

target $ rman_duplicate.rman
# AUXILIARY = the empty target instance in NOMOUNT; TARGET = the live source
rman TARGET sys/****@ERPPROD AUXILIARY sys/****@ERPPROD_AZURE

DUPLICATE TARGET DATABASE TO ERPPROD
  FROM ACTIVE DATABASE
  USING COMPRESSED BACKUPSET
  SECTION SIZE 4G
  NOFILENAMECHECK
  SPFILE
    SET db_create_file_dest '+DATA'
    SET db_recovery_file_dest '+RECO'
    SET cluster_database 'FALSE'
  ;
-- Re-enable RAC parameters afterwards if the target is a cluster database

⚠️ RMAN gotchas on Oracle Database@Azure targets

Targets generally expect encrypted tablespaces, so restoring an unencrypted source can require configuring TDE and encrypting on the way in — test this early. Use SECTION SIZE on large datafiles so multiple channels parallelise a single big file. And confirm the target's ASM disk groups (+DATA, +RECO) and db_create_file_dest rather than assuming the source paths exist.

When RMAN is the right call: same version, whole database, window available, and you want a well-understood, scriptable path. When it is not: different versions, big-endian source, or a downtime budget too small for an offline restore — use Data Pump, GoldenGate, or ZDM instead.

4.Method 2 — Data Pump

The workhorse for cross-version moves, partial migrations, character-set changes, and reorganising as you go. It is logical, so it rebuilds objects rather than copying blocks — slower for very large databases, but far more flexible. Two approaches: dump-file based, and a direct network import with no files at all.

4.1  Network import — no dump files

The cleanest option for a migration. A database link from the target pulls straight from the source; nothing lands on disk in between.

STEP 1 On the target, create a link back to the source and a directory object.

target $ setup_link.sql
CREATE PUBLIC DATABASE LINK src_link
  CONNECT TO system IDENTIFIED BY "****"
  USING 'ERPPROD';                       -- TNS alias for the source

-- Verify the link works before the import
SELECT banner FROM v$version@src_link WHERE ROWNUM = 1;

STEP 2 Run the import directly over the link, remapping tablespaces and enabling parallelism.

target $ impdp_network.sh
impdp system@ERPPROD_AZURE \
  network_link=src_link \
  schemas=SALES,FINANCE,HR \
  remap_tablespace=USERS:DATA,TOOLS:DATA \
  parallel=8 \
  exclude=STATISTICS \
  transform=OID:n \
  logfile=DATA_PUMP_DIR:imp_network.log \
  metrics=y \
  flashback_time=SYSTIMESTAMP           # read-consistent snapshot of the source

4.2  Dump-file based — when the link is not an option

Export on the source, move the dumps, import on the target. Use when there is no direct database link or you want an artifact you can retain.

source $ expdp.sh
expdp system@ERPPROD \
  schemas=SALES,FINANCE,HR \
  directory=DATA_PUMP_DIR \
  dumpfile=erp_%U.dmp \
  filesize=8G \
  parallel=8 \
  compression=ALL \
  flashback_time=SYSTIMESTAMP \
  logfile=exp_erp.log
target $ impdp.sh
# After copying erp_*.dmp into the target DATA_PUMP_DIR
impdp system@ERPPROD_AZURE \
  schemas=SALES,FINANCE,HR \
  directory=DATA_PUMP_DIR \
  dumpfile=erp_%U.dmp \
  remap_tablespace=USERS:DATA,TOOLS:DATA \
  parallel=8 \
  exclude=STATISTICS \
  logfile=imp_erp.log \
  metrics=y

4.3  Handy Data Pump patterns

You want to…Add this
Estimate size without exportingestimate_only=y on expdp
Move only structure, no rowscontent=metadata_only
Move only data into an existing schemacontent=data_only with table_exists_action=append
Skip and log rows that faildata_options=skip_constraint_errors
Regather stats fresh on the targetexclude=STATISTICS, then DBMS_STATS after
Rename a schema during importremap_schema=OLDNAME:NEWNAME

✅ Why exclude=STATISTICS is almost always right

Importing optimizer statistics is slow and frequently imports stats that no longer match the target's storage and layout. Exclude them during the load and regather with DBMS_STATS.GATHER_DATABASE_STATS afterwards — the load runs faster and the plans come out better.

5.Method 3 — GoldenGate

When downtime has to be measured in seconds, GoldenGate is the answer. It captures changes from the source redo, ships them to the target, and applies them continuously — so you can do the bulk load in advance, keep the target in sync while production keeps running, and cut over only when you are ready. It also crosses versions, which RMAN and Data Guard cannot.

The shape of a GoldenGate migration is always the same:


.

STEP 1 Prepare the source: enable supplemental logging and force logging, and create a GoldenGate admin user.

source $ gg_source_prep.sql
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA;
ALTER DATABASE FORCE LOGGING;
ALTER SYSTEM SWITCH LOGFILE;

-- Dedicated GoldenGate user with the right privileges
CREATE USER ggadmin IDENTIFIED BY "****" DEFAULT TABLESPACE users;
GRANT CONNECT, RESOURCE TO ggadmin;
EXEC DBMS_GOLDENGATE_AUTH.GRANT_ADMIN_PRIVILEGE('GGADMIN');
ALTER SYSTEM SET ENABLE_GOLDENGATE_REPLICATION=TRUE SCOPE=BOTH;

STEP 2 Configure the Extract on the source. Register it, then add and start it.

source $ ggsci — extract
-- Login and register the integrated Extract
DBLOGIN USERIDALIAS ggsource
REGISTER EXTRACT ext_erp DATABASE

-- Parameter file: ext_erp.prm
EXTRACT ext_erp
USERIDALIAS ggsource
EXTTRAIL ./dirdat/et
TABLE SALES.*;
TABLE FINANCE.*;
TABLE HR.*;

-- Create and start
ADD EXTRACT ext_erp INTEGRATED TRANLOG BEGIN NOW
ADD EXTTRAIL ./dirdat/et EXTRACT ext_erp
START EXTRACT ext_erp

STEP 3 Do the initial load with Data Pump using a flashback_scn that matches where Extract began — this is the join that guarantees no lost or duplicated rows.

source $ initial_load.sh
# Get the current SCN, start Extract at/after it, then export AS OF that SCN
SCN=$(sqlplus -s system/****@ERPPROD <<'EOF'
set head off feedback off
SELECT current_scn FROM v$database;
EOF
)
expdp system@ERPPROD schemas=SALES,FINANCE,HR \
  directory=DATA_PUMP_DIR dumpfile=load_%U.dmp parallel=8 \
  flashback_scn=$SCN logfile=load.log

STEP 4 On the target, apply the load, then configure Replicat to start from the same SCN and catch up.

target $ ggsci — replicat
-- After impdp of the load dumps completes on the target:
DBLOGIN USERIDALIAS ggtarget

-- Parameter file: rep_erp.prm
REPLICAT rep_erp
USERIDALIAS ggtarget
MAP SALES.*,   TARGET SALES.*;
MAP FINANCE.*, TARGET FINANCE.*;
MAP HR.*,      TARGET HR.*;

-- Start applying from just after the load's SCN
ADD REPLICAT rep_erp INTEGRATED EXTTRAIL ./dirdat/rt
START REPLICAT rep_erp AFTERCSN <load_scn>

STEP 5 Watch lag until it reaches zero, then cut over. This is the low-downtime moment: quiesce the app, confirm zero lag, switch connections.

both $ ggsci — monitor & cut over
-- On both sides, confirm health and lag
INFO ALL
LAG EXTRACT ext_erp
LAG REPLICAT rep_erp
STATS REPLICAT rep_erp LATEST

-- When lag is 0 and the app is quiesced:
--   1. stop application writes on the source
--   2. confirm Replicat has applied the final transactions (lag = 0)
--   3. repoint the application to ERPPROD_AZURE
--   4. stop Replicat and Extract
STOP REPLICAT rep_erp
STOP EXTRACT ext_erp

The SCN handshake is the whole trick

The single thing that makes GoldenGate migrations reliable is aligning three numbers: the SCN where Extract starts capturing, the flashback_scn of the initial export, and the AFTERCSN where Replicat begins applying. Get those consistent and every change is accounted for exactly once. Get them wrong and you silently lose or duplicate data — so verify row counts after catch-up, before cutover.

6.Method 4 — Zero Downtime Migration

ZDM is the method to reach for first, because it automates the others. Under the hood it uses RMAN, Data Guard, Data Pump, database links, and GoldenGate depending on the workflow you pick — but you drive the whole thing with a single command and a response file. Oracle positions it as the Maximum Availability Architecture-recommended path to Oracle Cloud, and Oracle Database@Azure (ExaDB-D) is a supported target.

ZDM 21.5 supports source databases on 11.2.0.4, 12.1.0.2, 12.2.0.1, 18c, 19c, 21c, and the 26ai generation. The physical workflow needs source and target on the same release and a Linux source; the logical workflow is more forgiving on versions. Sources can be single-instance, RAC One Node, or full RAC.

6.1  Two workflows, one tool

WorkflowHow it moves dataDowntimeUse when
Physical onlineBuilds a standby (Data Guard / restore-from-service), syncs, then switches overNear zeroSame release, want minimal downtime and a clean role swap
Physical offlineRMAN backup to a location, restore on targetA windowSame release, a maintenance window is acceptable
Logical onlineData Pump plus GoldenGate for change captureNear zeroCross-version or cross-platform with minimal downtime
Logical offlineData Pump, optionally over a database linkA windowCross-version where a window is fine

6.2  Physical online migration, end to end

The physical online direct-transfer workflow follows a fixed sequence: configure ZDM, start the migration, restore-from-service onto the target, instantiate a standby, sync primary and standby, switch over and swap roles, run post-migration validation, and finalise. Here is how you actually drive it.

STEP 1 Install ZDM on a dedicated host that can reach both source and target over SSH, and start the service.

zdmhost $ install_zdm.sh
# As the zdmuser, after unzipping the ZDM software
cd /home/zdmuser/zdm21.5
./zdminstall.sh setup \
  oraclehome=/home/zdmuser/zdm21.5/zdmhome \
  oraclebase=/home/zdmuser/zdmbase \
  ziploc=/home/zdmuser/zdm21.5/zdm_home.zip

# Start the ZDM service and confirm it is up
$ZDM_HOME/bin/zdmservice start
$ZDM_HOME/bin/zdmservice status

STEP 2 Set up passwordless SSH from the ZDM host to the source and to the target VM-cluster node — ZDM orchestrates both over SSH.

zdmhost $ ssh_setup.sh
ssh-keygen -t rsa -b 4096 -f ~/.ssh/zdm_key -N ""
ssh-copy-id -i ~/.ssh/zdm_key.pub oracle@source-db-host
ssh-copy-id -i ~/.ssh/zdm_key.pub opc@erpdb-node1.example.oraclevcn.com

# Prove both hops work non-interactively
ssh -i ~/.ssh/zdm_key oracle@source-db-host 'hostname'
ssh -i ~/.ssh/zdm_key opc@erpdb-node1.example.oraclevcn.com 'hostname'

STEP 3 Write the response file — this is where the whole migration is described. Key parameters below; the rest take sensible defaults.

zdmhost $ erpprod_azure.rsp
TGT_DB_UNIQUE_NAME=ERPPROD_azure
MIGRATION_METHOD=ONLINE_PHYSICAL
DATA_TRANSFER_MEDIUM=DIRECT

# Source database
SOURCEDATABASE_CONNECTIONDETAILS_HOST=source-db-host
SOURCEDATABASE_CONNECTIONDETAILS_PORT=1521
SOURCEDATABASE_CONNECTIONDETAILS_SERVICENAME=ERPPROD.example.com
SOURCEDATABASE_ADMINUSERNAME=SYS

# Target on Oracle Database@Azure (ExaDB-D)
TARGETDATABASE_OCID=ocid1.database.oc1..aaaa____
TARGETDATABASE_ADMINUSERNAME=SYS
TARGETDATABASE_CONNECTIONDETAILS_HOST=erpdb-scan.example.oraclevcn.com
TARGETDATABASE_CONNECTIONDETAILS_PORT=1521
TARGETDATABASE_CONNECTIONDETAILS_SERVICENAME=ERPPROD_azure.example.oraclevcn.com

# OCI authentication for the target
OCIAUTHENTICATIONDETAILS_USERPRINCIPAL_TENANTID=ocid1.tenancy.oc1..aaaa____
OCIAUTHENTICATIONDETAILS_USERPRINCIPAL_USERID=ocid1.user.oc1..aaaa____
OCIAUTHENTICATIONDETAILS_USERPRINCIPAL_FINGERPRINT=aa:bb:cc:____
OCIAUTHENTICATIONDETAILS_USERPRINCIPAL_PRIVATEKEYFILE=/home/zdmuser/.oci/oci_api_key.pem
OCIAUTHENTICATIONDETAILS_REGIONID=eastus

# Keep the source primary until you decide to switch over
SHUTDOWN_SRC=FALSE

STEP 4 Run an evaluation first (-eval) — it validates everything without moving data. Fix whatever it flags, then run for real.

zdmhost $ run_zdm.sh
# Dry run: validates connectivity, versions, space, config
$ZDM_HOME/bin/zdmcli migrate database \
  -rsp /home/zdmuser/erpprod_azure.rsp \
  -sourcenode source-db-host \
  -sourcedb ERPPROD \
  -srcauth zdmauth -srcarg1 user:oracle \
    -srcarg2 identity_file:/home/zdmuser/.ssh/zdm_key \
    -srcarg3 sudo_location:/usr/bin/sudo \
  -targetnode erpdb-node1.example.oraclevcn.com \
  -tgtauth zdmauth -tgtarg1 user:opc \
    -tgtarg2 identity_file:/home/zdmuser/.ssh/zdm_key \
    -tgtarg3 sudo_location:/usr/bin/sudo \
  -eval

# Real run: drop -eval, add -pauseafter to stop before switchover
$ZDM_HOME/bin/zdmcli migrate database \
  -rsp /home/zdmuser/erpprod_azure.rsp \
  -sourcenode source-db-host -sourcedb ERPPROD \
  -srcauth zdmauth -srcarg1 user:oracle \
    -srcarg2 identity_file:/home/zdmuser/.ssh/zdm_key \
    -srcarg3 sudo_location:/usr/bin/sudo \
  -targetnode erpdb-node1.example.oraclevcn.com \
  -tgtauth zdmauth -tgtarg1 user:opc \
    -tgtarg2 identity_file:/home/zdmuser/.ssh/zdm_key \
    -tgtarg3 sudo_location:/usr/bin/sudo \
  -pauseafter ZDM_CONFIGURE_DG_SRC

STEP 5 Monitor the job by its ID, let it sync, and resume it to perform the switchover when you are ready.

zdmhost $ monitor_resume.sh
# The migrate command returns a job ID; watch it
$ZDM_HOME/bin/zdmcli query job -jobid 12

# ZDM pauses after building and syncing the standby.
# When you are ready for the (near-zero-downtime) switchover, resume:
$ZDM_HOME/bin/zdmcli resume job -jobid 12

# After it completes, the target on Azure is the new primary.
$ZDM_HOME/bin/zdmcli query job -jobid 12   # expect: SUCCEEDED

Always run -eval, always use -pauseafter

The -eval dry run catches version mismatches, missing privileges, space problems, and connectivity gaps before you have moved a single block — it is the cheapest insurance in this whole article. And -pauseafter lets ZDM build and sync the standby days in advance, so the only thing left for your maintenance window is the switchover itself, which takes minutes.

🔧 Why ZDM is usually the right default

It removes the manual choreography that makes the other methods error-prone: it sequences the backup, the standby build, the sync, the switchover, and the validation, and it rolls back cleanly if a step fails. You still need to understand RMAN, Data Guard, and GoldenGate to debug it — ZDM does not replace that knowledge — but for a standard move it turns a multi-day manual runbook into one command and a response file.

7.Method 5 — Data Guard

If you want a same-version move that is fully rehearsed and instantly reversible, build the Azure target as a physical standby of your source, let it sync, and switch over. It is essentially what ZDM's physical workflow automates — doing it by hand gives you complete control and a deep understanding of the cutover, at the cost of more steps.

STEP 1 Prepare the source (primary) for standby: force logging, standby redo logs, and the parameters that let it ship redo.

source $ dg_primary_prep.sql
ALTER DATABASE FORCE LOGGING;

-- Standby redo logs, one more group than online redo, same size
ALTER DATABASE ADD STANDBY LOGFILE THREAD 1
  GROUP 11 SIZE 1G, GROUP 12 SIZE 1G, GROUP 13 SIZE 1G, GROUP 14 SIZE 1G;

ALTER SYSTEM SET LOG_ARCHIVE_CONFIG='DG_CONFIG=(ERPPROD,ERPPROD_AZURE)';
ALTER SYSTEM SET LOG_ARCHIVE_DEST_2=
  'SERVICE=ERPPROD_AZURE ASYNC VALID_FOR=(ONLINE_LOGFILES,PRIMARY_ROLE)
   DB_UNIQUE_NAME=ERPPROD_AZURE';
ALTER SYSTEM SET LOG_ARCHIVE_DEST_STATE_2=ENABLE;
ALTER SYSTEM SET FAL_SERVER=ERPPROD_AZURE;
ALTER SYSTEM SET STANDBY_FILE_MANAGEMENT=AUTO;

STEP 2 Build the standby on the Azure target with RMAN DUPLICATE ... FOR STANDBY FROM ACTIVE DATABASE — no backup needed.

target $ dg_build_standby.rman
# Target instance started NOMOUNT with a minimal init; source reachable via TNS
rman TARGET sys/****@ERPPROD AUXILIARY sys/****@ERPPROD_AZURE

DUPLICATE TARGET DATABASE FOR STANDBY
  FROM ACTIVE DATABASE
  DORECOVER
  SPFILE
    SET db_unique_name 'ERPPROD_AZURE'
    SET db_create_file_dest '+DATA'
    SET db_recovery_file_dest '+RECO'
    SET fal_server 'ERPPROD'
    SET standby_file_management 'AUTO'
  NOFILENAMECHECK;

STEP 3 Start managed recovery on the standby and confirm redo is applying and the gap is closing.

target $ dg_start_apply.sql
-- On the standby (target)
ALTER DATABASE RECOVER MANAGED STANDBY DATABASE
  USING CURRENT LOGFILE DISCONNECT FROM SESSION;

-- Confirm apply progress and that there is no growing gap
SELECT process, status, sequence# FROM v$managed_standby WHERE process='MRP0';
SELECT thread#, MAX(sequence#) FROM v$archived_log
 WHERE applied='YES' GROUP BY thread#;

STEP 4 When you are ready, verify the standby is ready, then switch over. This is the cutover — a few minutes.

source → target $ dg_switchover.sql
-- On the PRIMARY (source): confirm it is safe to switch
SELECT switchover_status FROM v$database;   -- want TO STANDBY or SESSIONS ACTIVE
ALTER DATABASE SWITCHOVER TO ERPPROD_AZURE VERIFY;

-- The former standby (Azure) is now primary. Open it:
ALTER DATABASE OPEN;

-- Confirm roles flipped
SELECT db_unique_name, database_role, open_mode FROM v$database;
--   ERPPROD_AZURE | PRIMARY  | READ WRITE

The reversibility you are paying for

Because switchover is a clean role swap rather than a one-way restore, the old primary becomes a standby of the new one. If something looks wrong in the first minutes after cutover, you switch back the same way. That safety net is the reason Data Guard is favoured for high-value systems where an irreversible mistake is unacceptable — and it is exactly what makes the ZDM physical workflow trustworthy, since that is what it runs underneath.

⚠️ Data Guard prerequisites people forget

Same Oracle version and patch level on both ends. Little-endian to little-endian only. Standby redo logs sized to match online redo. Clean TNS resolution both directions. And on an Oracle Database@Azure target, factor in TDE — if the source is encrypted, the keys have to be available on the target before recovery will apply.

8.Post-Migration Validation

The migration is not done when the data lands — it is done when you have proven the target matches the source and performs. Run these regardless of method.

STEP 1 Object and row-count reconciliation. The fastest way to catch a partial load.

both $ reconcile.sql
-- Run identically on source and target, diff the output
SELECT owner, object_type, COUNT(*) AS cnt
FROM   dba_objects
WHERE  owner IN ('SALES','FINANCE','HR')
GROUP  BY owner, object_type
ORDER  BY owner, object_type;

-- Row counts per table (spot-check the big and the critical ones)
SELECT table_name, num_rows
FROM   dba_tables
WHERE  owner = 'SALES'
ORDER  BY num_rows DESC NULLS LAST;

-- Anything that failed to compile?
SELECT owner, object_type, object_name
FROM   dba_objects
WHERE  status = 'INVALID' AND owner IN ('SALES','FINANCE','HR');

STEP 2 Recompile invalids and regather statistics fresh on the target.

target $ post_load.sql
-- Recompile everything that came in invalid
@?/rdbms/admin/utlrp.sql

-- Fresh, target-appropriate optimizer statistics
EXEC DBMS_STATS.GATHER_DATABASE_STATS(-
  degree => 8, -
  options => 'GATHER AUTO', -
  cascade => TRUE);

STEP 3 Prove performance with real queries, not a feeling. Pull your worst statements from the source and run them on the target.

source $ capture_top_sql.sql
-- On the source: your ten heaviest statements by elapsed time
SELECT sql_id, elapsed_time/1e6 AS elapsed_s, executions, sql_text
FROM   v$sqlstats
ORDER  BY elapsed_time DESC
FETCH  FIRST 10 ROWS ONLY;

-- Then run those same statements on the target and compare
-- elapsed time and plans (DBMS_XPLAN.DISPLAY_CURSOR).
ValidationWhat good looks like
Object counts by typeIdentical between source and target for every migrated schema
Row counts on key tablesMatch exactly (or match the agreed cutover SCN for online methods)
Invalid objectsZero after utlrp
Top-SQL elapsed timeEqual or better on the target — investigate any regression before go-live
Sequences and last valuesAdvanced past the source's last value to avoid collisions
Grants, roles, DB links, jobs, directoriesPresent and repointed — these are the classic "forgotten" objects

⚠️ The objects everyone forgets

Logical migrations move schema data but not always the surrounding furniture: database links (which now need to point at new hosts), directory objects (whose paths differ on Exadata), scheduler jobs, profiles, public synonyms, and context/roles. Build a checklist of these from the source and verify each one on the target — they are the top cause of "the migration worked but the app is broken" incidents.

9.Cutover Runbook

Whatever method you used, the cutover itself follows the same disciplined sequence. Write it down, rehearse it on the non-production copy, and time every step.

  1. Freeze changes. Announce the window, stop batch jobs, and put the application into maintenance mode so no new writes hit the source.
  2. Drain in-flight work. Let running transactions complete; confirm no active sessions are still writing.
  3. Final sync. For online methods, confirm replication lag is zero (GoldenGate) or the standby is fully applied (Data Guard/ZDM). For offline methods, this is where the restore/import finishes.
  4. Reconcile. Run the row-count and object checks from section 8 one last time against the frozen source.
  5. Repoint the application. Switch connection strings, TNS aliases, or the service name to the Azure target. This is the actual moment of cutover.
  6. Smoke test. Run a scripted set of read and write transactions that exercise the critical paths.
  7. Open the gates. Take the application out of maintenance mode and monitor closely.
  8. Keep the source recoverable. Do not decommission the source immediately — keep it as a fallback for an agreed period, especially with Data Guard where a switchback is trivial.
app $ repoint_and_smoke.sh
# Repoint the app tier at the Azure target (example: update the alias)
cat > $TNS_ADMIN/tnsnames.ora <<'EOF'
ERPPROD =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = erpdb-scan.example.oraclevcn.com)(PORT = 1521))
    (CONNECT_DATA = (SERVER = DEDICATED)
      (SERVICE_NAME = ERPPROD_azure.example.oraclevcn.com)))
EOF

# Scripted smoke test: read, write, read-back
sqlplus app_user/****@ERPPROD <<'EOF'
SELECT COUNT(*) FROM sales.orders;
INSERT INTO sales.smoke_test(id, ts) VALUES (1, SYSTIMESTAMP);
COMMIT;
SELECT * FROM sales.smoke_test WHERE id = 1;
DELETE FROM sales.smoke_test WHERE id = 1; COMMIT;
EOF

The migration you can undo is the migration you can trust. Never cut over without a tested fallback and a clear, time-boxed decision point for using it.

10.Pitfalls That Bite

PitfallWhat happensFix
Version/patch mismatch on physical methodsRMAN, Data Guard, or ZDM physical simply refuse to proceedMatch the release exactly, or switch to a logical method
Endianness overlookedA big-endian source cannot be RMAN-restored to little-endian ExadataUse Data Pump or cross-platform transportable tablespaces
Character-set incompatibilityLogical import fails or silently corrupts multibyte dataAssess with the character-set scanner before migrating
TDE not plannedRecovery or restore stalls because keys are unavailable on the targetConfigure the keystore and keys on the target before you start
SCN handshake wrong (GoldenGate)Rows lost or duplicated between load and replication startAlign Extract start, flashback_scn, and AFTERCSN; reconcile counts
Network throughput underestimatedAn offline copy overruns the maintenance windowMeasure real throughput; switch to an online method if it will not fit
Statistics imported from sourceSlow load and plans that do not fit the targetexclude=STATISTICS, regather with DBMS_STATS
Sequences behind the sourcePrimary-key collisions right after cutoverAdvance sequences past the source's last value at cutover
Forgotten peripheral objectsApp breaks despite correct data — links, jobs, directories missingBuild and verify a peripheral-object checklist (section 8)
No rehearsalThe first time you run the cutover is in production — and it is timed wrongRehearse end to end on a non-prod copy and time every step

11.Frequently Asked Questions

Which method should I default to?

ZDM. It automates RMAN, Data Guard, Data Pump, and GoldenGate under one command, runs an evaluation dry run first, and rolls back cleanly on failure. Drop to a manual method only when you need fine-grained control or ZDM's workflows do not fit an unusual source.

Can I migrate across Oracle versions?

Yes, with a logical method — Data Pump, or ZDM's logical workflow, or GoldenGate. Physical methods (RMAN, Data Guard, ZDM physical) require the same release on both ends.

What is the lowest-downtime option?

GoldenGate, or ZDM's online workflows which use it. You do the bulk load ahead of time, replicate changes continuously, and cut over in seconds when lag reaches zero.

Do I need staging space?

Not always. RMAN active duplicate, Data Pump network import, and ZDM direct transfer all move data over the network with no intermediate dump or backup on disk. Choose those when staging capacity is tight.

How do I handle a very large database?

Favour physical methods (block-level is faster than logical rebuild for huge volumes), parallelise with multiple RMAN channels and SECTION SIZE, and use an online method so the long copy happens while production keeps running. Measure real network throughput before committing to a window.

What about encryption on the target?

Oracle Database@Azure targets typically expect encrypted tablespaces, so plan TDE from the start: configure the keystore and keys on the target before recovery or import, and decide whether you are encrypting on the way in or matching an already-encrypted source.

Is the source safe if the migration fails?

With online methods, yes — the source stays the live primary until you switch over, and Data Guard lets you switch back. Keep the source recoverable for an agreed period after cutover regardless of method; never decommission it on day one.

How do I prove the migration actually worked?

Reconcile object and row counts against the frozen source, recompile invalids to zero, regather statistics, and run your ten heaviest queries on the target to confirm performance holds. Do not rely on "it opened without errors."

12.Key Takeaways

The short version

•  Default to ZDM. It orchestrates the other four, runs a dry-run evaluation, and pauses before switchover so your window only contains the cutover.

•  RMAN and Data Guard are the manual same-version paths — RMAN for a windowed whole-database move, Data Guard for a rehearsed, reversible cutover.

•  Data Pump is the flexible logical option: cross-version, partial, character-set changes, reorg on the way in.

•  GoldenGate is how you get seconds of downtime — and the SCN handshake between capture, load, and apply is the detail that makes it correct.

•  Validate and rehearse. Reconcile counts, recompile, regather stats, test real queries, and never cut over without a tested fallback.

None of these methods is exotic — they are the same tools Oracle DBAs have used for years, pointed at a target that happens to sit inside an Azure datacenter. The discipline that makes a migration succeed is unchanged: pick the method that matches your downtime and version constraints, rehearse the whole thing on a copy, prove the result, and keep a way back. Do that, and moving a database to Oracle Database@Azure becomes a routine operation rather than a leap of faith.

SZ

Syed Zaheer

Service Delivery Director · Techvisions · Cloud, AI & Managed Infrastructure

Writes, speaks, and builds across the Oracle stack — databases, middleware, E-Business Suite, AI, and cloud infrastructure. Most of what appears here comes out of delivery work with enterprises modernizing their platforms, published in the hope it saves someone else the same afternoon.




Comments

Popular posts from this blog

Installation of Oracle Applications R12.1.1 on Linux and vmware

EBS R12.2 Install Error - oracle.apps.fnd.txk.config.ProcessStateException: Patch directory does not exist or not writable -

ntp service in Maintenance mode Solaris 10