Migration

Data Type Mapping: Oracle to PostgreSQL Complete Reference

Rakesh Mamidala·Founder & Lead Engineer··12 min read

The Reference Table You Wish Your Migration Tool Came With

Data type mapping is where every Oracle → PostgreSQL migration starts, and where the silent bugs hide if you get it wrong. A column declared NUMBER(10) mapped to INTEGERlooks correct on the schema diff — but if your data has a value that overflows int4, you find out at production cutover, not assessment.

This is the comprehensive reference: every common Oracle type, the PostgreSQL equivalent, and whether the mapping is clean (no semantic loss), note (something to be aware of), review (silent-data-loss risk if not checked), or manual (no direct equivalent — needs application change). Modern transpilers handle the clean and note cases automatically; the review and manual cases are what they surface for your eyes.

Numeric Types

Oracle's NUMBER is one type that maps to four different PostgreSQL types depending on precision and scale. Get this wrong and you either waste bytes or risk overflow.

OraclePostgreSQLRiskNote
NUMBER(p, 0), p ≤ 4SMALLINT✓ CleanFits in 2 bytes
NUMBER(p, 0), p ≤ 9INTEGER✓ Clean4 bytes; matches int4 range
NUMBER(p, 0), p ≤ 18BIGINT✓ Clean8 bytes; matches int8 range
NUMBER(p, 0), p ≥ 19NUMERIC(p)· NoteArbitrary precision; ~5% slower than BIGINT
NUMBER(p, s), s > 0NUMERIC(p, s)✓ CleanDirect equivalent
NUMBER (no spec)NUMERIC⚠ ReviewUnconstrained — tighten to NUMERIC(p,s) where possible
INTEGER, INT, SMALLINTINTEGER / SMALLINT✓ CleanIdentical
PLS_INTEGER, BINARY_INTEGERINTEGER✓ CleanPL/SQL-only types — same range as INTEGER in PG
FLOAT, BINARY_FLOATREAL· Note4-byte float; verify precision matches your tolerance
BINARY_DOUBLEDOUBLE PRECISION✓ Clean8-byte float

The NUMBER-with-no-precision trap: Oracle lets you declare NUMBER with no precision or scale — effectively a free-form numeric. Mapping it blindly to NUMERIC in PG works but is slow and prevents the optimiser from making good choices. Whenever possible, tighten to NUMERIC(p, s) based on actual data ranges. A good migration tool surfaces every NUMBER-no-precision column in a risk report so you can audit them upfront.

String Types

The single biggest string-type gotcha is the BYTE vs CHAR semanticson VARCHAR2. Oracle defaults to byte semantics in some installs and character semantics in others — and the wrong choice on a UTF-8 multi-byte column causes silent truncation.

OraclePostgreSQLRiskNote
VARCHAR2(n)VARCHAR(n)✓ CleanPG VARCHAR length is in characters, not bytes
VARCHAR2(n BYTE)VARCHAR(n)⚠ ReviewOracle counted bytes; PG counts characters. Multi-byte UTF-8 may overflow
VARCHAR2(n CHAR)VARCHAR(n)✓ CleanPG default — character semantics
NVARCHAR2(n)VARCHAR(n)✓ CleanBoth UTF-8 internally in modern installs
CHAR(n)CHAR(n)· NoteBoth space-pad. Most teams migrate to VARCHAR — padding rarely wanted
NCHAR(n)CHAR(n)✓ CleanSame as CHAR in UTF-8 environments
LONGTEXT· NoteLONG is deprecated even in Oracle; safe to map straight to TEXT

Check your NLS_LENGTH_SEMANTICS setting in Oracle before quoting timeline. If it's BYTE (the default in older installs) and your data has multi-byte UTF-8 strings, mapping VARCHAR2(50 BYTE) to VARCHAR(50) in PG can silently truncate non-ASCII rows. The safe rule: widen by 1.5× when migrating BYTE-semantics columns, OR audit your actual data first.

Date and Time

The single most common silent-bug in any Oracle → PostgreSQL migration: Oracle DATE includes time-of-day. PostgreSQL DATE does not. If you map Oracle DATE to PG DATE, every hour/minute/second is lost — and your application probably depends on those.

OraclePostgreSQLRiskNote
DATETIMESTAMP⚠ ReviewOracle DATE includes time-of-day. PG DATE does NOT — must map to TIMESTAMP or you lose hours/minutes/seconds silently
TIMESTAMPTIMESTAMP✓ CleanMicrosecond precision on both sides
TIMESTAMP(p)TIMESTAMP(p)✓ CleanPrecision preserved (0–6)
TIMESTAMP WITH TIME ZONETIMESTAMPTZ✓ CleanBoth normalise to UTC internally
TIMESTAMP WITH LOCAL TIME ZONETIMESTAMPTZ⚠ ReviewDifferent semantics — Oracle stores in DB tz, PG stores UTC. Verify application-side conversions
INTERVAL YEAR TO MONTHINTERVAL· NotePG INTERVAL covers all forms; lose the year/month restriction
INTERVAL DAY TO SECONDINTERVAL· NoteSame — PG has single INTERVAL with range qualifiers

