Skip to content
IntoSQL home

Migration guides

PostgreSQL to MySQL migration guide

Convert a PostgreSQL dump into a MySQL script with the same tables, keys, indexes and data: how to dump, convert, load, and every rule the converter applies.

View as Markdown

IntoSQL reads a plain PostgreSQL dump and writes one MySQL script with the same tables, columns, keys, indexes, foreign keys, defaults, counters, comments and rows. It reads the dump itself, without a database server, and lists in the report every value MySQL can't hold as it is and every object it doesn't carry. The script loads on MySQL 5.7 and later.

Convert in the browser: PostgreSQL to MySQL opens the converter set up for this pair.

Dump for the best result

Any plain dump from pg_dump or pg_dumpall from PostgreSQL 10 on works, as .sql or .sql.gz, with COPY rows (the default) or --inserts:

Terminal
pg_dump --no-owner --no-privileges shop | gzip > shop.sql.gz
  • --no-owner --no-privileges leave out roles and grants, which the converter skips anyway.
  • A custom or directory archive (-Fc, -Fd) is binary: pg_restore -f shop.sql shop.backup turns it into plain SQL without a server. The converter says so when it gets an archive (dump_archive).
  • pg_dumpall writes every database in one file; each database's tables are named after it, so nothing collides.

A dump made with --data-only still converts: the types are read from the values, and the report says the schema was missing (no_schema). Partitioned tables arrive as one table with every partition's rows (partitions_dropped).

Convert

In the browser, open PostgreSQL to MySQL or the converter on the home page, drop the dump and choose MySQL. 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 does the same with the file on disk.

With the command line:

Terminal
intosql inspect shop.sql.gz
intosql convert shop.sql.gz --to mysql --output shop.mysql.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, a dump converts like any other file: open a conversion with its name ending in .sql or .sql.gz and "output": {"dialect": "mysql"}.

Load into MySQL

Terminal
mysql -e "CREATE DATABASE shop"
mysql --default-character-set=utf8mb4 shop < shop.mysql.sql

The script sets the character set and a binary collation, so text compares exactly as in PostgreSQL, then creates each table with its keys and copies the rows with foreign key and unique checks off. Schemas other than public become a prefix on their tables' names (app.orders is app_orders). The serial or identity column that leads each primary key becomes AUTO_INCREMENT and continues after the highest id; MySQL counts one column per table, so the report names any other (counter_dropped).

Or load it straight in

intosql load and the desktop app load the dump into MySQL without a script, with the same tables, keys and indexes the script creates:

Terminal
intosql load shop.sql.gz --into mysql://ana@localhost/shop

It stops before writing anything when one of the dump's tables already exists. MySQL can't roll back a table, so a failed load drops the tables it created. Loading into a database server is part of IntoSQL Pro, with a 14-day trial. See Loading.

What isn't converted

Views, materialized views, functions, procedures, triggers, rules, row security policies, extensions and types are written in PostgreSQL's own language. They are listed in the report by kind and name, never dropped silently, so you can recreate them in MySQL.

Data types

PostgreSQL to MySQL data types 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 PostgreSQL In MySQL Why Report line
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. not_null_relaxed
citext and nondeterministic collations exact comparison Tables use utf8mb4_bin so text compares exactly, as PostgreSQL's does; columns that ignored case in the source lose that. case_insensitive_collation
sequences not owned by a column not created MySQL has no sequences. Columns that count with one become AUTO_INCREMENT instead. sequence_not_converted
generated columns generated columns, STORED as in the source MySQL computes them the same way. None
generated columns with functions MySQL lacks a plain column A formula that can't be written for MySQL leaves a plain column and a report line. generated_not_translated
defaults with functions MySQL lacks, and defaults from a sequence no default A default that can't be written for MySQL is left out and reported. default_not_translated
CHECK constraints translated, inline in CREATE TABLE MySQL 8.0.16 and MariaDB enforce them; MySQL 5.7 keeps them without checking. None
CHECK constraints with functions MySQL lacks not created A check that can't be written for MySQL is left out and reported. check_skipped
CHECK ... NOT VALID not created PostgreSQL never checked the rows that were there before, so they may break it. check_not_valid
gin, gist, brin and hash indexes, partial and expression indexes not created MySQL has b-tree, full-text and spatial indexes only, on whole columns, without a WHERE. 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. None
foreign keys to tables the dump doesn't create not created A key can't name a table that isn't there. 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. partitions_dropped
schemas one database, tables named after their schema first MySQL's schemas are databases, which a script loaded into one database can't create; tables in public keep their names. schemas_merged
views, functions, procedures, triggers, rules, policies, extensions and types listed in the report Their code is written in PostgreSQL's language, so they are listed for you to recreate. not_converted_objects
dumps without CREATE TABLE (--data-only) types read from the values A data-only dump has only rows, so the types come from the values. no_schema
expressions in INSERT values NULL A converter that doesn't run a database can't evaluate them. expression_value
text decoded twice (ñ) copied as it is The dump holds exactly these characters, so they are copied; the report points them out. mojibake_suspected