MySQL to Postgres migration that moves MySQL to PostgreSQL without a rewrite
The MySQL to Postgres migration from Adapters copies MySQL tables into PostgreSQL, converts MySQL types to their Postgres equivalents, and keeps replicating changed rows on a schedule so you can run both databases in parallel and cut over when the row counts match. Field mapping is no-code, so try it against sample records in the live demo.
No credit card required.
Field mapping auto-plugged · tap a port to rewire
Plug a source port into
Transform on this cable
JSON in
JSON out
5 sample records ready
Last updated September 2026
What running MySQL to Postgres by hand costs you
- A one-shot mysqldump gets stale the moment it finishes, so teams schedule downtime they cannot really afford.
- MySQL and Postgres disagree on types: TINYINT(1) is a boolean in practice, DATETIME has no zone, unsigned integers overflow, and zero dates like 0000-00-00 have no Postgres equivalent at all.
- MySQL is case-insensitive on table and column names by default and Postgres is not, so half the queries break after a naive copy.
The field mapping, out of the box
These cables are pre-wired when you pick the pair. Rewire any of them, or add your own, in the same visual data mapping tool you use for every adapter.
Input / MYSQL
Output / POSTGRES
Transforms included
Incremental runs read a MySQL updated_at watermark so only changed rows move; TINYINT(1) lands as Postgres BOOLEAN, unsigned INT widens to BIGINT so large ids do not overflow, DECIMAL keeps its declared precision as NUMERIC, DATETIME and TIMESTAMP normalize to TIMESTAMPTZ in UTC, invalid zero dates map to NULL instead of failing the load, JSON columns land as JSONB, and writes upsert with ON CONFLICT on the primary key so re-running the migration updates rows in place.
MySQL to Postgres: the type conversions that change your data, and a cutover you can reverse
A MySQL to PostgreSQL move is rarely a throughput problem. It is a semantics problem: MySQL accepts things PostgreSQL refuses, and stores several types in ways that have no exact Postgres equivalent. The migration that goes badly is the one where every row copied successfully and a handful of columns now mean something different. Below: the five conversions that actually decide the outcome, and how to run both databases side by side so the cutover has a way back. PostgreSQL 18 documentation read 16 August 2026.
TINYINT(1) is a boolean only by convention
MySQL has no dedicated boolean type. BOOLEAN is an alias for TINYINT(1), and the display width in the parentheses is not a constraint, so a column your ORM treats as true or false can legally hold 7. PostgreSQL has a real boolean and will reject anything that is not true, false or null. Before you map TINYINT(1) to boolean, run a SELECT DISTINCT on each such column. Usually you get 0 and 1 and the mapping is safe. Occasionally you find a column where an early version of the application stored a small integer status, somebody later reused it as a flag, and both meanings are still in the table. That column needs a decision from a person, not a cast rule, and finding it during a rehearsal is much cheaper than finding it after cutover.
Unsigned integers have no PostgreSQL equivalent
PostgreSQL has no unsigned integer types at all. A MySQL INT UNSIGNED reaches 4,294,967,295, while a Postgres integer stops at 2,147,483,647, so any value above that overflows on load. The correct move is to widen: INT UNSIGNED becomes bigint, and BIGINT UNSIGNED becomes numeric(20,0) if the top of the range is genuinely in use. Add a CHECK (col >= 0) constraint if the non-negativity was load-bearing, because the unsigned declaration was doing that job in MySQL and nothing carries it over. This is worth checking against real data rather than the schema: most unsigned columns never go above two billion and can quietly become bigint, but auto-increment ids on high-volume tables are exactly where they do.
Zero dates, and DATETIME versus TIMESTAMP
MySQL will happily store 0000-00-00 in a date column under its default settings. PostgreSQL will not, and there is no cast that makes it work, so those rows fail the load unless you decide up front what they mean. In practice they are almost always a missing value written by code that could not use null, so mapping them to NULL is right, but it is a data decision to record rather than a silent transform. The zone question is separate and equally important: MySQL DATETIME stores no zone and TIMESTAMP converts to UTC using the session zone. Map DATETIME to plain timestamp and TIMESTAMP to timestamptz. Mapping a naive DATETIME into timestamptz makes PostgreSQL reinterpret it in the server zone, and every value shifts.
Case sensitivity flips, and unique indexes start failing
MySQL's default collations are case-insensitive, so a unique index on an email column treats [email protected] and [email protected] as the same value and only one of them exists. PostgreSQL compares text case-sensitively by default, so both are allowed. Migrating the other way is where it hurts: the MySQL table you are copying may already contain rows that collide once PostgreSQL stops folding case, and the unique index creation fails at the end of a long load with a duplicate key error. Check for collisions before the migration with a GROUP BY lower(col) HAVING count(*) > 1. Then decide deliberately: normalize to lowercase on write, or use citext, or build the unique index on lower(col). Identifier case flips too, since PostgreSQL folds unquoted names to lowercase.
AUTO_INCREMENT becomes a sequence, and sequences do not set themselves
A MySQL AUTO_INCREMENT column maps to a PostgreSQL identity column or a serial, both of which are backed by a sequence object. Here is the step that gets skipped: bulk-loading existing rows writes the id values directly and never advances the sequence, so it is still sitting at 1 while the table contains ids up to several million. The first insert after cutover collides with an existing row, and so does every insert after that until somebody works out why. Fix it as an explicit step in the runbook, calling setval on each sequence from the maximum id in its table once the load finishes. This is the same class of problem that catches teams using native PostgreSQL logical replication, where sequence data is documented as not replicated, so the habit is worth building either way.
Run both in parallel, and how Adapters fits
The riskiest version of this migration is a single dump and restore inside a maintenance window, because if the row counts disagree at 3am your only option is to go back and try again next month. The safer shape is to load the history once, then keep replicating changed rows on a schedule while MySQL is still serving production. That gives you a Postgres copy that is minutes behind rather than weeks stale, so you can point read-only traffic at it, compare row counts and checksums per table over several days, and pick a cutover time on your terms. Adapters runs the ongoing half of that: a watermark on each table so only changed rows move, the type conversions above configured in a visual mapper, upserts on the primary key so a re-run is harmless, and per-record error logs listing the exact rows a cast rejected.
How it goes live
Three steps, minutes end to end, covered by flat data integration pricing from $49 a month.
STEP 01
Pick the pair
Connect MySQL and Postgres with scoped credentials. About a minute each.
STEP 02
Confirm the mapping
The cables above are pre-wired. Adjust any field, preview the transform on sample records, done.
STEP 03
Schedule the sync
Hourly down to every minute, with retries, alerting, and a full log on every run.
Prefer to understand the moving parts first? Our long-form guide to the MySQL to PostgreSQL migration guide covers the field-by-field detail, the failure cases, and what changes at volume.
MySQL to Postgres sync: common questions
How do I migrate a MySQL database to PostgreSQL?
Convert the schema, load each table once from a replica, then keep both in step with incremental loads while you port the application. Running the two in parallel is what lets you compare row counts and totals before cutover, instead of finding the gap afterwards.
What is the MySQL to PostgreSQL data type mapping?
TINYINT(1) becomes BOOLEAN, unsigned INT widens to BIGINT so large ids cannot overflow, DECIMAL keeps its declared precision as NUMERIC, DATETIME and TIMESTAMP normalize to TIMESTAMPTZ in UTC, and JSON becomes JSONB so you can index keys with GIN.
What breaks when you move MySQL queries to PostgreSQL?
Backtick quoting, case-insensitive string comparison, MySQL zero dates, GROUP BY that selects ungrouped columns, and functions like IFNULL and DATE_FORMAT. PostgreSQL is stricter on all of them, so a query that silently worked on MySQL raises an error rather than returning something approximate.
How long does a MySQL to PostgreSQL migration take?
A schema with a few dozen tables and no stored routines takes days. An application with hundreds of tables, triggers, and raw SQL scattered through the codebase takes a quarter, and most of that time goes on the queries rather than on moving rows.
How does the MySQL to Postgres sync work?
The MySQL to Postgres migration from Adapters copies MySQL tables into PostgreSQL, converts MySQL types to their Postgres equivalents, and keeps replicating changed rows on a schedule so you can run both databases in parallel and cut over when the row counts match. Field mapping is no-code, so try it against sample records in the live demo.
Is there a prebuilt MySQL connector for Postgres?
Yes. This MySQL to Postgres connector ships prebuilt: the field mapping is wired the moment you pick the pair, transforms are included, and you can try it against sample records in the live demo. No code or engineering sprint required.
How much does the MySQL Postgres integration cost?
Pricing is flat and monthly: Starter at $49, Growth at $149, Scale at $399. Every plan includes this pair, visual field mapping, and per-record logs. There are no per-task or per-row fees, so the bill stays the same as volume grows.
How often can Adapters sync MySQL to Postgres?
Hourly on Starter, every 5 minutes on Growth, and down to every minute on Scale. Failed records retry automatically with backoff, and alerting plus a full per-record log come standard on every run.
Do I need to write code to connect MySQL and Postgres?
No. Fields are auto-mapped the moment you pick the pair, and you can rewire any mapping visually before the first sync. Incremental runs read a MySQL updated_at watermark so only changed rows move; TINYINT(1) lands as Postgres BOOLEAN, unsigned INT widens to BIGINT so large ids do not overflow, DECIMAL keeps its declared precision as NUMERIC, DATETIME and TIMESTAMP normalize to TIMESTAMPTZ in UTC, invalid zero dates map to NULL instead of failing the load, JSON columns land as JSONB, and writes upsert with ON CONFLICT on the primary key so re-running the migration updates rows in place.
More pairs from the API connector library
Browse the full api connector library, or request a pair you do not see.
MySQL and Postgres, finally in agreement
Map the pair once and let it sync on schedule. Flat price from $49 a month, no per-task fees.
No credit card required.