---
title: "MySQL to Oracle migration guide"
description: "Convert a MySQL or MariaDB dump into an Oracle script with the same tables, keys and data: how to dump, convert, load with SQL*Plus, and every rule applied."
canonical: https://intosql.com/docs/migrations/mysql-to-oracle
last_updated: 2026-10-15
---

# MySQL to Oracle migration guide

IntoSQL reads a MySQL or MariaDB dump and writes one Oracle script with the same tables, columns, keys, indexes, foreign keys, defaults, identities, sequences, comments and rows. It reads the dump itself, without a database server, and lists in the report every value Oracle can't hold as it is and every object it doesn't carry. The script loads on Oracle Database 19c and later.

**Convert in the browser:** [MySQL to Oracle](/mysql-to-oracle) opens the converter set up for this pair.

## Dump for the best result

Any dump from mysqldump, mariadb-dump, mysqlpump, phpMyAdmin or Adminer works, as `.sql` or `.sql.gz`. These options keep more of your data:

```shell
mysqldump --default-character-set=utf8mb4 --hex-blob \
  --routines --triggers --events --single-transaction \
  shop | gzip > shop.sql.gz
```

- `--default-character-set=utf8mb4` keeps emoji and other 4-byte characters. A dump made with `utf8` has already lost them, and the report says so (`dump_lost_four_byte_chars`).
- `--hex-blob` writes binary columns as hexadecimal, which survives copying between systems.
- `--routines --triggers --events` add those objects to the dump, so the report can list them by name.
- `--single-transaction` takes a consistent snapshot of InnoDB tables without locking them.

A dump made with `--no-create-info` still converts: the types are read from the values, and the report says the schema was missing (`no_schema`).

## Convert

In the browser, open [MySQL to Oracle](/mysql-to-oracle) or the converter on the home page, drop the dump and choose Oracle. It reads the file from your disk in pieces, twice, and writes the script as it goes, so the dump is never uploaded and its size isn't limited by memory. The [desktop app](/docs/desktop) does the same with the file on disk.

With the [command line](/docs/cli):

```shell
intosql inspect shop.sql.gz
intosql convert shop.sql.gz --to oracle --output shop.oracle.sql --report shop.report.json
```

`inspect` lists the tables, their rows and what won't be converted, without writing anything. `convert` writes the script and, with `--report`, the report as JSON.

Through the [API](/docs/api), a dump converts like any other file: open a conversion with its name ending in `.sql` or `.sql.gz` and `"output": {"dialect": "oracle"}`.

## Load into Oracle

```shell
NLS_LANG=.AL32UTF8 sqlplus -S -L shop@//localhost/FREEPDB1 @shop.oracle.sql
```

`NLS_LANG` makes SQL*Plus read the script as UTF-8. The user needs `CREATE TABLE`, `CREATE SEQUENCE`, `CREATE TRIGGER` and `CREATE PROCEDURE`, and quota on its tablespace. The script stops at the first error, creates the tables, copies the rows in blocks of 100 with a commit after each, then adds keys, indexes, foreign keys, identities, sequences, triggers and comments. Foreign keys are added `ENABLE NOVALIDATE`, so new rows are checked at once, and then validated: an old row that points to a missing row is named by its constraint instead of failing the load half way.

Oracle stores an empty string as NULL, so values that were `''` read back as NULL; the report counts them per column (`empty_string_null`). Plain names become uppercase (`customers` is `CUSTOMERS`), so queries can leave the quotes out.

## What isn't converted

Views, triggers, stored procedures, functions and events are written in each database's own language. They are listed in the report by kind and name, never dropped silently. `ON UPDATE CURRENT_TIMESTAMP` is the exception: it becomes an Oracle trigger.


## Data types

[MySQL to Oracle data types](/docs/types/mysql-to-oracle) lists each type that changes in the script: what it becomes, why, and the report line.

## Every other rule

Generated from the engine, where each rule is decided.

