/

Data Infrastructure

MySQL to Postgres: Migration and Replication Guide

Intergalactic Data Labs

—

—

14 min read

Table of contents

Summarize

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.

version: 1

sources:
  orders_mysql:
    type: mysql
    config:
      connection_method: url
      dsn: env:MYSQL_DSN
      replication: cdc

sinks:
  reporting_pg:
    type: postgres
    config:
      connection_method: url
      dsn: env:POSTGRES_DSN
      schema: mysql_replica

pipelines:
  orders-replica:
    source:
      ref: orders_mysql
    sink:
      ref: reporting_pg
    resources:
      - customers
      - orders
      - order_items
    write_mode

version: 1

sources:
  orders_mysql:
    type: mysql
    config:
      connection_method: url
      dsn: env:MYSQL_DSN
      replication: cdc

sinks:
  reporting_pg:
    type: postgres
    config:
      connection_method: url
      dsn: env:POSTGRES_DSN
      schema: mysql_replica

pipelines:
  orders-replica:
    source:
      ref: orders_mysql
    sink:
      ref: reporting_pg
    resources:
      - customers
      - orders
      - order_items
    write_mode

version: 1

sources:
  orders_mysql:
    type: mysql
    config:
      connection_method: url
      dsn: env:MYSQL_DSN
      replication: cdc

sinks:
  reporting_pg:
    type: postgres
    config:
      connection_method: url
      dsn: env:POSTGRES_DSN
      schema: mysql_replica

pipelines:
  orders-replica:
    source:
      ref: orders_mysql
    sink:
      ref: reporting_pg
    resources:
      - customers
      - orders
      - order_items
    write_mode

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

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

Stay up to date with what we’re building

Stay up to date with what we’re building

Stay up to date with what we’re building

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?

Own your knowledge stack

Own your knowledge stack

Copyright © 2026 Galaxy. All rights reserved.