Convert Oracle to MySQL
Drop the .sql file SQL Developer's Database Export or SQLcl writes, with its tables and Insert statements, and download a script for MySQL with the same tables, keys, indexes and data. Every value MySQL can't hold as it is, and every object it doesn't carry, is listed in the report.
18 rows converted, no fixes, none rejected
Tables in shop_2024_oracle.sqlOracle dump · written by SQL Developer · utf-8
customers8 rows
7 columns id bigint not null name varchar(100) not null email varchar(150) not null country char(2) not null is_active smallint not null signed_up datetime created_at datetime not null orders10 rows
9 columns id bigint not null customer_id bigint not null invoice bigint status varchar(10) not null total decimal(10,2) not null items int not null ordered_at datetime not null shipped_at datetime note varchar(255)
-- Written by IntoSQL from a Oracle dump. Load it with: mysql --default-character-set=utf8mb4 <database> < <file>
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
SET UNIQUE_CHECKS = 0;
SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO,ALLOW_INVALID_DATES';
CREATE TABLE `customers` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`email` varchar(150) NOT NULL,
`country` char(2) NOT NULL DEFAULT 'US',
`is_active` smallint NOT NULL DEFAULT 1,
`signed_up` datetime,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `customers_email_uk` (`email`),
CONSTRAINT `check1` CHECK (`is_active` IN (0, 1))
) AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='People who bought at least once';
CREATE TABLE `orders` (
`id` bigint NOT NULL AUTO_INCREMENT,
`customer_id` bigint NOT NULL,
`invoice` bigint,
`status` varchar(10) NOT NULL DEFAULT 'pending',
`total` decimal(10,2) NOT NULL,
`items` int NOT NULL DEFAULT 1,
`ordered_at` datetime NOT NULL,
`shipped_at` datetime,
`note` varchar(255),
PRIMARY KEY (`id`),
KEY `orders_status_date_ix` (`status`,`ordered_at`),
CONSTRAINT `check1` CHECK (`status` IN ('pending', 'paid', 'shipped', 'cancelled')),
CONSTRAINT `orders_customer_fk` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`)
) AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO `customers` (`id`,`name`,`email`,`country`,`is_active`,`signed_up`,`created_at`) VALUES (1,'Ana Martínez','ana@example.com','MX',1,'2023-01-15 00:00:00','2023-01-15 10:24:00.000000');
INSERT INTO `customers` (`id`,`name`,`email`,`country`,`is_active`,`signed_up`,`created_at`) VALUES (2,'Zoë Müller','zoe@example.de','DE',1,'2023-02-03 00:00:00','2023-02-03 08:11:42.000000');
INSERT INTO `customers` (`id`,`name`,`email`,`country`,`is_active`,`signed_up`,`created_at`) VALUES (3,'José Peña','jose@example.es','ES',0,'2023-03-21 00:00:00','2023-03-21 19:05:17.000000');
INSERT INTO `customers` (`id`,`name`,`email`,`country`,`is_active`,`signed_up`,`created_at`) VALUES (4,'李雷','lilei@example.cn','CN',1,'2023-05-09 00:00:00','2023-05-09 02:40:00.000000');
INSERT INTO `customers` (`id`,`name`,`email`,`country`,`is_active`,`signed_up`,`created_at`) VALUES (5,'Noah O\'Brien','noah@example.ie','IE',1,NULL,'2023-06-30 14:00:00.000000');
INSERT INTO `customers` (`id`,`name`,`email`,`country`,`is_active`,`signed_up`,`created_at`) VALUES (6,'Amélie Laurent','amelie@example.fr','FR',1,'2024-01-02 00:00:00','2024-01-02 09:30:00.000000');
INSERT INTO `customers` (`id`,`name`,`email`,`country`,`is_active`,`signed_up`,`created_at`) VALUES (7,'Ravi Patel','ravi@example.in','IN',0,'2024-02-29 00:00:00','2024-02-29 23:59:59.000000');
INSERT INTO `customers` (`id`,`name`,`email`,`country`,`is_active`,`signed_up`,`created_at`) VALUES (8,'Sofia Rossi 🌻','sofia@example.it','IT',1,'2024-04-12 00:00:00','2024-04-12 12:12:12.000000');
INSERT INTO `orders` (`id`,`customer_id`,`invoice`,`status`,`total`,`items`,`ordered_at`,`shipped_at`,`note`) VALUES (1,1,1000,'shipped',129.9,3,'2024-03-01 10:00:00','2024-03-02 16:30:00',NULL);
INSERT INTO `orders` (`id`,`customer_id`,`invoice`,`status`,`total`,`items`,`ordered_at`,`shipped_at`,`note`) VALUES (2,2,1001,'paid',54,1,'2024-03-02 11:15:00',NULL,'Gift wrap 🎁');
INSERT INTO `orders` (`id`,`customer_id`,`invoice`,`status`,`total`,`items`,`ordered_at`,`shipped_at`,`note`) VALUES (3,1,1002,'cancelled',12.5,1,'2024-03-03 09:45:00',NULL,'Customer changed their mind');
INSERT INTO `orders` (`id`,`customer_id`,`invoice`,`status`,`total`,`items`,`ordered_at`,`shipped_at`,`note`) VALUES (4,4,1003,'shipped',980,12,'2024-03-05 02:10:00','2024-03-07 08:00:00','请小心轻放');
INSERT INTO `orders` (`id`,`customer_id`,`invoice`,`status`,`total`,`items`,`ordered_at`,`shipped_at`,`note`) VALUES (5,5,1004,'pending',45.75,2,'2024-03-08 17:20:00',NULL,'Leave at the door; ring twice');
INSERT INTO `orders` (`id`,`customer_id`,`invoice`,`status`,`total`,`items`,`ordered_at`,`shipped_at`,`note`) VALUES (6,6,1005,'shipped',310.4,5,'2024-03-10 13:00:00','2024-03-11 09:00:00','Fragile: "glass"');
INSERT INTO `orders` (`id`,`customer_id`,`invoice`,`status`,`total`,`items`,`ordered_at`,`shipped_at`,`note`) VALUES (7,8,1006,'paid',76.2,2,'2024-03-11 18:45:00',NULL,NULL);
INSERT INTO `orders` (`id`,`customer_id`,`invoice`,`status`,`total`,`items`,`ordered_at`,`shipped_at`,`note`) VALUES (8,2,1007,'paid',19.99,1,'2024-03-12 08:00:00',NULL,'Paid at the counter');
INSERT INTO `orders` (`id`,`customer_id`,`invoice`,`status`,`total`,`items`,`ordered_at`,`shipped_at`,`note`) VALUES (9,3,1008,'shipped',64,2,'2024-03-14 15:30:00',NULL,'Shipping date never recorded');
INSERT INTO `orders` (`id`,`customer_id`,`invoice`,`status`,`total`,`items`,`ordered_at`,`shipped_at`,`note`) VALUES (10,7,1009,'paid',1499,1,'2024-03-15 21:05:00',NULL,'C:\\Users\\ravi\\invoice.pdf');
SET UNIQUE_CHECKS = 1;
SET FOREIGN_KEY_CHECKS = 1;
Fixes
Nothing needed fixing.
Needs a look
- 1 sequence isn't created; MySQL has no sequences. Keep the next value in a table if your application needs it.
- The default of "invoice" in orders can't be written for MySQL (a sequence), so the column has no default. Add the default by hand if new rows need it.
Your file is converted on this computer and never uploaded.
Other samples:
What changes from Oracle to MySQL
Each row is a rule the converter applies, with the report line it writes when the rule changes something in your data.
BOOLEAN (23ai)
tinyint(1)
MySQL has no boolean column, so TRUE and FALSE are 1 and 0.
VARCHAR2(n BYTE|CHAR), NVARCHAR2(n), CHAR(n), NCHAR(n), CLOB, NCLOB, LONG
varchar(n) up to 1024 characters, text types beyond
Lengths count characters, whether Oracle counted bytes or characters, so every value fits.
NUMBER, NUMBER(p,s), FLOAT(b)
decimal(p,s), or bigint
NUMBER keeps every digit; a NUMBER without precision whose values are all whole numbers of up to 18 digits is a 64-bit integer, and a negative scale holds as many digits before the point.
INTERVAL YEAR TO MONTH, INTERVAL DAY TO SECOND
varchar(100) with Oracle's text, such as +01-06
MySQL's TIME holds durations up to 838 hours, and has no years or months; the text keeps every part.
Report line: type_replaced
DATE, TIMESTAMP(p)
datetime(p)
Oracle's DATE holds a time to the second, so it is a date and time. An export in an American or German session writes dates without their time or century (DD-MON-RR); export in an ISO session (SYYYY-MM-DD HH24:MI:SS) to keep them.
TIMESTAMP(7) to TIMESTAMP(9)
six digits of a second, the seventh cut
MySQL keeps microseconds; the seventh digit (100 nanoseconds) is cut, never rounded, so no value moves to the next second or day.
Report line: fraction_cut
TIMESTAMP WITH TIME ZONE, TIMESTAMP WITH LOCAL TIME ZONE
datetime(p) in UTC
MySQL's TIMESTAMP only reaches 2038, so moments are stored as datetime in UTC, the same instant whatever offset each value had.
Report line: moments_as_utc
JSON (21c), and CLOB holding JSON
json
The same JSON values; a SQL NULL the export wrote as 'null' stays NULL.
BLOB, RAW(n), LONG RAW
binary(n), varbinary(n) and blob types
Bytes stay bytes.
ROWID, UROWID, XMLTYPE, SDO_GEOMETRY
text
Oracle's own types have no equivalent; their values keep their text, ROWID values included, though they mean nothing off Oracle.
Report line: type_replaced
collations that ignore case (BINARY_CI)
exact comparison (utf8mb4_bin)
Tables use utf8mb4_bin so unique keys keep apart what Oracle kept apart; columns that ignored case lose that. Accent rules differ between versions, so no _ci collation matches Oracle's on every one.
Report line: case_insensitive_collation
identity columns, and a sequence a BEFORE INSERT trigger assigns
AUTO_INCREMENT, continuing after the highest id
The copied ids load as they are, and the counter continues after the highest one. A column a trigger fills from a sequence (the way before 12c) is an identity too.
identity columns the target can't count
a plain integer
MySQL counts one column per table, at the start of a key; any other keeps its values without a counter.
Report line: counter_dropped
CREATE SEQUENCE
not created
The export writes where a sequence has reached as its START WITH, so it continues from there.
Report line: sequence_not_converted
virtual columns (GENERATED ALWAYS AS ... VIRTUAL)
generated columns, STORED when PERSISTED, VIRTUAL otherwise
MySQL computes them from the same formula; the values the export wrote for them are left out.
virtual columns with functions IntoSQL doesn't write, and the || operator
a plain column
A formula that can't be written for MySQL leaves a plain column and a report line; the values the export wrote for virtual columns are left out, and MySQL has none to compute.
Report line: generated_not_translated
defaults with functions IntoSQL doesn't write (SYS_GUID(), ||)
no default
A default that can't be written is left out and reported.
Report line: default_not_translated
CHECK constraints
translated
MySQL 5.7 keeps them without checking; the others test them as each row loads.
CHECK constraints with functions IntoSQL doesn't write
not created
A check that can't be written is left out and reported.
Report line: check_skipped
CHECK constraints that are DISABLE or ENABLE NOVALIDATE
not created
Oracle doesn't apply these checks to the rows already there, so the rows may break them.
Report line: check_not_enforced
function-based, bitmap and domain indexes
not created
IntoSQL writes b-tree indexes on whole columns; create the others again after loading.
Report line: index_skipped
foreign keys
foreign keys, loaded with FOREIGN_KEY_CHECKS=0
The rows load in any order, and the keys check new rows afterwards.
foreign keys to tables the export doesn't create, and DISABLE or NOVALIDATE ones
not created
A key can't name a table that isn't there, and one Oracle doesn't check may have rows without a parent.
Report line: foreign_key_skipped
partitioned tables
one table with every partition's rows
The rows are the same; partition the table again in MySQL if it needs it.
Report line: partitions_dropped
several schemas in one export
one database, tables named after their schema first
MySQL's schemas are databases, which an export loaded into one database can't create; an export of one schema keeps its tables' names.
Report line: schemas_merged
NOT NULL columns with values that become NULL
NOT NULL dropped
A column whose values had to become NULL can't keep NOT NULL, or the load would stop.
Report line: not_null_relaxed
views, synonyms, types, packages, procedures, functions and other triggers
listed in the report
Their code is written in PL/SQL, so they are listed for you to recreate.
Report line: not_converted_objects
exports without CREATE TABLE (data only)
types read from the values
A data-only export has only rows, so the types come from the values.
Report line: no_schema
expressions in INSERT values other than the ones exports write
NULL
A converter that doesn't run a database can't evaluate them; to_date, to_timestamp, TO_CLOB, HEXTORAW and the others exports write are read.
Report line: expression_value
types IntoSQL doesn't know, and object types
longtext
Text holds any value; change the type after loading.
Report line: unknown_type
dates before year 1 and time zones given as region names
NULL
MySQL stores years 1000 to 9999, and Oracle goes back to the year 1. Those values become NULL and the report counts them.
Report line: date_out_of_range
NaN and Infinity in BINARY_FLOAT and BINARY_DOUBLE
NULL
MySQL's floating-point types have no NaN or infinity, so those values become NULL and the report counts them.
Report line: invalid_value_null
NUL characters in text
kept
The value is copied with the NUL character escaped.
text that isn't valid in its encoding
the bad bytes as the replacement character
The script is UTF-8 and every value has to be valid text.
Report line: invalid_utf8_replaced
text decoded twice (ñ)
copied as it is
The script holds exactly these characters, so they are copied; the report points them out.
Report line: mojibake_suspected
| In Oracle | In MySQL | Report line |
|---|---|---|
| BOOLEAN (23ai) | tinyint(1)MySQL has no boolean column, so TRUE and FALSE are 1 and 0. | |
| VARCHAR2(n BYTE|CHAR), NVARCHAR2(n), CHAR(n), NCHAR(n), CLOB, NCLOB, LONG | varchar(n) up to 1024 characters, text types beyondLengths count characters, whether Oracle counted bytes or characters, so every value fits. | |
| NUMBER, NUMBER(p,s), FLOAT(b) | decimal(p,s), or bigintNUMBER keeps every digit; a NUMBER without precision whose values are all whole numbers of up to 18 digits is a 64-bit integer, and a negative scale holds as many digits before the point. | |
| INTERVAL YEAR TO MONTH, INTERVAL DAY TO SECOND | varchar(100) with Oracle's text, such as +01-06MySQL's TIME holds durations up to 838 hours, and has no years or months; the text keeps every part. | type_replaced |
| DATE, TIMESTAMP(p) | datetime(p)Oracle's DATE holds a time to the second, so it is a date and time. An export in an American or German session writes dates without their time or century (DD-MON-RR); export in an ISO session (SYYYY-MM-DD HH24:MI:SS) to keep them. | |
| TIMESTAMP(7) to TIMESTAMP(9) | six digits of a second, the seventh cutMySQL keeps microseconds; the seventh digit (100 nanoseconds) is cut, never rounded, so no value moves to the next second or day. | fraction_cut |
| TIMESTAMP WITH TIME ZONE, TIMESTAMP WITH LOCAL TIME ZONE | datetime(p) in UTCMySQL's TIMESTAMP only reaches 2038, so moments are stored as datetime in UTC, the same instant whatever offset each value had. | moments_as_utc |
| JSON (21c), and CLOB holding JSON | jsonThe same JSON values; a SQL NULL the export wrote as 'null' stays NULL. | |
| BLOB, RAW(n), LONG RAW | binary(n), varbinary(n) and blob typesBytes stay bytes. | |
| ROWID, UROWID, XMLTYPE, SDO_GEOMETRY | textOracle's own types have no equivalent; their values keep their text, ROWID values included, though they mean nothing off Oracle. | type_replaced |
| collations that ignore case (BINARY_CI) | exact comparison (utf8mb4_bin)Tables use utf8mb4_bin so unique keys keep apart what Oracle kept apart; columns that ignored case lose that. Accent rules differ between versions, so no _ci collation matches Oracle's on every one. | case_insensitive_collation |
| identity columns, and a sequence a BEFORE INSERT trigger assigns | AUTO_INCREMENT, continuing after the highest idThe copied ids load as they are, and the counter continues after the highest one. A column a trigger fills from a sequence (the way before 12c) is an identity too. | |
| identity columns the target can't count | a plain integerMySQL counts one column per table, at the start of a key; any other keeps its values without a counter. | counter_dropped |
| CREATE SEQUENCE | not createdThe export writes where a sequence has reached as its START WITH, so it continues from there. | sequence_not_converted |
| virtual columns (GENERATED ALWAYS AS ... VIRTUAL) | generated columns, STORED when PERSISTED, VIRTUAL otherwiseMySQL computes them from the same formula; the values the export wrote for them are left out. | |
| virtual columns with functions IntoSQL doesn't write, and the || operator | a plain columnA formula that can't be written for MySQL leaves a plain column and a report line; the values the export wrote for virtual columns are left out, and MySQL has none to compute. | generated_not_translated |
| defaults with functions IntoSQL doesn't write (SYS_GUID(), ||) | no defaultA default that can't be written is left out and reported. | default_not_translated |
| CHECK constraints | translatedMySQL 5.7 keeps them without checking; the others test them as each row loads. | |
| CHECK constraints with functions IntoSQL doesn't write | not createdA check that can't be written is left out and reported. | check_skipped |
| CHECK constraints that are DISABLE or ENABLE NOVALIDATE | not createdOracle doesn't apply these checks to the rows already there, so the rows may break them. | check_not_enforced |
| function-based, bitmap and domain indexes | not createdIntoSQL writes b-tree indexes on whole columns; create the others again after loading. | index_skipped |
| foreign keys | foreign keys, loaded with FOREIGN_KEY_CHECKS=0The rows load in any order, and the keys check new rows afterwards. | |
| foreign keys to tables the export doesn't create, and DISABLE or NOVALIDATE ones | not createdA key can't name a table that isn't there, and one Oracle doesn't check may have rows without a parent. | foreign_key_skipped |
| partitioned tables | one table with every partition's rowsThe rows are the same; partition the table again in MySQL if it needs it. | partitions_dropped |
| several schemas in one export | one database, tables named after their schema firstMySQL's schemas are databases, which an export loaded into one database can't create; an export of one schema keeps its tables' names. | schemas_merged |
| NOT NULL columns with values that become NULL | NOT NULL droppedA column whose values had to become NULL can't keep NOT NULL, or the load would stop. | not_null_relaxed |
| views, synonyms, types, packages, procedures, functions and other triggers | listed in the reportTheir code is written in PL/SQL, so they are listed for you to recreate. | not_converted_objects |
| exports without CREATE TABLE (data only) | types read from the valuesA data-only export has only rows, so the types come from the values. | no_schema |
| expressions in INSERT values other than the ones exports write | NULLA converter that doesn't run a database can't evaluate them; to_date, to_timestamp, TO_CLOB, HEXTORAW and the others exports write are read. | expression_value |
| types IntoSQL doesn't know, and object types | longtextText holds any value; change the type after loading. | unknown_type |
| dates before year 1 and time zones given as region names | NULLMySQL stores years 1000 to 9999, and Oracle goes back to the year 1. Those values become NULL and the report counts them. | date_out_of_range |
| NaN and Infinity in BINARY_FLOAT and BINARY_DOUBLE | NULLMySQL's floating-point types have no NaN or infinity, so those values become NULL and the report counts them. | invalid_value_null |
| NUL characters in text | keptThe value is copied with the NUL character escaped. | |
| text that isn't valid in its encoding | the bad bytes as the replacement characterThe script is UTF-8 and every value has to be valid text. | invalid_utf8_replaced |
| text decoded twice (ñ) | copied as it isThe script holds exactly these characters, so they are copied; the report points them out. | mojibake_suspected |
Load the script into MySQL
The script creates the tables, copies the rows, then adds keys, indexes and foreign keys, so it loads fast and stops at the first error.
mysql --default-character-set=utf8mb4 mydb < oracle.mysql.sqlForeign keys are created as the dump declares them, with FOREIGN_KEY_CHECKS=0 while the rows load, so a row that points to a missing row doesn't stop the load.
For a large dump, run the client on the same machine as the server, and raise max_allowed_packet if a single row is very large.
IntoSQL Pro loads the file straight into MySQL from the desktop app or the command line, without writing a script.
Dump Oracle for the best result
The converter reads what the dump holds. These options keep more of your data in it:
- In SQL Developer, choose Tools, Database Export, with DDL and the Insert data format. SQLcl writes the same with SET SQLFORMAT INSERT.
- Run ALTER SESSION SET NLS_DATE_FORMAT = 'SYYYY-MM-DD HH24:MI:SS' before exporting: the default format writes dates without their time or century.
- A .dmp file from exp or expdp is binary: import it into an Oracle database, Oracle Database Free in Docker for one, and export it with SQL Developer.
What isn't converted
Views, packages, procedures, functions, triggers, types and synonyms are written in PL/SQL. They are listed in the report by name, never dropped silently, so you can rewrite them for MySQL.
Questions about Oracle to MySQL
Not answered here? Write to us from the contact page and a person replies. Contact page
Is my dump uploaded?
No. The converter runs in your browser and reads the dump from your disk in pieces, so it never leaves your computer. The desktop app and the command line convert on your computer too.
How large can the dump be?
The converter never holds the whole dump in memory: it reads the file twice, by ranges, and writes the script as it goes. Multi-gigabyte dumps convert in the browser, in the desktop app and with the command line.
Can I convert a .dmp file from exp or expdp?
Not directly: Data Pump and the original Export write a binary format only Oracle reads. Import it into any Oracle database, Oracle Database Free runs in Docker, and export it with SQL Developer's Database Export or SQLcl. The converter says so when it gets a .dmp file.
Which Oracle versions can the export come from?
Exports from SQL Developer and SQLcl, with DBMS_METADATA's table definitions, from Oracle 11g on. Every test export comes from Oracle 21c and 23ai, in American, German and ISO sessions, and is compared with its source cell by cell.
What happens to NUMBER, DATE and the other Oracle types?
NUMBER becomes decimal, or bigint for whole numbers, and text past 65 digits; DATE becomes datetime, keeping its time; CLOB longtext, BLOB blob, and intervals keep Oracle's text. NaN and Infinity become NULL and the report counts them.
Are identity columns and sequences kept?
Identity columns become AUTO_INCREMENT and continue after the highest id. MySQL has no sequences, so they are reported, and so are the defaults that draw from them.
What about PL/SQL packages, procedures and triggers?
They are listed in the report by name and not converted, because each database writes them in its own language.
Which MySQL versions does the script load on?
MySQL 5.7 and later. Every test export loads on 5.7 and 9, stopping at the first error if anything goes wrong.