
Oracle tables reach Snowflake in one of three ways: spool them to files and COPY INTO once, poll them with timestamp or ORA_ROWSCN queries, or mine the redo log for committed changes (log-based CDC). An export suits a one-off migration with a write freeze; for a live database, CDC fits better because it also sees hard deletes, which queries never do.
Oracle exposes redo through LogMiner, which is part of Oracle Database, and XStream, which Oracle licenses through GoldenGate. Either way the source needs ARCHIVELOG mode, supplemental logging and archived logs kept longer than your worst outage. In Snowflake, warehouse time per load and the mapping of decimal NUMBER columns decide cost and correctness. Estuary is one managed option here: its Oracle capture reads redo through LogMiner sessions into a durable collection, which its Snowflake materialization loads on a schedule you set.
Choosing an Oracle to Snowflake method
| Approach | Best fit | Freshness | Operational burden | Example implementation |
|---|---|---|---|---|
| One-time export | One migration with a write freeze | Point in time | Low, but manual | SQL*Plus spool → PUT → COPY INTO |
| Scheduled incremental queries | Daily freshness, no redo access | At the schedule | Medium; misses hard deletes | Polling on an update timestamp or ORA_ROWSCN |
| XStream CDC | GoldenGate licensed, or Snowflake-native tooling | Ongoing | Medium, plus an XStream license | GoldenGate; Openflow Connector for Oracle |
| Self-managed LogMiner CDC | Kafka Connect in place | Ongoing; changes appended | High: Debezium tuning, your own MERGE | Debezium + Snowflake Kafka connector |
| Managed LogMiner CDC | Production Oracle, no GoldenGate license | Redo mined continuously, loads on a schedule | Low: grants, logging, retention | Estuary |
Polling never sees hard deletes, and ORA_ROWSCN is per block unless a table uses ROWDEPENDENCIES; the rest of this guide assumes redo-based CDC.
How Oracle to Snowflake CDC works
Oracle records every change in its redo log, ordered by system change number (SCN), and CDC reads that log instead of the tables.
- Archived logs. Online redo logs are reused in a cycle; ARCHIVELOG mode copies each filled log first, so a pipeline that falls behind can still read it.
- Supplemental logging adds the column values needed to identify rows. Oracle enables none by default, which leaves LogMiner unusable.
- LogMiner (
DBMS_LOGMNR): the tool registers redo files for an SCN range and queriesV$LOGMNR_CONTENTS, which returns each change as SQL. - The LogMiner dictionary maps object IDs to names. The online catalog knows only each table's current version; a dictionary extracted into the redo handles DDL, at a resource cost.
- XStream Out: an in-database capture process mines redo and an outbound server streams changes in commit order.
The pipeline records a starting SCN, copies existing rows, then mines redo from that SCN, reconciling the overlap so a row updated mid-copy ends in its latest state. It holds each transaction until commit and checkpoints its SCN. Snowflake is loaded by staging files and running MERGE by primary key, or by appending through Snowpipe Streaming.
LogMiner vs XStream, and when a GoldenGate license applies
Oracle's licensing manual lists LogMiner as "available for use with all Oracle Database offerings", Standard Edition 2 included, and states that XStream "is licensed via the Oracle GoldenGate product". Snowflake's Openflow documentation quotes Oracle: "a license to Oracle Database Enterprise Edition is a prerequisite to license and use Oracle XStream." The same manual says LogMiner used with GoldenGate Supplemental Logging, which ENABLE_GOLDENGATE_REPLICATION = TRUE turns on, needs a GoldenGate license; Oracle's reference sets that parameter's default to FALSE. XStream can also capture on a downstream database. Check your Oracle agreement.
Oracle requirements
Every log-based method needs four things on the source. This SQL is for a self-managed non-CDB on 12c or later:
sql-- 1. ARCHIVELOG mode (as SYSDBA; back up before and after)
SELECT LOG_MODE FROM V$DATABASE;
SHUTDOWN IMMEDIATE
STARTUP MOUNT
ALTER DATABASE ARCHIVELOG;
ALTER DATABASE OPEN;
-- 2. Supplemental logging: minimal, plus all columns (database-wide or per table)
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA;
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;
-- or, to limit redo growth: ALTER TABLE sales.orders ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;
-- 3. A read-only capture user
CREATE USER cdc_user IDENTIFIED BY "<password>";
GRANT CREATE SESSION, LOGMINING TO cdc_user;
GRANT SELECT ON sales.orders TO cdc_user; -- or SELECT ANY TABLE
GRANT SELECT_CATALOG_ROLE, EXECUTE_CATALOG_ROLE TO cdc_user;
GRANT SELECT ON V_$DATABASE TO cdc_user; -- V$DATABASE is a public synonym for V_$DATABASE
GRANT SELECT ON V_$LOG TO cdc_user;The fourth is archived-log retention longer than your worst outage plus repair time, with the archive destination sized for it at peak redo rate.
| Where Oracle runs | Enable the prerequisites | Watch out for |
|---|---|---|
| Self-managed non-CDB | SQL above | Switching archive mode needs a restart and makes earlier backups unusable |
| CDB (the only architecture from 21c) | In CDB$ROOT, for a C## common user, with CONTAINER=ALL grants plus SET CONTAINER | V$LOGMNR_CONTENTS is restricted to the root; per-PDB mining needs 19c RU10+ |
| RAC | SQL above | Each instance writes its own redo thread; LogMiner returns partial results unless every active thread's logs are added |
| Amazon RDS for Oracle | rdsadmin procedures (below); archived logs exist only while backup retention is above zero | Grant V_$ views through rdsadmin.rdsadmin_util.grant_sys_object; RDS CDBs block root access |
sql-- Amazon RDS for Oracle
BEGIN
rdsadmin.rdsadmin_util.alter_supplemental_logging(p_action => 'ADD');
rdsadmin.rdsadmin_util.alter_supplemental_logging(p_action => 'ADD', p_type => 'ALL');
rdsadmin.rdsadmin_util.set_configuration(name => 'archivelog retention hours', value => '168');
END;
/
COMMIT; -- required for the retention change; retained logs use instance storageThree rules prevent most incidents:
- Enable supplemental logging before the pipeline's starting point. LogMiner needs it in the redo it mines.
- Keep table and column names to 30 characters. LogMiner doesn't support longer ones; GoldenGate and XStream do.
- Add a primary key to keyless tables. Without one, a Snowflake
MERGEhas nothing to match each Oracle change against.
Snowflake requirements
In Snowflake, the Oracle data needs a target database and schema, a virtual warehouse for loads, and a dedicated user and role granted access to all three, authenticated with a key pair as Snowflake phases out single-factor passwords for service users.
- Warehouse:
AUTO_SUSPEND = 60keeps the gaps between Oracle loads cheap, given per-second billing with a 60-second minimum per resume. - Timestamp type:
TIMESTAMP_TZkeeps aTIMESTAMP WITH TIME ZONEoffset,TIMESTAMP_LTZstores the instant, andTIMESTAMP_NTZsuits zone-lessDATEandTIMESTAMP; an account-levelTIMESTAMP_TYPE_MAPPINGmust match. - Identifiers: both systems upper-case unquoted names; keep
QUOTED_IDENTIFIERS_IGNORE_CASEoff so quoted mixed-case Oracle columns resolve exactly.
Setting up Oracle to Snowflake replication
- Enable ARCHIVELOG mode and supplemental logging, and confirm both in
V$DATABASE. - Set archived-log retention above your worst outage.
- Create the capture user with the LogMiner grants (a
C##common user on a CDB,grant_sys_objecton RDS) and network access: allowlisted IP, SSH bastion or private networking. - Prepare Snowflake as described above.
- Decide per table between current state (
MERGE) and an append-only history, and how to treat keyless tables andCLOB/BLOBcolumns. - Load existing data, keeping archived logs from the snapshot's starting SCN until the stream catches up.
- Start mining redo from the snapshot's SCN, and choose a Snowflake load interval to match report freshness.
- Validate. Compare row counts and
SUM()of high-precisionNUMBERcolumns. Update and delete a row, wait one load interval, and check Snowflake. Check aDATEwith a time. Add a column. Stop the pipeline across a log switch and confirm a clean resume.
What happens after the initial load?
- Updates carry every column with all-column supplemental logging; with only primary-key logging, just the key and changed columns.
- Primary-key updates must become a delete plus an insert in Snowflake, or the old row lingers.
- Deletes arrive as redo
DELETEoperations; most tools flag the Snowflake row rather than remove it unless hard deletes are on. - Rolled-back transactions never reach Snowflake; nothing appears before commit.
- DDL depends on the dictionary: with the online catalog, redo written before an
ALTER TABLEcomes back as undecodable hex. - In Snowflake, most tools add new columns (earlier rows stay
NULL) and keep dropped ones; narrowing a type usually forces a rebuild.
Oracle to Snowflake data type mapping
| Oracle | Snowflake target | Notes |
|---|---|---|
NUMBER(p,0), INTEGER | NUMBER(p,0) | Lossless up to 38 digits |
NUMBER(p,s) | NUMBER(p,s) | Snowflake's maximum scale is 37; Oracle allows −84 to 127 |
NUMBER (no precision) | NUMBER(38,s) or VARCHAR | FLOAT keeps about 15 digits |
FLOAT(p) | NUMBER(38,s) or FLOAT | Oracle FLOAT is a NUMBER subtype, not a binary double |
BINARY_FLOAT, BINARY_DOUBLE | FLOAT | IEEE floating point on both sides |
VARCHAR2, NVARCHAR2, CHAR | VARCHAR | Oracle stores '' as NULL |
DATE | TIMESTAMP_NTZ(0) | Oracle DATE has a time; Snowflake DATE truncates it |
TIMESTAMP(n) | TIMESTAMP_NTZ(n) | Up to 9 fractional digits on both |
TIMESTAMP WITH TIME ZONE | TIMESTAMP_TZ | Snowflake keeps the offset, not the region name |
INTERVAL types | Matching Snowflake INTERVAL type | |
CLOB, NCLOB | VARCHAR | 128 MB maximum; some CDC tools skip LOBs |
BLOB, RAW | BINARY | 64 MB maximum |
NUMBER precision
Oracle can hold 38 significant digits in a NUMBER, but a Snowflake FLOAT keeps about 15, so any pipeline that types decimal columns as floating point rounds them. Scaled and unconstrained NUMBER columns suffer most: money, high-scale rates, long identifiers. Map each column to NUMBER(p,s) within Snowflake's 38-digit, scale-37 limits, or VARCHAR, then reload the table; rounded digits can't be recovered in Snowflake.
Performance, latency and Snowflake cost
LogMiner can be polled continuously, but rows appear in Snowflake only when a load runs, so the load interval usually dominates latency. CDC releases a transaction only at commit, so a long batch job's changes arrive together.
Reducing MERGE cost
- Load less often. Each load wakes the warehouse and bills at least 60 seconds, however few changes it carries.
- Coalesce by key between loads, so a row updated 500 times is merged once.
- Lead with a write-ordered key. Snowflake prunes micro-partitions by per-column min/max, so a sequence-generated key touches few partitions and a random GUID scans most of the table.
- Append only where it fits. Snowpipe Streaming inserts without a
MERGE, but updates arrive as new rows and deletes as marker rows, so current state needs a view on top.
Production considerations
- Retries. A restarted pipeline re-mines from its last checkpointed SCN, so merged tables absorb repeated changes and append-only tables duplicate them unless the SCN checkpoint and the Snowflake write commit together.
- Source impact. Mining uses CPU and PGA, extracted dictionaries add redo, and backfills add table reads.
- Monitoring. Track SCN lag (
V$DATABASE.CURRENT_SCNagainst the pipeline's checkpoint), free space in the archive destination, and time since the last Snowflake load.
Common problems and troubleshooting
Archived logs purged before the pipeline read them
If retention deletes a log the pipeline still needs, LogMiner can't start on that SCN range (ORA-01291: missing logfile). LogMiner lists a sequence missing mid-range in V$LOGMNR_LOGS (MISSING_LOGFILE), and returns partial results on RAC if a thread's logs aren't added; check for gaps yourself. Check what is still on disk:
sqlSELECT THREAD#, SEQUENCE#, FIRST_CHANGE#, NEXT_CHANGE#, FIRST_TIME, NAME, DELETED
FROM V$ARCHIVED_LOG WHERE FIRST_TIME > SYSDATE - 2
ORDER BY THREAD#, SEQUENCE#;Recovery, cheapest first:
- Restore the missing logs from backup (RMAN, or on RDS
rdsadmin.rdsadmin_archive_log_downloadwithin backup retention) and resume. - Re-read only rows changed since the gap, by update timestamp or key range, and merge them. Rows deleted in that window stay in Snowflake.
- With no reliable cutoff, re-snapshot the affected tables and merge into Snowflake rather than truncating.
Then raise retention above the outage you just had.
Redo log SCN gap after a restart or failover
A capture that checks SCN continuity can stop after a restart even when no log is missing. Compare sequences and incarnations:
sqlSELECT THREAD#, SEQUENCE#, FIRST_CHANGE#, NEXT_CHANGE#, RESETLOGS_ID
FROM V$ARCHIVED_LOG ORDER BY THREAD#, SEQUENCE#;
SELECT INCARNATION#, RESETLOGS_ID, STATUS FROM V$DATABASE_INCARNATION;- Consecutive
SEQUENCE#, sameRESETLOGS_ID: nothing is missing; the SCN jump marks the restart. - A missing sequence: a log was deleted; treat it as a purge.
- A new incarnation (opened with
RESETLOGS): redo after the reset point was discarded, so Snowflake may hold changes Oracle no longer has. Re-read the tables.
LogMiner CPU and PGA pressure on the source
LogMiner runs inside the source database. Each dictionary build consumes database resources, so a pipeline that extracts one every mining cycle pays continuously; use the online catalog between schema changes. A mining query can also fail with ORA-04036: PGA memory used by the instance exceeds PGA_AGGREGATE_LIMIT; Oracle's remedy is a higher limit or less memory use.
Using Estuary for Oracle to Snowflake
On this route, Estuary's Oracle CDC capture mines redo with LogMiner only, never XStream, and writes each table to a collection (below) that the Snowflake materialization reads. Each Snowflake load is staged in an internal stage, then applied with COPY INTO if every key is new or MERGE if any exists:
- Startup checks: the capture fails without supplemental logging and warns when a table has less than all-column logging. Under 24 hours of archived logs fails; under 7 days warns.
- Mining passes: each runs from the checkpointed SCN toward the current one, capped at 2 GiB of redo files.
- CDB: a
C##common user, with the PDB as the database; the capture switches toCDB$ROOTto mine. RDS CDBs aren't supported. - Load interval: once caught up, Snowflake syncs every 30 minutes by default; backfills don't wait, and repeated changes to a key are combined between syncs.
- Merge vs append: merge by default; delta updates per binding, through Snowpipe Streaming with key-pair auth; the sync schedule still applies to them.
- Deletes: a
DELETEis soft (_meta/op = 'd') unless Hard Delete is on; a key-changing update arrives as a delete plus an insert, andTRUNCATEemits nothing, so truncated rows stay in Snowflake. - Types:
NUMBER(p,s), unconstrainedNUMBERandFLOATarrive as exact decimal strings but land asFLOAT.castToString: trueon the field keeps the exact value in aTEXTcolumn; aDDLoverride alone still converts throughFLOAT.NUMBER(p,0)lands asINTEGER, but asTEXTin primary keys.DATE,TIMESTAMPandTIMESTAMP WITH LOCAL TIME ZONEland asTEXTwithout an offset; cast them in a view.TIMESTAMP WITH TIME ZONEis normalized to UTC. LOB,LONG,XMLTYPE,BINARY_FLOATandBINARY_DOUBLEcolumns are left out of the discovered schema. Names over 30 characters aren't supported; a long table name fails the startup check.
Implementing Oracle to Snowflake with Estuary
These steps adapt Estuary's general data flow guide to this route.
- Complete the prerequisites in Oracle requirements and Snowflake requirements. Estuary also needs:
- A
FLOW_WATERMARKStable in the capture user's schema (in the PDB, on a CDB) withINSERTandUPDATE, and quota onUSERS(non-CDB and CDB SQL). - The Snowflake objects from the connector's setup script, with a public key registered for key-pair authentication. Password auth is deprecated.
- A
- Create the Oracle capture. On the Sources page, click New Capture, choose Oracle Database (not Oracle Database (Batch)), name the capture, and fill in the endpoint properties:
- Server Address:
host:port(port 1521 if omitted). Allowlist Estuary's IP addresses, or use Network Tunnel for an SSH tunnel. Private and BYOC deployments can use private networking. - User and Password:
CDC_USERfrom the SQL above, orC##CDC_USERon a CDB. - Database:
ORCLby default; on a CDB, the PDB name. - Advanced Options: leave Dictionary Mode empty so the capture uses
smart.
- Server Address:
- Select tables. Click Next. Estuary discovers the tables the user can read, one binding per table, under Output Collections.
- Remove tables you don't want. Each binding writes its own collection.
- Keyless tables get Backfill Mode "Without Primary Key" automatically. Raise Backfill Priority to load key tables first.
- Keep Automatically keep schemas up to date on.
Click Save and publish. The backfill starts, with redo mining alongside.
- Create the Snowflake materialization. On the capture's details page, click Materialize, choose Snowflake, and fill in the endpoint properties:
- Host (Account URL):
orgname-accountname.snowflakecomputing.com, no protocol. - Database, Schema, Warehouse and Role from the setup script.
- Snowflake Timestamp Type: used only for
TIMESTAMP WITH TIME ZONEcolumns. - Authentication: Private Key (JWT), with User and Private Key.
- Hard Delete: on to delete rows; off keeps soft deletes.
- Host (Account URL):
- Choose the sync schedule. Under Sync Schedule, Sync Frequency defaults to 30 minutes. Shorter intervals give fresher tables and more warehouse resumes. For business-hours freshness, set Timezone, Fast Sync Start Time, Fast Sync Stop Time and optionally Fast Sync Enabled Days; outside that window, syncs run every 4 hours. See sync schedules.
- Map collections to tables and keep decimals exact. Click Next. For the naming convention, Match source structure keeps Oracle schema and table names. Bindings under Source Collections can set Alternative Schema and Delta Updates. Before publishing, add
castToString: trueunder each binding'sfields.includein the specification editor for decimalNUMBERcolumns, such asAMOUNT: { castToString: true }(custom column types). - Publish. Click Save and publish. Estuary creates the Snowflake tables and loads the backfill without waiting for the schedule.
- Verify that data is landing.
- On the capture details page, the Shard Information card reads PRIMARY and Connector Status shows backfill progress; open a collection from the Collections list for its Data Preview.
- The materialization details page shows Connector Status and charts Data Read.
- In Snowflake, query a target table, then run the checks from Setting up Oracle to Snowflake replication.
How Estuary handles redo gaps, dictionaries and re-backfills
Every change mined from redo, and every backfilled row, lands in a collection: JSON files in your own cloud storage bucket that act as a durable log. Snowflake reads the collection, not Oracle, and the materialization commits its read position with each load, so a crash neither repeats nor skips a batch.
Redo gaps stop the capture instead of dropping changes
Before each mining pass, the capture checks that its redo files cover the whole SCN range, per redo thread. A purged log fails the capture with a redo log SCN gap error instead of yielding a partial result; a gap between consecutive sequence numbers in one incarnation, the signature of a restart, is tolerated. RAC writes one redo thread per instance, and the check covers each thread; test restarts and failover against your cluster.
Dictionary builds only after DDL
In smart mode, the default, the capture mines with the online catalog. After a dictionary mismatch from an ALTER TABLE, it uses an extracted dictionary until mining passes the most recent DDL, then switches back. extract mode builds a dictionary every pass; online can't follow schema changes. Auto-discovery picks up new tables and columns every 15 minutes by default.
What a re-backfill does and doesn't avoid
| Situation | Re-reads Oracle? | What to do |
|---|---|---|
| A Snowflake table is lost or needs a new column type | No | Materialization backfill; it reloads from the collection |
| Add a second destination | No | New materialization; it reads the collection's history first |
| Archived logs purged, gap start known | Changed rows only | Incremental backfill of all bindings, with an Additional Backfill Filter on large tables (ask Estuary support first) |
| Archived logs purged, no usable timestamp or key | Full tables | Incremental backfill of every binding, merged into existing tables |
Limits: replay needs collection retention (trial storage keeps about 20 days); backfills emit no deletes, so rows deleted during a gap stay in Snowflake; delta-update tables get duplicates.
Tables without a primary key
Keyless tables backfill by ROWID range, without a keyed backfill's exact correctness guarantees. Their collection key is LogMiner's record-set ID and SQL sequence number, which identify a change rather than a row, so the Snowflake table accumulates one row per change. Add a primary key, then re-backfill, to get current-state rows.
When another approach may make more sense
- Snowflake-only, XStream licensing accepted: the Openflow Connector for Oracle needs a paid XStream license, embedded through Snowflake on a 36- or 12-month commitment, or your own entitlement. It runs against Oracle 11g+, including single-tenant but not multi-tenant RDS; needs a key per table; soft-deletes with
_SNOWFLAKE_DELETED; and ignoresTRUNCATE. - GoldenGate already licensed: its Snowflake Stage and Merge handler applies full CDC through micro-batched
MERGE. - Kafka and Debezium already run well: Debezium mines with LogMiner by default and emits LOB values only when enabled; the Snowflake Kafka connector inserts rows, so you build the
MERGElayer. - One-time migration with a cutover window: spool with SQL*Plus
SET MARKUP CSV ON QUOTE ON, setNLS_DATE_FORMATto include the time, and keepCOPY INTO's defaultON_ERROR = ABORT_STATEMENT, sinceSKIP_FILEdrops whole files. - No LogMiner access (RDS CDBs, read replicas) or daily freshness: query-based capture, accepting missed hard deletes. Estuary's Oracle Batch connector covers this, polling on
ORA_ROWSCNevery 5 minutes by default, and readsCLOBandRAWas strings.
Related resources
- Estuary Oracle CDC connector, Oracle Batch, Snowflake materialization, sync schedules, backfilling data, schema evolution
- Oracle: LogMiner, XStream, licensing information, archived redo logs
- AWS: RDS for Oracle logging tasks, retaining archived redo logs
- Snowflake: Openflow Connector for Oracle, numeric types, date and time types
To try the route, follow the data flow guide or create an Oracle capture in the Estuary dashboard. Pricing: estuary.dev/pricing.
FAQs
Can I replicate an Oracle view to Snowflake?
Why does a read-only CDC capture need a writable table in Oracle?

About the author
Seán Whelan is a Solutions Engineer at Estuary, helping customers design, troubleshoot, and run production data pipelines. Seán works across change data capture, database replication, and SaaS integrations, and writes about what actually comes up when real teams move data.

















