MySQL to PostgreSQL migration tools compared: converters, data type conversion, and the mappings that change your data
Twelve tools that move a MySQL database to PostgreSQL, and the part no feature list covers: the default type mappings that convert without an error and quietly alter what your columns mean. A TINYINT(1) becomes a boolean even though MySQL says display width does not constrain the values stored in it. A unique index that MySQL enforced case insensitively stops being unique. Both tables are below, with the one line queries that catch each one before the load.
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
Vendor documentation read 31 August 2026 · Last updated September 2026
Which MySQL to PostgreSQL migration tool should you use?
Start with pgloader. It is free, it runs as a single command, it converts the schema and loads the data in one pass, and its casting rules are published so you can override them. Add AWS Database Migration Service when you need the cutover measured in seconds instead of hours, because it can follow the MySQL binary log after the initial load. Use Debezium instead if you already run Kafka and want the change stream itself rather than a finished migration. Reach for DMS Schema Conversion when you want a formal assessment report to size the work before committing to a date. And if the MySQL instance is not actually going to switch off, which is the common outcome, you are buying an ongoing sync rather than a migration and should choose accordingly.
The correction worth making early: almost every page in this category ranks tools by how much of the schema they convert. That is the wrong question, because nearly everything in a MySQL schema is an ordinary table that every tool converts. The question that decides whether your migration succeeds is what each tool does with the dozen types where MySQL and PostgreSQL genuinely disagree, and whether it tells you. For the wider category see data migration tools, and for the ongoing case rather than the one time move, Postgres ETL tools.
MySQL to PostgreSQL migration tools compared
Pricing models rather than price tags, because every vendor here except us either quotes or meters, and any figure printed on this page would be stale within a quarter. Where a tool is wrong for a job, the last column says so.
| Tool | Owner | Approach | Best for | Pricing model | Watch out for |
|---|---|---|---|---|---|
| pgloader | Open source | One command that reads a MySQL schema, converts it with a documented casting ruleset and loads the data | The default choice for most MySQL to PostgreSQL migrations, and the fastest way to get a first result | Free | Its own reference states views are not migrated and triggers are not migrated. Override the TINYINT rule before the first run |
| AWS DMS | Amazon Web Services | Managed replication instance: full load, then change capture from the MySQL binary log | Cutting over to RDS or Aurora PostgreSQL with seconds of downtime rather than a weekend | Per hour of replication instance, plus storage and data transfer | Needs binlog_format ROW, binlog_row_image Full and binlog_checksum NONE. Cannot capture when binlogs are not on standard block storage |
| DMS Schema Conversion | Amazon Web Services | Converts the schema and produces an assessment report listing every object it could not convert | Sizing the conversion work before you commit to a cutover date | No license charge, you pay for the infrastructure it runs against | Read the conversion settings page before you accept the generated DDL. Defaults are choices, not facts |
| AWS SCT | Amazon Web Services | Downloadable schema conversion tool, the desktop predecessor to the console experience | Converting offline, or against database versions the console does not cover | Free download | AWS is steering new work towards DMS Schema Conversion. Check which one your version is supported by |
| Debezium | Open source, Apache 2.0 | Kafka Connect source connector that turns MySQL binary log events into a change stream | Teams already running Kafka who want the stream rather than a one-time move | Free, you pay for the Kafka platform underneath it | It emits change events. Landing them in PostgreSQL correctly is a second component you build and own |
| Google Cloud DMS | Google Cloud | Managed migration into Cloud SQL and AlloyDB, with a conversion workspace for heterogeneous pairs | MySQL to Cloud SQL for MySQL, and Oracle or SQL Server into PostgreSQL | Heterogeneous migrations are charged, homogeneous ones are not | Google documents its heterogeneous PostgreSQL paths from Oracle and SQL Server. Confirm the current pair list before planning MySQL to PostgreSQL on it |
| Azure DMS | Microsoft | Managed migration into Azure Database for PostgreSQL, paired with Data Factory for orchestration | Estates already standardized on Azure | Service tiers plus the compute the migration runs on | Source coverage varies by pair and changes. Confirm the current support matrix rather than assuming parity with AWS |
| Striim | Striim | Streaming change capture with in-flight processing between MySQL and PostgreSQL | Continuous replication where the MySQL instance is staying online long term | Subscription, quoted by capacity | Priced as a streaming platform, which is heavy if all you need is one cutover |
| Qlik Replicate | Qlik | Log-based change capture across many source and target engines | Enterprises replicating several legacy sources into PostgreSQL at once | Subscription, quoted | Moves rows well and converts no application code at all |
| Fivetran | Fivetran | Managed connectors that replicate a MySQL source into an analytics destination | Analytics replication rather than making PostgreSQL your production database | Metered by monthly active rows | Built to feed a warehouse. It is the wrong shape for an application cutover |
| Airbyte | Airbyte | Open source and hosted connectors, including MySQL source and PostgreSQL destination | Teams who want to self-host the pipeline and keep the bill predictable | Cloud metered by credits, open source free to run on your own infrastructure | A replication tool, not a schema converter. It will not rewrite your types for you |
| Adapters | Adapters | Field-level mapping between MySQL, PostgreSQL and 50 other systems, with the backfill and the ongoing sync using one mapping | Migrations where the old MySQL does not actually switch off, and both databases have to stay in agreement | Flat from $49 a month, not metered by rows | We are not a bulk schema converter. For a 4,000 table lift and shift, run pgloader or DMS first and use us for what stays connected |
Three of the twelve are free, which is unusual for a software category and worth saying plainly. If budget is the reason you are reading a comparison table, the answer is that pgloader, Debezium and AWS SCT cost nothing to license, and the money in a MySQL to PostgreSQL migration goes to engineer time rather than to tooling. Our own price is flat and published, which is why ours is the only figure on this page.
MySQL to PostgreSQL data type conversion, and the mapping that looks right
Most type mapping tables published for this pair list a MySQL type and a PostgreSQL type and stop. That is the easy half. The column that matters is the third one, because these are the mappings a converter picks by default, and every one of them produces a schema that deploys without a single error.
| MySQL type | Target we would defend | The mapping that looks right | What it costs you |
|---|---|---|---|
| TINYINT(1) | smallint, unless you have proved every row is 0 or 1 | boolean | The pgloader reference documents the rule "type tinyint to boolean when (= 1 precision)". The MySQL manual states that display width "does not constrain the range of values that can be stored in the column", so a TINYINT(1) legally holds -128 to 127. Every row holding 2 or 7 arrives as true. |
| BIGINT UNSIGNED | numeric(20,0) | bigint | MySQL BIGINT UNSIGNED reaches 18,446,744,073,709,551,615. PostgreSQL bigint stops at 9,223,372,036,854,775,807 and PostgreSQL has no unsigned integer types, so there is no larger integer to promote into. |
| INT UNSIGNED | bigint | integer | MySQL INT UNSIGNED reaches 4,294,967,295. PostgreSQL integer stops at 2,147,483,647. Slightly over half the declared range does not fit, which is fine until the table is big enough to reach it. |
| DATETIME holding 0000-00-00 | timestamp, nullable, with the zero rows converted to NULL first | timestamp NOT NULL | PostgreSQL has no zero date. The pgloader rule that rescues this fires when the column DEFAULT is the zero date. Rows containing zero dates in a column with an ordinary default are not covered by it. |
| TIMESTAMP | timestamptz | timestamp, the same target as DATETIME | MySQL converts TIMESTAMP to UTC on write and back to the session time zone on read. DATETIME does neither. Mapping both to the same PostgreSQL type erases a distinction the application was relying on. |
| ENUM | text with a CHECK constraint, or a PostgreSQL enum type | varchar with no constraint | You keep every value and lose the validation. The application then writes a value MySQL would have rejected, and nothing complains until a report groups by that column. |
| SET | a junction table, or text[] | varchar | MySQL stores a SET as a comma joined string. Flattening it to varchar preserves how it looks and destroys the ability to query membership, so every filter has to become a LIKE. |
| DECIMAL(m,d) on a money column | numeric(m,d) | double precision | Row counts match, totals do not. Binary floating point cannot represent most decimal fractions exactly, so the sum of a million invoice lines drifts by an amount finance will find before you do. |
| AUTO_INCREMENT column | identity or serial, with setval run to the current maximum at cutover | identity or serial left at its initial value | The schema deploys, the data loads, every existing row is correct, and the first insert after go-live raises a duplicate key error on the primary key. |
| YEAR | smallint | date | A YEAR is a year, not a day. Converting it to a date invents a month and a day that were never in the source, and those invented values then flow into every date comparison downstream. |
| Columns in the utf8 character set | UTF8, after checking what was already lost | UTF8, without checking | Nothing breaks on the way across. MySQL utf8 is a deprecated alias for utf8mb3, the three byte encoding, which cannot store a four byte character at all. Anything outside the Basic Multilingual Plane was lost before the migration started. |
| DOUBLE used for a quantity | numeric, or double precision if it really is a measurement | double precision by reflex | This one is often correct. It is on the list because it is the mapping people override wrongly after being burned by DECIMAL. Measurements belong in floating point. Money does not. |
The first row is the one to sit with, because it is a documented default resting on a MySQL feature MySQL itself has deprecated. The pgloader reference lists the rule plainly. The MySQL manual, in the same words on the numeric type attributes page, says display width "does not constrain the range of values that can be stored in the column" and that support for it "should be expected to be removed in a future version of MySQL". So the rule reads a hint that means nothing about the data, and turns a column that may hold 127 distinct values into two. The same construct by construct treatment for the other big commercial to Postgres route is on SQL Server to PostgreSQL migration tools, and for Oracle in Oracle to PostgreSQL migration tools. If the destination under consideration is a warehouse rather than another operational database, the same TINYINT(1) question is answered differently again on MySQL to Snowflake migration tools, where the column arrives as an integer instead of a boolean.
Eight failures that report success
A MySQL to PostgreSQL migration rarely fails loudly. It converts cleanly, loads cleanly, reconciles on row counts, and is wrong. Every row here is documented behavior from a primary manual rather than folklore, and the last column is the query that catches it while it is still cheap.
| Failure | What you see | Cause | The check that catches it |
|---|---|---|---|
| Boolean flattening | Every non-zero value reads as true, and nobody notices until a count is wrong | The converter mapped TINYINT(1) to boolean on display width, which MySQL says does not constrain the stored range and has deprecated | Run SELECT DISTINCT col FROM t on the source. Anything other than 0 and 1 means the column is not a boolean |
| Unsigned overflow | A handful of rows fail to load, or the load succeeds and the largest identifiers are wrong | BIGINT UNSIGNED mapped to PostgreSQL bigint, which has half the positive range and no unsigned variant to fall back on | SELECT MAX(col) on the source and compare it against 9,223,372,036,854,775,807 before the load, not after |
| Uniqueness quietly widens | Duplicate accounts and duplicate SKUs appear weeks after a clean cutover | MySQL 8.4 defaults to utf8mb4_0900_ai_ci, which is accent insensitive and case insensitive. PostgreSQL default collation is neither, so a unique index that MySQL enforced across case now allows both spellings | Count grouped by lower(col) on the source and compare it to the plain row count. A gap is the number of collisions you are about to permit |
| Zero dates | The load rejects a small number of rows, or NULLs appear where dates used to be | With strict mode off MySQL converts an invalid date to 0000-00-00 and only raises a warning. PostgreSQL has no such value and refuses it | SELECT count(*) WHERE col < DATE('1000-01-01') on every date column before you start planning the cutover |
| Sequences start at one | Everything works until the first insert after go-live, which fails on the primary key | The identity column was created without setting its sequence to the current maximum from the source | Run setval on every sequence as the final step of cutover, then insert and roll back one row per table as a smoke test |
| Totals drift | Row counts reconcile exactly and financial dashboards move by a rounding amount | A DECIMAL money column landed in double precision, where most decimal fractions have no exact representation | Checksum the SUM of every money column on both sides. Row counts prove nothing about values |
| Sort order changes | Reports and paginated lists come back reshuffled with no code change | The MySQL default collation sorted case insensitively. PostgreSQL sorts by its own collation, so uppercase and lowercase interleave differently | Run the same ORDER BY query on both databases and diff the first two hundred rows |
| Grouped queries start erroring | Application errors on reporting screens immediately after cutover | SQL written against MySQL 5.7 defaults, where ONLY_FULL_GROUP_BY was off. It is on by default in MySQL 8.4 and PostgreSQL has always enforced it | Replay the application query log against the new database in a staging environment before the cutover, not during it |
Notice how many of those are collation rather than data. MySQL 8.4 ships utf8mb4_0900_ai_ci as the default, which sorts and compares without regard to case or accent. PostgreSQL does neither by default. That single difference changes uniqueness, sort order and the result of every equality comparison on a text column, and it does not produce one error message anywhere in the migration. The same class of problem across other engines is covered in data migration tools and change data capture tools.
The limits that decide the plan
18,446,744,073,709,551,615
The maximum value of a MySQL BIGINT UNSIGNED. PostgreSQL bigint stops at 9,223,372,036,854,775,807, and PostgreSQL has no unsigned integer types at all.
MySQL 8.4 and PostgreSQL manuals, read 31 August 2026
-128 to 127
The range MySQL says a TINYINT(1) column can hold, because display width "does not constrain the range of values that can be stored". Converters map that column to a boolean.
MySQL 8.4 Reference Manual, read 31 August 2026
ai_ci
MySQL 8.4 defaults to the utf8mb4_0900_ai_ci collation: accent insensitive and case insensitive. PostgreSQL default collation is neither.
MySQL 8.4 Reference Manual, read 31 August 2026
ROW
The binlog_format AWS DMS requires on a MySQL source, alongside binlog_row_image set to Full and binlog_checksum set to NONE, before change capture will work.
AWS DMS user guide, read 31 August 2026
3 bytes
What the MySQL utf8 character set actually is: a deprecated alias for utf8mb3, which cannot store a four byte character such as an emoji at all.
MySQL 8.4 Reference Manual, read 31 August 2026
0
Views and triggers migrated by pgloader. Its own reference states both are out of scope, because supporting them would mean parsing the full SQL dialect.
pgloader reference documentation, read 31 August 2026
The unsigned numbers are the ones to check first, because they are arithmetic rather than judgement. If any BIGINT UNSIGNED column on your source holds a value above 9,223,372,036,854,775,807, PostgreSQL has no integer type that can take it and the column has to become numeric. That is a five second query on the source and it is the difference between finding out now and finding out during the load window.
How to migrate MySQL to PostgreSQL in six steps
-
01
Inventory the types before you pick a tool
Query information_schema.columns on the source and group by data type. You are looking for four things: unsigned columns, TINYINT columns with a display width of 1, ENUM and SET columns, and every date column. That single query tells you more about how hard this migration will be than any vendor assessment report, and it takes a minute.
-
02
Prove the data, not the schema
For each TINYINT(1) run SELECT DISTINCT. For each unsigned column run SELECT MAX. For each date column count the rows before the year 1000. For each unique index count the rows grouped by lower(). Every one of those is a one line query and each one prevents a class of failure that otherwise surfaces after go-live.
-
03
Convert the schema with the casting rules written down
Run pgloader with an explicit CAST block rather than accepting the defaults, or run DMS Schema Conversion and read its settings page before accepting the generated DDL. Commit the resulting DDL to your repository. A schema conversion you cannot reproduce from a file is a schema conversion you cannot review.
-
04
Load, then reconcile on values rather than counts
Do a full load into an empty PostgreSQL database. Then compare, per table, the row count, the SUM of every numeric column, the MIN and MAX of every date column, and a hash of the primary keys. Row counts alone will reconcile perfectly on a migration that has quietly changed every money column.
-
05
Turn on change capture and let it catch up
Set binlog_format to ROW, binlog_row_image to Full and binlog_checksum to NONE, then start the change stream from the snapshot position. Watch the lag fall towards zero. This is the step that turns a weekend of downtime into a cutover of seconds, and it is the step most plans leave until too late to test.
-
06
Replay the application before you switch it
Point a copy of the application at PostgreSQL and replay real queries from the MySQL query log. This is where the case sensitivity, the grouping rules and the implicit casts surface. Finding them here costs an afternoon. Finding them after the DNS change costs a rollback.
Who moves from MySQL to PostgreSQL, and why
Escaping a per core licence
The commercial reason most of these projects get funded. The database work is the visible part and the application query rewrite is the part that decides the date.
Consolidating onto one engine
Teams running MySQL for the application and PostgreSQL for analytics usually end up wanting one. Migrating the smaller side is cheaper than maintaining two dialects forever.
Moving to a managed PostgreSQL
RDS, Aurora, Cloud SQL and AlloyDB all take PostgreSQL. The migration is the price of admission to the operational model you actually wanted.
Needing constraints MySQL was not enforcing
If the reason for moving is that bad data keeps arriving, expect the load itself to fail first. That is the migration doing its job.
Keeping both databases live
The common outcome nobody plans for. A department, a reporting tool or an old integration keeps MySQL alive, and the one time move becomes an ongoing sync.
Feeding a warehouse on the way past
Several teams take the opportunity to land the same data in Snowflake or BigQuery while the mapping work is already open in front of them.
Where we are the wrong tool
A comparison page written by a vendor is worth reading only if the vendor says where they lose. Five cases where you should use something else.
- A pure lift and shift of a 4,000 table schema. Run pgloader or DMS Schema Conversion. That is what they are built for and we are not going to pretend otherwise.
- Converting stored procedures and triggers. Nobody converts these well, ourselves included, and any vendor claiming otherwise is selling you a review job you have not budgeted.
- Petabyte scale bulk loading. If the constraint is raw throughput into an empty database, a native bulk loader beats a mapping layer every time.
- Air gapped environments with no outbound network. We are a hosted service. If nothing may leave your network, this is the wrong shape of product.
- One off migrations that genuinely finish. If MySQL really does switch off next month, you do not need an ongoing sync and you should not pay for one.
Four questions to ask any migration vendor
Show me your default type mapping, in writing
Any tool worth using publishes its casting rules. pgloader does. If a vendor cannot show you what their converter does with TINYINT(1), BIGINT UNSIGNED and a zero date, they are asking you to trust an undocumented transformation of your production data.
What happens to views, triggers and stored procedures?
The honest answer from most tools is nothing. pgloader states it outright. If a vendor implies these convert automatically, ask for the assessment report on your actual schema before you sign anything, not a demo schema.
How do you reconcile, and on what?
If the answer is row counts, keep looking. Row counts reconcile perfectly on a migration that has rounded every money column and flattened every boolean. Ask for value level checks: sums, hashes and distinct counts per column.
What does this cost if the project runs three months long?
Per hour and per row pricing both grow with delay, and migrations are the projects most likely to slip. Ask the vendor to price the overrun case rather than the happy path, because the overrun case is the one you are more likely to live in.
Related migration and integration guides
MySQL is one route into PostgreSQL among several. These cover the neighboring engines, the ongoing sync case, and the cost model behind a migration program.
Data migration tools
The pillar above this page: twelve tools, type mapping traps and cutover methods.
Convert MySQL data types to PostgreSQL
Type by type, with the pre-flight query that proves each mapping is safe.
MySQL to Postgres migration guide
The walkthrough version: replication setup, cutover sequencing and rollback.
Oracle to PostgreSQL migration tools
The other commercial escape route, where PL/SQL rather than types dominates.
Postgres ETL tools
Once the data is in PostgreSQL, the tools that keep loading into it.
What a data migration really costs
Nine pricing models scored on how each behaves when the project overruns.
Questions buyers ask about MySQL to PostgreSQL migration
- How do I migrate a MySQL database to PostgreSQL?
- Convert the schema first, then move the rows, and treat those as two separate pieces of work. pgloader does both in one command for small and medium databases. For a low-downtime cutover, convert the schema with pgloader or DMS Schema Conversion, then run a full load followed by binlog-based change capture until the lag is near zero. The step teams skip is auditing the type mapping before the load, which is where the damage happens.
- What is the best MySQL to PostgreSQL migration tool?
- pgloader for most migrations, because it is free, it runs as a single command, and its casting rules are documented and overridable. AWS Database Migration Service when you need change capture and a cutover measured in seconds rather than hours. Debezium when you already run Kafka and want the change stream itself. The important part is not which tool you pick, it is whether you override the default type mapping before the first load.
- Can MySQL be converted to PostgreSQL automatically?
- The tables and the data convert automatically. The parts that do not are views, triggers, stored procedures and anything relying on MySQL-specific behavior. The pgloader documentation states plainly that views are not migrated and triggers are not migrated, because supporting them would require parsing the whole SQL dialect. Budget hand conversion for every stored routine and every application query that assumed MySQL semantics.
- How long does a MySQL to PostgreSQL migration take?
- Estimate from the application, not the database. Copying rows is bounded work that pgloader or AWS DMS finishes in hours for most databases. Rewriting the queries the application sends is unbounded, because MySQL and PostgreSQL disagree about case sensitivity, grouping, implicit casts and date handling. A schema with a thin ORM layer over it can move in a two weeks. A codebase full of hand-written MySQL SQL is a quarter.
- Is PostgreSQL compatible with MySQL?
- They both speak SQL and disagree on the details that matter. PostgreSQL has no unsigned integer types, no ENUM in the MySQL sense, no SET type, no zero dates, and a default collation that is case sensitive where MySQL 8.4 defaults to case insensitive. Ordinary SELECT and INSERT statements move without friction. Anything relying on MySQL being permissive will behave differently, and usually without an error.
- What data types do not convert from MySQL to PostgreSQL?
- SET has no equivalent and becomes either a junction table or an array. Unsigned integers have no equivalent because PostgreSQL has no unsigned types, so each one has to be promoted a size, and BIGINT UNSIGNED has nowhere left to be promoted to except numeric. Zero dates such as 0000-00-00 are not valid PostgreSQL values at all. ENUM converts, but only if you also carry across the constraint that made it useful.
- How much does a MySQL to PostgreSQL migration cost?
- The tooling is usually free and the labor never is. pgloader costs nothing, DMS Schema Conversion carries no license charge, and AWS Database Migration Service bills by the hour of replication instance. What you actually pay for is engineer time spent auditing type mappings, rewriting application queries and running both databases in parallel until you trust the new one. The tool is the smallest line in the budget.
- Can you migrate MySQL to PostgreSQL with zero downtime?
- With change data capture, yes, down to a cutover of seconds. Load a consistent snapshot into PostgreSQL, then stream the MySQL binary log until the target has caught up, then switch the application. AWS Database Migration Service needs binlog_format set to ROW, binlog_row_image set to Full and binlog_checksum set to NONE on the source. Miss any of those and you get a full load with no changes following it.
- Why is PostgreSQL better than MySQL?
- For the migrations people actually run, the reason is strictness. PostgreSQL refuses data that does not fit rather than adjusting it, which is why a MySQL database that has been running in a permissive mode often will not load without cleaning first. That is the migration made harder and the next five years made easier. If your reason for moving is licensing or hosting rather than correctness, say so, because it changes what you optimize for.
- What goes wrong in a MySQL to PostgreSQL migration?
- The conversions that succeed and change your data. A TINYINT(1) column becomes a boolean even though MySQL says display width does not constrain the stored range. A unique index that MySQL enforced case insensitively becomes case sensitive, so duplicates that could never exist before start appearing. A sequence starts at 1 instead of the current maximum. None of those raise an error.
For the people cost that dominates every migration program, read what a data migration really costs. For the difference between moving data once and moving it continuously, see ETL vs ELT.
Migrate off MySQL once, then keep PostgreSQL in agreement with everything else
Map the fields once, run the backfill, then let the same mapping run incrementally with retries, alerts and per-record logs. From $49 a month, not metered by rows.
No credit card required.