Migrating from PostgreSQL to MySQL: Tools, Type Mapping, and What You Give Up
Migrating from PostgreSQL to MySQL
This is the less-traveled direction, and it shows: the polished tooling mostly points the other way. pgloader, the workhorse of MySQL-to-PostgreSQL moves, only loads into PostgreSQL — it can't help you here. What remains is one maintained GUI tool, a couple of abandoned scripts you should skip, and a manual path that's more reliable than it sounds.
Worth being direct about motive first. The common good reasons to land on MySQL: your organization standardized on it, your ops team runs a large MySQL/Vitess or RDS-MySQL fleet and PostgreSQL is the odd one out, or you're consolidating onto managed infrastructure where MySQL expertise is deep. "MySQL is faster" is not a good reason in 2026 — both engines are fast, and you'll spend the presumed performance win rewriting features PostgreSQL gave you for free. Read the feature-loss list below before committing; for some schemas this migration is a weekend, for others it's a re-architecture.
Tooling Landscape (as of July 2026)
| Tool | State | Verdict |
|---|---|---|
| MySQL Workbench Migration Wizard (8.0.47) | Maintained, official | The main automated path; connects to PostgreSQL over ODBC, converts schema + copies data |
Manual pg_dump + convert + LOAD DATA | Always works | Most control; right answer for large or gnarly schemas |
| SQLines | Open-source converter, last meaningful updates 2023 | Useful for one-off DDL/SQL translation, don't lean on it for data |
| pg2mysql (Lightbox / pivotal-cf) | Abandoned (pivotal-cf archived 2018) | Skip |
| AWS DMS / cloud migration services | Maintained | Sensible for large RDS/Aurora-to-RDS moves with CDC-based cutover |
MySQL Workbench Migration Wizard
Workbench connects to your PostgreSQL server through the psqlODBC driver, reverse-engineers the schema, converts DDL to MySQL dialect, lets you review and edit every conversion decision, then bulk-copies the data. For small-to-medium databases with mainstream types, it does most of the work. Two caveats: install and configure the PostgreSQL ODBC driver first (the wizard's most common failure is an ODBC connection error, not a migration error), and treat its type choices as proposals — review the generated DDL before accepting, especially around the types below.
The manual path
For anything the wizard chokes on: dump schema only (pg_dump --schema-only), rewrite the DDL by hand or with SQLines, create the MySQL schema, then export data as CSV (\copy table TO 'table.csv' CSV) and import with LOAD DATA LOCAL INFILE. CSV is the interoperability layer — it strips every engine-specific representation problem down to "does this string parse."
Type Mapping
| PostgreSQL | MySQL | Notes |
|---|---|---|
serial / identity | INT AUTO_INCREMENT / BIGINT AUTO_INCREMENT | Sequences don't exist in MySQL; per-table auto-increment only |
text | TEXT / LONGTEXT | MySQL TEXT caps at 64 KB — use LONGTEXT if unsure |
boolean | TINYINT(1) | MySQL has no real boolean; true/false become 1/0 |
timestamptz | TIMESTAMP — carefully | The 2038 trap — see below |
timestamp | DATETIME | Safe range to year 9999, no timezone conversion |
numeric | DECIMAL(m,n) | MySQL requires explicit precision; PostgreSQL's unconstrained numeric doesn't map |
uuid | CHAR(36) or BINARY(16) | No native type; BINARY(16) + UUID_TO_BIN() for index efficiency |
jsonb | JSON | Different internals, similar function set; expression indexes replace GIN |
text[] and other arrays | JSON or a child table | No arrays in MySQL — schema redesign, see below |
enum types | ENUM(...) inline | PostgreSQL enums are schema objects; MySQL enums are per-column |
inet / cidr | VARCHAR(45) or VARBINARY(16) | No equivalent |
interval | nothing clean | Store seconds as BIGINT or a string; rewrite arithmetic |
The TIMESTAMP 2038 trap. MySQL's TIMESTAMP is still internally a 32-bit epoch value with a hard ceiling of January 19, 2038 — verified against MySQL's own bug tracker as still true in 8.4 and the 9.x series (as of July 2026). PostgreSQL's timestamptz happily stores dates far beyond that, and contract end dates, 30-year mortgages, and long retention windows already do. Audit before choosing: SELECT max(col) FROM t; on every timestamptz column. Anything at or past 2038 must become DATETIME — and since DATETIME has no timezone awareness, normalize everything to UTC during export (SET timezone = 'UTC' in your dump session) and enforce UTC at the application layer.
Arrays are the schema redesign. A text[] column has two honest translations: a JSON column (queryable with JSON_CONTAINS, indexable via multi-valued indexes on 8.0.17+) or a proper child table. If you ever join or filter on array elements, choose the child table; retrofitting relational integrity onto a JSON column later is worse than doing it now.
SQL Your Application Has to Change
RETURNING— gone in MySQL (MariaDB has it; MySQL does not). RewriteINSERT ... RETURNING idas insert +LAST_INSERT_ID().ON CONFLICT DO UPDATE→ON DUPLICATE KEY UPDATE(semantics differ: it fires on any unique key collision, not a named conflict target).ILIKE— MySQL comparisons are case-insensitive by default under*_cicollations, so plainLIKEusually already behaves likeILIKE. The inverse trap: case-sensitive matching now needsBINARYor a_cs/_bincollation.DISTINCT ON (...)→ window function (ROW_NUMBER() OVER (PARTITION BY ...)) — MySQL 8.0+ has full window function support, so this is mechanical.- Partial indexes (
CREATE INDEX ... WHERE ...) — don't exist. Nearest substitutes: functional indexes overNULLIF-style generated columns, or accept a full index. - Transactional DDL — gone. Every
ALTER TABLEin a MySQL migration script commits implicitly and can't be rolled back. Your migration tooling (Rails, Flyway, whatever) will run fine but failed migrations leave half-applied state — plan for it. TRUNCATEinside a transaction, deferred constraints,EXCLUDEconstraints, custom operators/types, extensions (PostGIS → MySQL spatial is a real but smaller feature set; pgvector → MySQL 9 vector type is younger) — audit each explicitly.
CTEs and recursive CTEs are fine on MySQL 8.0+. So are check constraints (8.0.16+, actually enforced). The gap is narrower than PostgreSQL folklore says — but it is not zero.
Validation
Same discipline as any migration: identical aggregate queries on both engines, diff the output.
SELECT COUNT(*), SUM(CRC32(CONCAT_WS('#', id, email))), MIN(created_at), MAX(created_at)
FROM users;CRC32(CONCAT_WS(...)) works on MySQL and on PostgreSQL 18+ (which added crc32() in 2025), making it a convenient shared checksum; on older PostgreSQL use md5() on both sides instead. Watch the columns where representation changed — booleans (t/f vs 1/0), timestamps (timezone normalization), anything that was an array. Mako connects to PostgreSQL and MySQL side-by-side, so you can keep one editor open per database and run the same validation query against both during cutover.
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| Reaching for pgloader | It only loads into PostgreSQL | Workbench wizard or the manual CSV path |
Mapping timestamptz to TIMESTAMP blindly | Data past Jan 2038 won't fit | Audit max() per column; use DATETIME + UTC discipline |
| Flattening arrays into comma-strings | Unqueryable data | JSON column or child table |
Assuming LIKE is case-sensitive | Matching behavior silently changed | Know your collation; BINARY where sensitivity matters |
| Trusting transactional DDL habits | Half-applied failed migrations | Make migration steps idempotent/re-runnable |
Porting ON CONFLICT 1:1 | Fires on unexpected unique keys | Review every upsert against all unique constraints |
If you're still deciding rather than migrating, PostgreSQL vs MySQL covers the feature comparison in both directions. For the new connection code, see connecting to MySQL from Python and the rest of the connect-from series.
Mako connects to PostgreSQL and MySQL with AI-powered autocomplete — handy for running validation queries against both databases side-by-side during a migration. Try it free at mako.ai.
Skip the terminal. Use Mako.
Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.