MySQL to Postgres: Migration and Replication Guide

Intergalactic Data Labs
—
—
14 min read
As of September 2026 there are three ways to move MySQL to Postgres. A bulk loader like pgloader copies schema and data once. A binlog CDC tool (Debezium, AWS DMS, Estuary, pg_chameleon, Filament) snapshots the tables and streams changes. A hosted ELT service like Fivetran or Airbyte does the same on a meter. Three questions decide. Can the app stop writing, must deletes propagate, and may data leave your network.
Approach | What you get | Deletes | Where it runs | Pricing unit |
|---|---|---|---|---|
Bulk loader | One copy, schema included | Not needed | Your machine | Free |
Binlog CDC | Snapshot plus live changes | Yes, via log | Self-hosted or hosted | Free or per hour |
Hosted ELT | Snapshot plus polling or CDC | Depends on plan | Vendor cloud | Rows or GB |
How does MySQL to Postgres replication work?
Every approach does two jobs. It copies the tables as they stand, then it either stops or keeps Postgres current. How it keeps Postgres current is the whole difference.
The binlog is what makes a live replica possible. MySQL records every row change in its binary log. The MySQL manual calls it "a record of the data changes to be sent to replicas." A CDC tool connects as if it were a replica, reads insert, update, and delete events, and applies them to Postgres. MySQL 8.0 enables binary logging by default in the row-based format, which every log-based tool needs.
Change data capture, or CDC, is the practice of reading that change log instead of re-querying tables. The alternative is an incremental read, which filters each table on a timestamp column. The Filament replication modes page states the trade plainly, an incremental route "is lighter than rereading the table, but it cannot see a hard delete."
What decides which MySQL to Postgres tool to use?
Five criteria settle the choice. Rank them by which one would hurt most if it went wrong.
Downtime. If the app can stop writing for the copy, a bulk loader is enough. If it cannot, the snapshot and the change stream must share one consistent point.
Deletes. A timestamp-based sync never sees a hard delete. Only binlog CDC or a soft-delete flag keeps Postgres honest about removed rows.
Type fidelity. MySQL has unsigned integers, zero dates, TINYINT(1) booleans, and inline ENUMs. Postgres has none of those, so the tool must rewrite types or fail loudly.
Restart cost. A copy that dies at 80 percent should resume, not restart. Resume needs primary keys and saved checkpoints.
Where the data may run. A hosted service moves rows through the vendor's cloud. A self-hosted tool keeps them inside your network.
Binlog retention sits under all of these, because a CDC tool must finish its snapshot before the captured position expires. MySQL's default retention is 30 days, but Amazon RDS "uses a parameter called binlog retention hours which defaults to 0," per Airbyte's MySQL docs, so RDS users raise it first.
Which tools move MySQL to Postgres?
Eleven tools run this route. The table lists how each handles deletes, where it runs, its license as stated in its LICENSE file, and its pricing unit.
Tool | Deletes | Deployment | License | Pricing unit | Maturity |
|---|---|---|---|---|---|
Filament | CDC merge | Binary, Go library, Helm | Apache 2.0 | None | MySQL source beta, Postgres sink beta |
pgloader | Not applicable | CLI or JAR | PostgreSQL | Free | Stable, v4 rewrite |
AWS DMS | CDC | AWS hosted | Proprietary | Instance hour or DCU | GA |
Debezium | CDC via Kafka or Server | Kafka Connect or Debezium Server | Apache 2.0 | Free | Stable |
Estuary Flow | CDC, hard delete optional | Hosted, private, BYOC | BSL | Per GB plus per connector | GA |
Fivetran | Soft delete flag | Hosted | Proprietary | Monthly active rows | GA |
Airbyte | CDC | Hosted or self-hosted | ELv2 | Credits | Certified source |
pg_chameleon | CDC | Python daemon | BSD | Free | v2.0.20 |
dlt | Not for MySQL | Python library | Apache 2.0 | Free, dltHub paid | Verified source |
Sling | CDC, paid tier | CLI | GPL-3.0 | Free CLI, Pro for CDC | Stable |
ingestr | CDC | CLI | FSL-1.1 | Free | v1.1.57 |
pgloader is the tool most migration guides recommend, and for a one-time copy it earns it. Its README promises "a full migration from MySQL, including schema definition (tables, indexes, foreign keys, comments) and parallel loading of the corrected data." It rewrites zero dates and unsigned integers by default. Its limitations page says views and triggers are not migrated, and nothing in its docs reads the binlog, so it is a copy, not a replica.
AWS DMS is the default for teams already on AWS. It reads the binlog for CDC and bills by the hour. Two documented limits matter. "The AUTO_INCREMENT attribute on a column isn't migrated," and on the Postgres target sequences are not migrated either, so you reset NEXTVAL by hand at cutover.
Debezium is the reference implementation of log-based CDC. Its MySQL connector reads the binlog and emits change events to Kafka, and the JDBC sink writes them into Postgres with upsert semantics. That path needs Kafka and Kafka Connect. Debezium Server drops Kafka but still runs the sink as a separate component. You get the most battle-tested change stream at the price of running the most pieces.
Fivetran has the widest connector catalog and a mature MySQL source that pulls a full dump and then connects to the binary log. Deletes arrive as a _fivetran_deleted flag rather than removed rows, and the meter is monthly active rows, which grows with your data. The Postgres destination is hosted, with a Hybrid option on Enterprise plans.
Airbyte has a certified MySQL source with binlog CDC, and its Postgres destination now writes "directly to final tables without using intermediate raw tables," per its docs. The same page warns that Postgres is "not a data warehouse" and suggests small volumes. The LICENSE is Elastic License 2.0 and cloud billing is by credits.
Estuary Flow captures MySQL via the binary log, backfills first, then streams, and its Postgres materialization can hard-delete rows when the option is on. It is hosted and priced per GB plus a monthly fee per connector.
pg_chameleon is the purpose-built replicator for this exact pair. It pulls row images from the binlog into jsonb and replays them with a PL/pgSQL function, per its docs, and can detach the replica for a minimal-downtime migration. It is a single-purpose Python daemon with a small maintainer base.
dlt, Sling, and ingestr round out the list. dlt's sql_database source reads MySQL by query and has no binlog source. Sling reads the binlog but CDC requires a CLI Pro Max token. ingestr, now a Go binary, streams the binlog under the mysql+cdc:// scheme, per its README, and its LICENSE is the Functional Source License 1.1, not Apache 2.0.
Where does Filament fit for MySQL to Postgres?
Filament is an Apache 2.0 replication engine written in Go, and Galaxy builds it. Measured against the five criteria above, it answers four with documented behavior and leaves type fidelity to Postgres's own casting.
Downtime. The MySQL source bootstraps CDC from a consistent snapshot. For a table with no checkpoint, "the run first reads it in full through a consistent snapshot," and the stream "continues from the binlog position captured just before that snapshot opened." One pipeline gives you the baseline and the stream from one exact point.
Deletes. The Postgres sink offers cdc_merge, which keeps current state, so in the docs' words "inserts and updates upsert, and deletes delete." Rows removed in MySQL are removed in Postgres, not flagged.
Restart cost. Snapshots are keyset reads that save the last key, so an interrupted copy resumes. Large tables split into as many as 64 primary-key ranges read in parallel. CDC runs stream from the checkpoint to the current binlog position and exit, so continuous replication is a cron entry.
Integrity and locality. The integrity page says that before every write "the pipeline calculates a CRC32-C checksum over the batch's Arrow buffers," the sink recalculates it, and a mismatch fails the run. Filament runs as one binary, a Go module inside your service, or a Helm chart, so rows never leave your network and there is no per-row meter.
Maturity and speed deserve two plain sentences. The MySQL source and Postgres sink are both beta today, per the connector overview, and the public benchmark cohort has no MySQL to Postgres run, so this route carries no speed claim.
On Postgres to Postgres the same engine moved 298,270,427 rows in 114.83 s, the fastest tool measured in that cohort, but that number belongs to that route only.
How to replicate MySQL to Postgres with Filament
What you need. MySQL 8.0 or later with InnoDB tables, a user holding REPLICATION SLAVE, REPLICATION CLIENT, SELECT, and LOCK TABLES, and binlog retention that covers the snapshot. Install the CLI with curl -fsSL https://getgalaxy.io/filament/install | sh or brew install galaxy-io/tap/filament, per the installation page. The local CLI uses file-backed SQLite and needs no other service.
The CLI page documents a declarative file at ~/.config/filament/filament.yaml with a Postgres source. The block below is that example adapted to MySQL, with field names taken from the MySQL source and Postgres sink pages.
Set MYSQL_DSN to a mysql:// URL and POSTGRES_DSN to a Postgres URL, then run the pipeline.
What the first run produces. For each table, Filament creates a typed table in the mysql_replica schema with the source primary key and NOT NULL constraints. It captures the binlog position, opens the snapshot, copies rows through Postgres binary COPY, then applies changes that landed during the copy. Rows carry _filament_ columns with the run ID, operation, and source position. filament run ls orders-replica lists every run with per-resource row and byte tallies.
Keeping it current. Add a schedule to the pipeline, and each run catches up from the saved GTID or file position and exits. A run with no changes still advances the watermark. Delivery is at-least-once and merge converges by primary key, so a crash mid-transaction replays that transaction safely. server_id defaults to 62347 and must be unique among replicas.
What breaks in a MySQL to Postgres migration?
The data copy rarely fails. The types and names do. These are the traps every guide on this route documents, with the fix for each.
Trap | MySQL behavior | Postgres behavior | Fix |
|---|---|---|---|
Mixed-case names | Column names case-insensitive | Unquoted names fold to lowercase | Lowercase everything or quote everywhere |
Zero dates | Stores 0000-00-00 | Rejects it | Convert to NULL during load |
TINYINT(1) | Used as boolean | smallint unless cast | Cast to boolean per column |
Unsigned INT | Up to 4,294,967,295 | integer stops at 2,147,483,647 | Map to bigint |
AUTO_INCREMENT | Column attribute | Identity column or sequence | Recreate and reset the sequence |
ENUM | Inline per column | Separate CREATE TYPE | Create a type or use text |
TIMESTAMP zone | Stored as UTC, shown in session zone | timestamp ignores zone | Prefer timestamptz |
Identifiers cause the most application bugs. Postgres folds unquoted identifiers to lowercase, and the manual notes that FOO, foo, and "foo" are the same name while "Foo" is different. MySQL column names are not case-sensitive on any platform. Decide once, lowercase or quoted, and apply it to schema and queries together.
Types need a decision, not a default. The Postgres numeric types have no unsigned variants, so unsigned INT must widen to bigint. MySQL's BOOL is a synonym for TINYINT(1), so a loader that does not know your intent produces a smallint. Check these columns before the first load, because a type change after data lands means a rewrite.
Deletes and replays are where replication goes wrong. An incremental sync silently keeps deleted rows. A CDC tool that saves its position before the write commits can skip a transaction on crash. Filament saves the CDC position "only after a successful commit," per its runs and recovery page. A column-count mismatch after DDL fails the run rather than mis-mapping values, so an incompatible ALTER TABLE means a re-snapshot of that table.
Which MySQL to Postgres option fits which job?
Pick by the constraint that breaks first.
Job | Pick | Why |
|---|---|---|
Live replica inside your network | Filament | Snapshot plus binlog from one point, merge deletes, checksummed batches, no meter |
One-time copy, downtime allowed | pgloader | Schema and data in one command, type casts built in |
Everything already on AWS | AWS DMS | Managed binlog CDC billed by the hour |
Kafka already in production | Debezium | The change stream is already there |
No software to run, budget by rows | Fivetran or Estuary | Hosted CDC with a meter |
Minimal-downtime cutover, Python shop | pg_chameleon | Detachable replica built for this pair |
For the main reader, a team that wants a current Postgres copy of production MySQL without sending rows through a vendor, Filament is the pick. It answers downtime, deletes, restart cost, and locality with documented behavior and runs as one process. A single copy with the app stopped is a pgloader job. A team that will not operate software should take a hosted CDC service and budget for the meter.
Related guides
What Is Change Data Capture (CDC)? The Complete Guide explains the log-reading model this route depends on.
Full Load vs Incremental vs CDC, Which to Use covers the read and write modes in depth.
Best CDC Tools in 2026 ranks the tool landscape.
Postgres Replication Benchmark, 7 Tools Tested on 298M Rows holds the numbers cited above.
The MySQL to Snowflake replication guide uses the same source connector.
Zero-Downtime Postgres Migration, Step by Step covers the cutover recipe.
Introducing Filament is the launch post.
Frequently asked questions
What is the easiest way to migrate MySQL to PostgreSQL?
For a one-time copy with downtime, pgloader is the shortest path. One command reads the MySQL schema and data, rewrites the types, and loads Postgres through COPY. If the application must keep writing during the move, you need a binlog CDC tool that takes a snapshot and then streams changes until you cut over.
Can MySQL replicate to PostgreSQL continuously?
Yes. MySQL writes every row change to its binary log, and a CDC tool reads that log as if it were a replica and applies inserts, updates, and deletes to Postgres. Debezium, AWS DMS, Estuary, pg_chameleon, and Filament all work this way. The binlog must be in ROW format and retained long enough to cover the initial snapshot.
What SQL breaks when moving from MySQL to PostgreSQL?
Unquoted mixed-case identifiers, because Postgres folds them to lowercase while MySQL keeps column names case-insensitive. Zero dates, which Postgres rejects. TINYINT(1) used as a boolean. Unsigned integers that overflow the signed Postgres range. AUTO_INCREMENT, which needs an identity column or a sequence on the Postgres side.
How do you map MySQL unsigned integers to PostgreSQL?
Postgres has no unsigned types. An unsigned INT tops out at 4,294,967,295, which does not fit a signed integer, so map it to bigint. pgloader does this by default with its unsigned integer to bigint cast rule. An unsigned BIGINT has no exact home and usually lands in numeric.
Do I need binlog enabled to replicate MySQL to Postgres?
Only for CDC. MySQL 8.0 enables binary logging by default in ROW format, which is what every log-based tool needs. Retention defaults to 30 days, but on Amazon RDS it defaults to zero hours and must be raised. A bulk copy or a timestamp-based incremental sync does not touch the binlog.
Does incremental sync from MySQL capture deletes?
No. An incremental read filters on a cursor such as updated_at and cannot see a hard delete, because the deleted row is gone. If deleted rows must disappear from Postgres, use binlog CDC with a merge write, or a tool that soft-deletes with a flag column.
How do I set up MySQL to Postgres replication with Filament?
Create a MySQL source with replication set to cdc, a Postgres sink, and a pipeline that lists the tables with write mode merge. The first run snapshots each table and continues from the binlog position captured just before the snapshot. Later runs catch up from the saved checkpoint, so the pipeline can run on a cron.
Does Filament preserve MySQL primary keys and types in Postgres?
The Postgres sink creates one typed table per source table, keeps primary keys and NOT NULL constraints, and maps logical types to native Postgres types such as bigint, timestamptz, jsonb, and bytea. Schema evolution is add-only, so new source columns appear and type changes are not applied.
Which tool is best for MySQL to Postgres?
It depends on the constraint that breaks first. For a one-time copy with downtime, pgloader. For a live replica that runs inside your own network with deletes propagated and every batch checksummed, Filament fits, with both connectors at beta. For a team that will not run software at all, a hosted CDC service such as Fivetran or Estuary.
More articles
Questions
Answered
FAQ
What does Galaxy do?
What is Filament?
What is enterprise context management?
What does working with Galaxy look like?
How do you handle security and compliance?
Why does Galaxy build in the open?
Company
Copyright © 2026 Galaxy. All rights reserved.