Modern transpilers default DATETIMESTAMP automatically. If you're writing your own conversion, lock this in by hand. It is genuinely the most common source of post-cutover bug reports.

LOBs and Binary Types

OraclePostgreSQLRiskNote
CLOBTEXT✓ CleanNo length limit either side. PG TEXT is the right answer.
NCLOBTEXT✓ CleanSame as CLOB in UTF-8 environments
BLOBBYTEA· NotePG stores inline; for files >1 MB consider external object storage + a URL column
RAW(n)BYTEA✓ CleanLength info dropped — PG BYTEA is unbounded
LONG RAWBYTEA· NoteDeprecated in Oracle; safe to map
BFILETEXT (filepath)✗ ManualPG has no equivalent — store the path, move bytes elsewhere. Application change required.

The BLOB-into-BYTEA performance question matters for large files. PostgreSQL stores BYTEA inline up to ~8 KB, then in a TOAST table beyond that — fine for most cases. But if you have multi-MB or GB binary columns, you'll get better performance moving them to object storage (S3, MinIO, GCS) and keeping only a URL/key column in the database. A migration is a good time to make that change.

XMLTYPE, Spatial, and Special Types

The Oracle-specific surface area. Some have direct PG equivalents (with caveats), some need application-level redesign.

OraclePostgreSQLRiskNote
XMLTYPEJSONB (preferred) or XML⚠ ReviewSemantic shift. If your code uses XPath / XQuery, plan a rewrite. JSONB has stronger indexing.
JSON (Oracle 21+)JSONB✓ CleanDirect map. JSONB is binary-encoded — same internal model.
BOOLEAN (PL/SQL)BOOLEAN✓ CleanIdentical
SDO_GEOMETRYGEOMETRY (PostGIS)✗ ManualRequires PostGIS extension and SDO → WKT/WKB conversion in the data layer
ROWID, UROWIDTEXT✗ ManualOracle-internal physical address. Any app logic using ROWIDs needs a rewrite — PG has CTID but it changes on UPDATE
ANYDATA, ANYTYPE(no equivalent)✗ ManualPolymorphic Oracle types. Migrate to a discriminator column + typed columns.
REFCURSOR / SYS_REFCURSORrefcursor✓ CleanSame concept, different declaration syntax. Covered in our PL/SQL Part 1 article.
User-defined object typesComposite types⚠ ReviewCREATE TYPE on both sides, but method bodies (Oracle MEMBER FUNCTION) become standalone functions in PG. Refactor required.

XMLTYPE → JSONB is a semantic shift, not a translation. If your application queries XML data via XPath or XQuery, those queries need to be rewritten in JSON path syntax. PostgreSQL also has a proper XML type if you want to preserve the original format — but JSONB's indexing (GIN) is dramatically better, and most teams who started with XMLTYPE wish they'd started with JSONB anyway.

Things With No PostgreSQL Equivalent

A few Oracle types genuinely have no direct PG counterpart. Each requires an application-side decision before the migration starts:

  • BFILE — read-only pointer to an OS file. PG has no equivalent; the path stays in the database but the file-system access must be reimplemented in the application layer.
  • ANYDATA / ANYTYPE — polymorphic value storage. Refactor to a discriminator column ("type") + typed columns, or to a JSONB column with a tagged structure.
  • VARRAY and nested table types — PG has array types (INT[], etc.) but the semantics differ on indexing and update. Often cleanest to migrate to a child table.
  • ROWID dependence in app code — Oracle's ROWID is the physical address of a row. PG has CTID but it changes when a row is updated. Anywhere your application relies on ROWID identity across updates needs a real primary-key dependency instead.

Validating Type Mappings Post-Load

A clean type-mapping table on paper isn't enough. After bulk load, run two checks:

  1. Per-column distribution check. Compare MIN, MAX, AVG, STDDEV on every numeric column. Identical statistics = data preserved. Any drift = precision loss somewhere.
  2. Per-row checksum. Hash every row on both sides; compare. The Merkle approach (covered in our mathematical proof article) makes this fast enough to run on every migration.

Catch silent precision loss before users notice it.

The Honest Verdict

Type mapping is the layer that looks easy and hides the most bugs. About 90% of the columns in a typical enterprise schema map cleanly with no human decision needed. The remaining 10% — the unconstrained NUMBERs, the BYTE-semantics VARCHAR2s, the XMLTYPEs, the BFILEs — are where the time goes. A migration tool that catches all of them and surfaces them upfront for review is worth its weight; one that silently casts and hopes for the best is the same tool that gives migration projects their bad reputation.

The reference table above covers the 95% case. For the 5% that doesn't map cleanly — run a real assessment against your own schema. The answer is always more interesting than the textbook version.

Audit your own schema in 60 seconds

DBMigrateAIPro's in-browser schema converter accepts pasted DDL and returns every column with its type mapping, risk level, and reasoning. No signup, no data leaves your machine. The fastest way to know what's actually in your schema vs the textbook version.