| In MySQL | In Oracle | Why | Report line |
| --- | --- | --- | --- |
| NOT NULL columns with values that become NULL | NOT NULL dropped | A column whose values had to become NULL, including empty strings, can't keep NOT NULL, or the load would stop. | `not_null_relaxed` |
| text decoded twice (Ã±) | copied as it is | The dump holds exactly these characters, so they are copied; the report points them out. | `mojibake_suspected` |
| ON UPDATE CURRENT_TIMESTAMP | a BEFORE UPDATE trigger | The trigger sets the column to the current time when an update doesn't set it. | `on_update_trigger` |
| generated columns | virtual columns | Oracle computes generated columns on read only; STORED columns become virtual with the same values. | `stored_as_virtual` |
| generated columns with functions Oracle lacks | a plain column | A formula that can't be written for Oracle leaves a plain column and a report line. | `generated_not_translated` |
| INVISIBLE columns | INVISIBLE columns | Oracle hides invisible columns from SELECT * as MySQL does. | None |
| CHECK constraints | translated, inline in CREATE TABLE | Inline checks are tested as each row loads. | None |
| CHECK constraints with functions Oracle lacks | not created | A check that can't be written for Oracle is left out and reported. | `check_skipped` |
| CHECK ... NOT ENFORCED | not created | MySQL keeps these checks without applying them, so the rows may break them. | `check_not_enforced` |
| defaults with functions Oracle lacks | no default | A default that can't be written for Oracle is left out and reported. | `default_not_translated` |
| keys and indexes on a prefix, c(20) | an index on SUBSTR of the column | A function index on the same start of the column keeps the same rows apart, and indexes CLOB columns Oracle can't index whole. | `prefix_unique` |
| FULLTEXT indexes | not created | Oracle Text builds full-text search with its own index type and queries. | `fulltext_not_created` |
| SPATIAL indexes | not created | Geometries are stored as WKB bytes, which a spatial index can't read. | `spatial_index_not_created` |
| functional indexes, and indexes on whole CLOB or BLOB columns | not created | Oracle can't index a large object whole, and some MySQL functions have no Oracle equivalent. | `index_skipped` |
| foreign keys | after the data ENABLE NOVALIDATE, then validated with a warning on orphans | Dumps load with FOREIGN_KEY_CHECKS=0 and MyISAM never checks, so rows without a parent are common. The key checks new rows at once and is validated last; orphans leave it unvalidated with a warning instead of stopping the load. | None |
| ON UPDATE CASCADE, SET NULL or SET DEFAULT; ON DELETE SET DEFAULT | dropped | Oracle's foreign keys act on delete only, with CASCADE or SET NULL. | `foreign_key_action_unsupported` |
| foreign keys to columns without a unique key | not created | Oracle needs a primary key or a unique constraint on the referenced columns; InnoDB doesn't. | `foreign_key_skipped` |
| table and column names | uppercase when they are plain names, else as written | Oracle stores names written without quotes in uppercase, so plain names become uppercase and queries can leave out the quotes; other names keep their case and need quotes. | `identifier_renamed` |
| names over 128 bytes | shortened | Oracle 12.2 and later names hold 128 bytes. | `name_shortened` |
| PARTITION BY | one table | The rows are the same; partition the table again in Oracle if it needs it. | `partitions_dropped` |
| WITH SYSTEM VERSIONING (MariaDB) | a table with the current rows | A dump holds the current rows only. | `system_versioning_dropped` |
| MyISAM, MEMORY and other engines | a regular table | Oracle has one table engine. | `engine_dropped` |
| views, triggers, procedures, functions and events | listed in the report | Their code is written in MySQL's language, so they are listed for you to recreate. | `not_converted_objects` |
| dumps without CREATE TABLE | types read from the values | A dump made with --no-create-info only has rows, so the types come from the values. | `no_schema` |
| _ci collations | exact comparison | Oracle compares text exactly unless told otherwise. | `case_insensitive_collation` |
| SET NAMES utf8 from utf8mb4 tables | converted, with a warning | MySQL 5.x mysqldump writes 3-byte utf8 by default and replaces every emoji with a question mark before any converter sees the file. | `dump_lost_four_byte_chars` |
| dumps without SET NAMES whose text isn't UTF-8 | read as Latin-1 (Windows-1252) | MySQL's latin1 is Windows-1252, the usual encoding of such dumps. | `charset_guessed` |
| sjis, cp932, gbk, big5 dumps | read as UTF-8, with a warning | These encodings are not decoded yet. | `unsupported_charset` |
| UNHEX('..'), CONVERT(b'..', UNSIGNED), CONVERT(".." USING x), ST_GeomFromText('..') in rows | evaluated | Some dump tools write values as these calls; they are read as the values they produce, with geographic WKT turned to longitude first. | None |
| other expressions in rows | NULL | A converter that doesn't run a database can't evaluate them. | `expression_value` |
| DEFAULT in VALUES | the column's default value | A literal default is copied into the row. | `default_keyword_filled` |
| DEFAULT in VALUES without a literal default | NULL | There is no value to copy. | `default_keyword_null` |
| REPLACE and INSERT IGNORE | plain rows | A dump has one row per key, so the rows are the same. | `replace_as_insert` |
| dumps of several databases | one schema, each table named after its database first | The script loads into the schema of the user who runs it, so tables of different databases get their database's name in front. | `databases_merged` |
| CREATE SEQUENCE (MariaDB) | CREATE SEQUENCE, at the value it had reached | Oracle sequences work the same way. | None |
