Whether it's an ongoing feed for reporting or a one-time cutover, Data Sync moves MySQL rows into Postgres with a schedule and a mapping you can see, no Docker container to babysit.
No credit card. 14 days. Cancel in one click.
Quick answer: Add MySQL and Postgres connections under Databases, then in Pipelines → Build, start a New Sync with MySQL as the source and Postgres as the target. Map fields, pick Insert, Update, or Upsert, and Dry Run before saving it. Pipelines tier; for a one-time migration, run it once instead of scheduling it, and re-add app-level connections by hand afterward.
A working MySQL connection and a working Postgres connection, both added under Databases, and a Postgres table already created with the columns you're syncing into. Pipelines tier.
"MySQL to Postgres sync" usually means one of two different things, and they call for different setups. One is an ongoing sync, keeping a Postgres copy current for a team that's standardized on Postgres for reporting while the app still runs on MySQL. The other is a one-time migration, moving off MySQL for good. QueryFlow handles the first well: build the sync once, schedule it, done. For the second, treat the sync as a bulk data mover and be honest that connections, users, and any MySQL-specific SQL in the app itself get re-added or rewritten by hand afterward — Data Sync moves rows, it doesn't migrate an application's dependency on MySQL.
A team running a MySQL-backed app wants a nightly copy of its customers table in Postgres, where the rest of their reporting already lives. The MySQL source query:
SELECT id, email, plan_tier, created_at, updated_at FROM customers WHERE updated_at >= NOW() - INTERVAL 1 DAY;
The Postgres target, a plain schema-qualified table reference, no special quoting needed unless a column or table name is mixed-case or reserved:
public.customers
Map id, email, plan_tier, and the two timestamp columns, set MODE to Upsert, and MATCH ON to id, since MySQL's auto-increment primary key and a Postgres integer primary key line up cleanly. Under the hood, QueryFlow implements Postgres Upsert with INSERT ... ON CONFLICT (id) DO UPDATE, which is the native way Postgres handles this rather than a separate delete-then-insert pass.
Dry Run first to confirm the row count and catch any type mismatch before it writes. After a live run, query the Postgres table directly and compare row counts and a handful of individual rows against the MySQL source, particularly any text columns using a different collation between the two engines.
| If you see | Fix |
|---|---|
| Records synced, with errors listed | Usually a type mismatch, most often a MySQL TEXT or ENUM column landing on a narrower Postgres column. Widen the target column or map to TEXT. |
| Duplicate key values in source for key column(s) … | Pick a MATCH ON column, or combination, that's unique per row in the MySQL query. |
| Choose at least one key field | Update and Upsert both require a MATCH ON column. Switch to Insert if you genuinely want every row appended, duplicates included. |
For an ongoing MySQL-to-Postgres feed at a small-to-medium scale, a scheduled sync covers the same ground a managed connector would, at a flat price instead of a metered one. What it doesn't do is streaming replication: Fivetran and Airbyte both offer log-based CDC options for MySQL that pick up row changes continuously rather than on an interval, which matters if downstream consumers need sub-minute freshness or if the source table sees enough write volume that even a five-minute batch window falls behind. For a nightly or hourly cadence feeding reports or a data warehouse, the interval-based approach here is simpler to reason about and doesn't require running or paying for a replication slot.
Fivetran meters this kind of connector by Monthly Active Rows: distinct primary keys touched by an insert, update, or delete in a calendar month, counted once regardless of how many times that row changes again. Fivetran's pricing page lists a $5 base charge per standard connection with usage billed across a 1–1,000,000 MAR band, and its free plan covers up to 500,000 MAR with no charge at all. A customers table with a few hundred thousand actively-changing rows a month can sit inside that free allowance; a busier one moves into paid MAR tiers that scale with row activity. QueryFlow Pipelines is $29.99/month or $199.99/year flat, with no MAR metering, so the price is the same whether the customers table has a thousand rows or several million — only the run time changes.
| MySQL type | Postgres type | Note |
|---|---|---|
| INT / BIGINT | INTEGER / BIGINT | Direct mapping. |
| DECIMAL(p,s) | NUMERIC(p,s) | Keep precision and scale identical to avoid silent rounding. |
| DATETIME | TIMESTAMP | MySQL's DATETIME has no time zone; confirm which zone your application assumes before mapping to a TIMESTAMPTZ column. |
| ENUM('a','b') | TEXT or a Postgres ENUM | A Postgres ENUM you've predefined preserves the constraint; TEXT is the simpler default. |
| TINYINT(1) | BOOLEAN | MySQL often uses TINYINT(1) as a boolean; map explicitly rather than leaving it as a small integer. |
| VARCHAR(n) | VARCHAR(n) or TEXT | Match the length constraint if it matters to your application, or widen to TEXT if not. |
The DATETIME-to-TIMESTAMP mapping is the one most worth double-checking: MySQL stores DATETIME values exactly as written with no zone conversion, while a Postgres TIMESTAMPTZ column stores everything normalized to UTC internally and converts on display. If the MySQL application writes local server time into a DATETIME column without any zone awareness, syncing it into a TIMESTAMPTZ column can shift the displayed time depending on the reading session's zone setting, unless you explicitly cast it in the source query first.
For an ongoing sync, an incremental WHERE clause on updated_at keeps each run's payload small regardless of the table's total size. For a one-time migration, run without that filter once to move the full table, verify counts on both sides, and only then decide whether to also stand up an ongoing incremental sync for the transition period before MySQL is fully decommissioned. Doing the full pull during low-traffic hours avoids adding read load to a production MySQL instance during business hours, particularly on a table with a large number of rows and no useful index on the column you're filtering by.
If this sync is feeding an ongoing reporting need rather than a one-time migration, check the Observatory's run history periodically for anything that started failing quietly, most often after someone adds a column to the MySQL side without updating the field mapping, or after a MySQL user's password rotates without the QueryFlow connection being updated to match.
A run that fails partway, say MySQL dropped the connection mid-query, shows up in the run history with the actual driver error rather than a generic failure message, and retrying re-issues the source query from scratch rather than attempting to resume a partial result set, so it's safe to simply retry once the underlying issue (a network blip, an expired credential) is resolved.
See also: Integrations Load a CSV into PostgreSQL. Excel to Postgres on Mac.
It's a sync engine. For an ongoing feed it's exactly what you want; for a one-time cutover off MySQL, use it to move the data, then re-add connections and any MySQL-specific SQL in the app by hand.
Those come through as their text representation. Map them to a Postgres TEXT or a matching Postgres ENUM you've created ahead of time if you want the constraint preserved.
Yes, set the job's trigger to Interval instead of Daily and pick how often it fires.
MySQL's TIMESTAMP columns are UTC-normalized by the server by default while Postgres stores what you send it plus an explicit zone if the column is TIMESTAMPTZ; check both column definitions before assuming they match, and convert in the source query if they don't.
No. Upsert only inserts new rows and updates matched ones; it never deletes. If you need the Postgres table to mirror deletions too, that has to be handled separately, for example by tracking a deleted_at column in MySQL.
14-day free trial, no card. Whether it's nightly or a one-time cutover.
No credit card. 14 days. Cancel in one click.