Migrating Oracle Databases to Oracle Database@Azure: Strategy, Architecture, and Best Practices
Migrating Oracle Databases to Oracle Database@Azure: Strategy, Architecture, and Best Practices
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.
| Method | Downtime | Cross-version | Best fit | Automation |
|---|---|---|---|---|
| RMAN | Hours (offline restore) | Same version only | Full DB, same release, maintenance window available | Scripted, manual |
| Data Pump | Hours to minutes | Yes — 11g up to current | Schema subsets, cross-version, cross-charset, reorg during move | Scripted, manual |
| GoldenGate | Near zero (seconds) | Yes — wide range | Minimal-downtime, cross-version, phased or bidirectional | Config-driven |
| ZDM | Near zero (online) or a window (offline) | Same release for physical; wider for logical | The default — automates RMAN/Data Guard/GoldenGate under one command | Fully automated |
| Data Guard | Minutes (switchover) | Same version only | Same-release move you want rehearsed and reversible | Scripted, 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.
# 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_AZURE2.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.
-- 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 methods2.3 A pre-flight checklist worth running
| Check | Why it matters |
|---|---|
| Source and target character sets | A 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 |
| Endianness | Exadata is little-endian; a big-endian source (older Solaris/AIX) rules out simple RMAN restore |
| Database size and network throughput | Determines whether an offline copy fits your window, or you need an online method |
| Invalid objects and unsupported types | Run a compatibility pass before, not during, the migration |
| TDE / encryption on the target | Oracle Database@Azure targets typically require encrypted tablespaces — plan keys early |
| Time sync and DNS | Data 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.
# 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/migstage3.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.
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.
# 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.
# 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.
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.
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 source4.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.
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# 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=y4.3 Handy Data Pump patterns
| You want to… | Add this |
|---|---|
| Estimate size without exporting | estimate_only=y on expdp |
| Move only structure, no rows | content=metadata_only |
| Move only data into an existing schema | content=data_only with table_exists_action=append |
| Skip and log rows that fail | data_options=skip_constraint_errors |
| Regather stats fresh on the target | exclude=STATISTICS, then DBMS_STATS after |
| Rename a schema during import | remap_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.
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.
-- 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_erpSTEP 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.
# 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.logSTEP 4 On the target, apply the load, then configure Replicat to start from the same SCN and catch up.
-- 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.
-- 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_erpThe 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
| Workflow | How it moves data | Downtime | Use when |
|---|---|---|---|
| Physical online | Builds a standby (Data Guard / restore-from-service), syncs, then switches over | Near zero | Same release, want minimal downtime and a clean role swap |
| Physical offline | RMAN backup to a location, restore on target | A window | Same release, a maintenance window is acceptable |
| Logical online | Data Pump plus GoldenGate for change capture | Near zero | Cross-version or cross-platform with minimal downtime |
| Logical offline | Data Pump, optionally over a database link | A window | Cross-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.
# 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 statusSTEP 2 Set up passwordless SSH from the ZDM host to the source and to the target VM-cluster node — ZDM orchestrates both over SSH.
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.
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=FALSESTEP 4 Run an evaluation first (-eval) — it validates everything without moving data. Fix whatever it flags, then run for real.
# 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_SRCSTEP 5 Monitor the job by its ID, let it sync, and resume it to perform the switchover when you are ready.
# 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: SUCCEEDEDAlways 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.
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 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.
-- 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.
-- 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 WRITEThe 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.
-- 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.
-- 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.
-- 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).| Validation | What good looks like |
|---|---|
| Object counts by type | Identical between source and target for every migrated schema |
| Row counts on key tables | Match exactly (or match the agreed cutover SCN for online methods) |
| Invalid objects | Zero after utlrp |
| Top-SQL elapsed time | Equal or better on the target — investigate any regression before go-live |
| Sequences and last values | Advanced past the source's last value to avoid collisions |
| Grants, roles, DB links, jobs, directories | Present 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.
- Freeze changes. Announce the window, stop batch jobs, and put the application into maintenance mode so no new writes hit the source.
- Drain in-flight work. Let running transactions complete; confirm no active sessions are still writing.
- 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.
- Reconcile. Run the row-count and object checks from section 8 one last time against the frozen source.
- Repoint the application. Switch connection strings, TNS aliases, or the service name to the Azure target. This is the actual moment of cutover.
- Smoke test. Run a scripted set of read and write transactions that exercise the critical paths.
- Open the gates. Take the application out of maintenance mode and monitor closely.
- 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.
# 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;
EOFThe 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
| Pitfall | What happens | Fix |
|---|---|---|
| Version/patch mismatch on physical methods | RMAN, Data Guard, or ZDM physical simply refuse to proceed | Match the release exactly, or switch to a logical method |
| Endianness overlooked | A big-endian source cannot be RMAN-restored to little-endian Exadata | Use Data Pump or cross-platform transportable tablespaces |
| Character-set incompatibility | Logical import fails or silently corrupts multibyte data | Assess with the character-set scanner before migrating |
| TDE not planned | Recovery or restore stalls because keys are unavailable on the target | Configure the keystore and keys on the target before you start |
| SCN handshake wrong (GoldenGate) | Rows lost or duplicated between load and replication start | Align Extract start, flashback_scn, and AFTERCSN; reconcile counts |
| Network throughput underestimated | An offline copy overruns the maintenance window | Measure real throughput; switch to an online method if it will not fit |
| Statistics imported from source | Slow load and plans that do not fit the target | exclude=STATISTICS, regather with DBMS_STATS |
| Sequences behind the source | Primary-key collisions right after cutover | Advance sequences past the source's last value at cutover |
| Forgotten peripheral objects | App breaks despite correct data — links, jobs, directories missing | Build and verify a peripheral-object checklist (section 8) |
| No rehearsal | The first time you run the cutover is in production — and it is timed wrong | Rehearse 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.
Comments