Data Type Mapping: Oracle to PostgreSQL Complete Reference
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.
| Oracle | PostgreSQL | Risk | Note |
|---|---|---|---|
| NUMBER(p, 0), p ≤ 4 | SMALLINT | ✓ Clean | Fits in 2 bytes |
| NUMBER(p, 0), p ≤ 9 | INTEGER | ✓ Clean | 4 bytes; matches int4 range |
| NUMBER(p, 0), p ≤ 18 | BIGINT | ✓ Clean | 8 bytes; matches int8 range |
| NUMBER(p, 0), p ≥ 19 | NUMERIC(p) | · Note | Arbitrary precision; ~5% slower than BIGINT |
| NUMBER(p, s), s > 0 | NUMERIC(p, s) | ✓ Clean | Direct equivalent |
| NUMBER (no spec) | NUMERIC | ⚠ Review | Unconstrained — tighten to NUMERIC(p,s) where possible |
| INTEGER, INT, SMALLINT | INTEGER / SMALLINT | ✓ Clean | Identical |
| PLS_INTEGER, BINARY_INTEGER | INTEGER | ✓ Clean | PL/SQL-only types — same range as INTEGER in PG |
| FLOAT, BINARY_FLOAT | REAL | · Note | 4-byte float; verify precision matches your tolerance |
| BINARY_DOUBLE | DOUBLE PRECISION | ✓ Clean | 8-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.
| Oracle | PostgreSQL | Risk | Note |
|---|---|---|---|
| VARCHAR2(n) | VARCHAR(n) | ✓ Clean | PG VARCHAR length is in characters, not bytes |
| VARCHAR2(n BYTE) | VARCHAR(n) | ⚠ Review | Oracle counted bytes; PG counts characters. Multi-byte UTF-8 may overflow |
| VARCHAR2(n CHAR) | VARCHAR(n) | ✓ Clean | PG default — character semantics |
| NVARCHAR2(n) | VARCHAR(n) | ✓ Clean | Both UTF-8 internally in modern installs |
| CHAR(n) | CHAR(n) | · Note | Both space-pad. Most teams migrate to VARCHAR — padding rarely wanted |
| NCHAR(n) | CHAR(n) | ✓ Clean | Same as CHAR in UTF-8 environments |
| LONG | TEXT | · Note | LONG 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.
| Oracle | PostgreSQL | Risk | Note |
|---|---|---|---|
| DATE | TIMESTAMP | ⚠ Review | Oracle DATE includes time-of-day. PG DATE does NOT — must map to TIMESTAMP or you lose hours/minutes/seconds silently |
| TIMESTAMP | TIMESTAMP | ✓ Clean | Microsecond precision on both sides |
| TIMESTAMP(p) | TIMESTAMP(p) | ✓ Clean | Precision preserved (0–6) |
| TIMESTAMP WITH TIME ZONE | TIMESTAMPTZ | ✓ Clean | Both normalise to UTC internally |
| TIMESTAMP WITH LOCAL TIME ZONE | TIMESTAMPTZ | ⚠ Review | Different semantics — Oracle stores in DB tz, PG stores UTC. Verify application-side conversions |
| INTERVAL YEAR TO MONTH | INTERVAL | · Note | PG INTERVAL covers all forms; lose the year/month restriction |
| INTERVAL DAY TO SECOND | INTERVAL | · Note | Same — PG has single INTERVAL with range qualifiers |
Modern transpilers default DATE → TIMESTAMP 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
| Oracle | PostgreSQL | Risk | Note |
|---|---|---|---|
| CLOB | TEXT | ✓ Clean | No length limit either side. PG TEXT is the right answer. |
| NCLOB | TEXT | ✓ Clean | Same as CLOB in UTF-8 environments |
| BLOB | BYTEA | · Note | PG stores inline; for files >1 MB consider external object storage + a URL column |
| RAW(n) | BYTEA | ✓ Clean | Length info dropped — PG BYTEA is unbounded |
| LONG RAW | BYTEA | · Note | Deprecated in Oracle; safe to map |
| BFILE | TEXT (filepath) | ✗ Manual | PG 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.
| Oracle | PostgreSQL | Risk | Note |
|---|---|---|---|
| XMLTYPE | JSONB (preferred) or XML | ⚠ Review | Semantic shift. If your code uses XPath / XQuery, plan a rewrite. JSONB has stronger indexing. |
| JSON (Oracle 21+) | JSONB | ✓ Clean | Direct map. JSONB is binary-encoded — same internal model. |
| BOOLEAN (PL/SQL) | BOOLEAN | ✓ Clean | Identical |
| SDO_GEOMETRY | GEOMETRY (PostGIS) | ✗ Manual | Requires PostGIS extension and SDO → WKT/WKB conversion in the data layer |
| ROWID, UROWID | TEXT | ✗ Manual | Oracle-internal physical address. Any app logic using ROWIDs needs a rewrite — PG has CTID but it changes on UPDATE |
| ANYDATA, ANYTYPE | (no equivalent) | ✗ Manual | Polymorphic Oracle types. Migrate to a discriminator column + typed columns. |
| REFCURSOR / SYS_REFCURSOR | refcursor | ✓ Clean | Same concept, different declaration syntax. Covered in our PL/SQL Part 1 article. |
| User-defined object types | Composite types | ⚠ Review | CREATE 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.VARRAYand 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.ROWIDdependence in app code — Oracle'sROWIDis the physical address of a row. PG hasCTIDbut it changes when a row is updated. Anywhere your application relies onROWIDidentity 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:
- Per-column distribution check. Compare
MIN,MAX,AVG,STDDEVon every numeric column. Identical statistics = data preserved. Any drift = precision loss somewhere. - 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.
- 🔗 Free schema converter: medaxai.com/tools/schema-converter
- 🔗 Download the desktop tool (free Year 1): medaxai.com
- 🔗 Related — PL/SQL to PL/pgSQL Complete Guide (Part 1)