Skip to content
adapters.io

MongoDB to MySQL migration tools, converters and sync software, and the milliseconds MySQL rounds away

Ten ways to move MongoDB collections into MySQL, compared on what actually lands. The surprise on this route is on the MySQL side: a DATETIME declared without a precision rounds every BSON date to the second, and MySQL's own manual says no warning or error is given.

The live demo needs no card, and Starter is $49 a month.

Field mapping auto-plugged · tap a port to rewire

5 sample records ready

Vendor documentation read 29 September 2026

Which MongoDB to MySQL migration tool should you use?

Pick by what has to land in MySQL. For a one-time move inside AWS, DMS supports this exact pair. For a few small collections, MySQL Shell's importJson is free. For an ongoing sync into typed tables, use a tool that lets you declare the columns. Whatever you choose, write the MySQL DDL yourself first: declare DATETIME(3) for dates and DECIMAL for money, because the defaults round milliseconds and turn Decimal128 into strings, silently.

One MongoDB order document arriving in MySQL three ways: one JSON column, a flattened wide row, or typed parent and child tables. MongoDB order document One JSON column DMS default, Shell default One wide flat row row limit, 64-char names orders + order_items typed, DATETIME(3), DECIMAL
The same document, three landing shapes. Only the bottom one is ready for SQL reporting.

This route sits under our data migration tools guide. If PostgreSQL is still on the table as the target, the same document problems with a different set of type rules are compared on MongoDB to PostgreSQL migration tools.

Eight documented defaults between MongoDB and MySQL

Each row quotes the vendor's own documentation. Most comparison pages on this route stop at the MongoDB side. The first three rows are on the MySQL side, and two of them come from MySQL's own manual and MySQL's own import tool, which is why nobody thinks to question them.

Tool and setting What its documentation says Why it matters to a buyer What we would ship
MySQL DATETIME, no precision given MySQL: "If omitted, the default precision is 0." Inserting a value with more fractional digits "results in rounding", and "No warning or error is given when such rounding occurs." Every BSON date carries milliseconds. Declare a plain DATETIME and each one rounds to the nearest second, so 23:59:59.600 on the last day of a quarter lands in the next quarter. Event ordering inside the same second is lost. Nothing in any log says so. Declare DATETIME(3) for every date column
MySQL Shell importJson, new table A new target table is created as a JSON column named doc plus an auto-increment id. BSON extensions are converted only "if you specify the convertBsonTypes option"; otherwise documents import "in the same way as they are represented in the input file." The import succeeds and every document sits in one JSON column with its $oid and $date wrappers still inside. Joins compare wrapper objects rather than ids. It is MySQL's own tool, so teams trust the default. Pass convertBsonTypes, then project columns
MySQL Shell, Decimal128 With convertBsonTypes, a decimal becomes "a string representation of the decimal value". The only alternative option, decimalAsDouble, converts it to the MySQL DOUBLE type. Neither default lands a DECIMAL. A string sorts 100.00 before 20.00; a DOUBLE rounds currency. Money fields are exactly where Decimal128 is used, so this is the row finance notices first. Cast to DECIMAL(19,4) in the projection
AWS DMS, default mode DMS documents that in document mode "the document data is consolidated into a single column named _doc", and that "Document mode is the default setting". The migration completes with every document in one column. Row counts match and the project has not started, because nothing in MySQL is typed yet. Choose the landing shape before run one
AWS DMS, table mode Table mode turns each top-level field into a column, scanning 1,000 documents by default, and "If there are fields that don't exist in the target, those fields aren't replicated." Ten million documents get their schema from the first thousand. A field added by a later release never becomes a column and is never copied. Inventory every field with an aggregation
Airbyte and Estuary MySQL destinations Airbyte: enable local_infile "as the connector uses LOAD DATA LOCAL INFILE". Estuary: "The local_infile global variable must be enabled". MySQL: "By default, local_infile is disabled", and its manual lists the security risks of turning it on. Two popular pipelines need a server setting your security team switched off on purpose. On a locked-down production MySQL that is a change request, not a checkbox, and it belongs in week one of the plan. Clear local_infile with security first
Airbyte MySQL destination, names Airbyte "forces all identifier (table, schema and columns) names to be lowercase". Typing and deduping needs MySQL 8.0 or above. customerId and customerID in the same collection become one column name. Older MySQL 5.7 servers fall back to untyped loading. Rename to snake_case in the mapping
Fivetran MySQL destination Fivetran lists MySQL as a destination in Beta and writes that MySQL "is not appropriate as a data warehouse" and can "fail to perform basic queries for even medium volumes of data". The vendor is telling you what the destination is for. Operational copies and moderate reporting fit. A reporting warehouse on years of order history does not. Pick MySQL for apps, a warehouse for BI

The first row is the one to take away. A migration that rounds dates to the second passes every row count and every spot check, then disagrees with the source on the one report that matters: revenue by day, month or quarter, where a handful of orders placed in the last half second before midnight UTC have moved across the boundary. The fix is one word in the DDL. For the full list of what change streams need switched on compared with binlog capture on MySQL itself, see change data capture tools.

BSON types in MySQL, and the wrong target a default picks

A MongoDB to MySQL converter is mostly a type converter. The second column shows how mongoexport writes each type in Extended JSON. The last column is the mistake we see most often, and most of these produce no error at load time.

BSON type In Extended JSON Right MySQL target Common wrong target
ObjectId {"$oid": "..."} in both modes CHAR(24) ascii, or BINARY(12) The $oid wrapper left inside JSON
Date Relaxed ISO string; canonical $date DATETIME(3), stored as UTC DATETIME, which rounds to the second
Date before 1970 or after 2038 Same DATETIME(3), range 1000 to 9999 TIMESTAMP, range 1970 to 2038 UTC
Int64 (long) Relaxed plain integer; canonical $numberLong BIGINT INT, or a parser reading a double
Decimal128 {"$numberDecimal": "..."} DECIMAL(19,4) or your real scale A string, or DOUBLE
String with emoji UTF-8 string utf8mb4 column utf8mb3, the old utf8 alias
Long free text String TEXT or MEDIUMTEXT Wide VARCHARs that hit the row limit
Array of subdocuments JSON array Child table keyed on parent id and position Columns on the parent row
Embedded document JSON object Typed columns for used paths, JSON for the rest Flattened names past 64 characters
Field with mixed types Number in old documents, string in new JSON column plus a generated column that casts A typed column that rejects or coerces

Two MySQL limits catch teams on shape rather than values. An InnoDB table holds at most 1,017 columns, and a row, not counting TEXT and BLOB stored off page, has to fit in roughly half a 16 KB page. Flatten a wide collection into VARCHAR columns and the CREATE TABLE fails with error 1118, "Row size too large". Child tables for arrays and TEXT for long strings solve both. The relational version of this mapping, where the source already enforced types, is on MySQL to PostgreSQL migration tools, and the one-query checks that prove each document mapping is safe before you load are in our guide to converting MongoDB documents into relational tables.

MongoDB to MySQL migration tools and converters compared

Billing units rather than price tags, except where the vendor publishes the number itself, as with Fivetran's $5 base charge per connection. Everything else is quoted, metered in ways that depend on your data, or free. Where a tool is wrong for a job, the last column says so, including for us.

Tool Approach Best for Billing unit What to watch
AWS DMS Full load plus CDC, document or table mode One-time moves inside AWS with a cutover date Replication instance hours or serverless capacity Default lands _doc; table mode samples 1,000
MySQL Shell importJson Imports JSON files into a collection or table Small collections, one-off loads Free with MySQL Shell Lands one JSON column; decimals become strings
Airbyte Change stream source to MySQL destination Teams who want open source they can patch Free self-hosted, credits on cloud Needs local_infile; lowercases every name
Estuary Streaming capture to a MySQL materialization Low-latency continuous sync Data moved plus connector instances Needs local_infile enabled
Fivetran Managed connector to a Beta MySQL destination Teams already on Fivetran Monthly active rows, plus $5 base per connection Vendor says MySQL is not a warehouse
Hevo Managed pipeline with a mapping UI Small teams with no data engineer Events per month Document rewrites count as events
Debezium plus JDBC sink Change stream to Kafka to MySQL Organizations already on Kafka Infrastructure and engineering time Needs a flattening transform
Desktop converters GUI or CLI copy, collection to table A single analyst, a single load One-time license No change capture, no audit trail
Relational Migrator MongoDB's own tool, relational into MongoDB The opposite direction of this page Free Does not migrate MongoDB to MySQL
Adapters Declared path to column mapping, incremental sync, per-record logs Typed MySQL tables on a flat bill Flat monthly from $49, not metered by rows Not a sub-second streaming platform

One row deserves a second look. MongoDB Relational Migrator is the tool many teams find first, because it comes from MongoDB and has "relational" and "migrator" in its name. It migrates from relational databases, MySQL included, into MongoDB, which is the opposite direction to this page. For what a cutover of this size costs once the tool is chosen, see what a data migration really costs, and for keeping MySQL fed after the move, our guide to MongoDB to MySQL sync software.

Six MySQL numbers worth knowing before you sign anything

0

Default fractional-second precision of a MySQL DATETIME. BSON dates carry milliseconds, and MySQL rounds them with no warning.

MySQL 8.4 manual, read 29 Sep 2026

2038

The year a MySQL TIMESTAMP runs out, on 19 January. A contract end date or a birth date in MongoDB can fall outside that range.

MySQL 8.4 manual, read 29 Sep 2026

1,017

Columns an InnoDB table can hold. Flattening every path of a wide collection into one table reaches it faster than teams expect.

MySQL 8.4 manual, read 29 Sep 2026

~8 KB

InnoDB row size on the default 16 KB page, not counting off-page TEXT and BLOB. Wide VARCHAR flattening fails with error 1118.

MySQL 8.4 manual, read 29 Sep 2026

64

Characters in a MySQL column name. Flattened paths like billing_address_geo_coordinates_latitude get there quickly.

MySQL 8.4 manual, read 29 Sep 2026

OFF

Default for local_infile in MySQL 8. Airbyte and Estuary both require it on to load MySQL.

MySQL, Airbyte and Estuary docs, read 29 Sep 2026

Eight ways this migration goes wrong while every check stays green

MongoDB never enforced a shape and MySQL enforces every one, so you would expect errors. Mostly you get none, because MySQL rounds, coerces and deduplicates quietly, and the pipelines are built to keep going.

The failure What you see What is actually happening
Milliseconds rounded away Load clean, no warnings Plain DATETIME has precision 0. Every BSON date rounds to the nearest second, which moves late-evening records into the next day, month or quarter.
Dates that fall off TIMESTAMP Job finished TIMESTAMP stops at 2038 and starts at 1970. Values outside it become zero dates in non-strict mode, or reject the row in strict mode.
Time zone shifted on read Values look right in the console TIMESTAMP converts from the session time zone to UTC and back. Two clients in different zones read two different times for one row.
Decimals landed as strings All rows present MySQL Shell turns Decimal128 into a string. Sorting and SUM() on the column behave oddly and nothing errors.
The key JSON dropped Insert succeeded When a key repeats in a document, MySQL keeps the last value and discards the earlier ones, with no trace left in the table.
The field the sample never saw Counts match Schema built from 1,000 or 10,000 documents. A field that appears later never became a column and was never copied.
Two fields, one lowercase column Every sync green A destination that lowercases names turns customerId and customerID into one column, and the second write wins.
Arrays multiplying rows Totals higher than source Line items flattened onto the order row turn one order into many rows, and revenue is overstated while the pipeline behaves as configured.

Five of the eight are fixed in the DDL before the first row moves. The other three need a field inventory before the load and a reconciliation after it. If MySQL is a stop on the way to a warehouse, the same analysis for the warehouse leg is on MySQL to Snowflake migration tools.

Six steps that decide whether this migration works

Step 1

Inventory every field, not a sample

Run an aggregation over each full collection listing every field, how often it appears and which BSON types it holds. It takes minutes and replaces the 1,000 or 10,000 document guess that DMS and Airbyte make by default.

Step 2

Write the MySQL DDL yourself

Decide each table and each type before any tool touches the target: DATETIME(3), BIGINT, DECIMAL with a real scale, utf8mb4 everywhere, TEXT for long strings. A tool that creates tables for you creates them with its defaults, and the defaults are what this page is about.

Step 3

Give arrays their own tables

An order with an array of line items becomes an orders table and an order_items table keyed on order id plus position. Retrofitting child tables after reports exist on a flattened shape is the most expensive change on this route.

Step 4

Keep the whole document once

Store the original document in a JSON column beside the typed ones during the migration. When a report disagrees with the source, you can see exactly what arrived, and you can add a generated column later instead of re-running the load.

Step 5

Full load, then change capture to cutover

Load history once, then follow the change stream until the cutover date so the switch takes minutes. That needs a replica set or a sharded cluster. A standalone mongod has no change stream, so converting it is the first ticket.

Step 6

Reconcile counts, NULLs and date boundaries

Compare document counts with row counts, the number of documents holding each field with the non-NULL count in its column, and daily totals around midnight UTC. The last check is the one that catches rounded timestamps.

Why US teams fund this project

Consolidating on the MySQL you already run

A US SaaS team with a MySQL core and one MongoDB service bolted on later, cutting a database engine to reduce hosting and on-call load before a renewal date.

Finance reporting that needs joins

Orders live in MongoDB, invoices and customers in MySQL. Finance wants one SQL query across both instead of an export every month end.

An acquired product on MongoDB

The parent company runs MySQL or Aurora MySQL. The acquired app's documents have to fit an existing relational model and its constraints.

Audit and compliance evidence

Auditors want foreign keys and a schema they can read. A typed MySQL model makes controls over financial records far easier to evidence.

Keep MongoDB, feed MySQL

The app stays on MongoDB while a MySQL copy updated by change capture serves an admin panel, a partner portal or a legacy PHP service.

Leaving a self-hosted replica set

Three MongoDB nodes nobody wants to patch, next to a managed MySQL instance that already has backups, monitoring and a DBA.

Five jobs where you should not pick us

A comparison page that never says the competition wins is an advert. These are the cases where something else on this page is the right answer.

  • This is a one-time move inside AWS with a fixed cutover. DMS already supports this source and target pair and bills by the hour. Design the tables first, reconcile after, and you do not need a subscription.
  • The whole job is three small collections for one analysis. MySQL Shell's importJson with convertBsonTypes, a JSON staging table and an afternoon of SQL will do it for nothing.
  • You need sub-second latency from a MongoDB write to a MySQL row. Streaming platforms built for that will beat our scheduled sync.
  • You already run Kafka and Debezium. A working change stream pipeline and a JDBC sink covers this route, and a vendor adds little.
  • The real goal is analytics across years of history. Put the data in a columnar warehouse instead of MySQL, and pick a tool built for that job.

Four questions to ask any vendor on this list

Question 01

What precision do you give date columns?

Ask for the DDL the tool will create for one real collection. If a date column is DATETIME with no precision, every millisecond is rounded away and MySQL will not warn you. The right answer is DATETIME(3) or wider.

Question 02

What do you need changed on our MySQL server?

Two popular destinations need local_infile turned on, which MySQL ships disabled for security reasons. Find out in week one whether your security team will allow it, because it can decide the tool.

Question 03

How do you decide which fields become columns?

If the answer is a sample of the first thousand or ten thousand documents, ask what happens to a field that appears later. Two well-known tools skip it or write NULL by default.

Question 04

What happens to arrays and deep paths?

A child table, columns named _0 and _1, a flattened name past 64 characters, or JSON left in place. Only one of those is what a reporting team can use without rework.

Questions buyers ask about MongoDB to MySQL migration

How do I migrate data from MongoDB to MySQL?
List every field in every collection with an aggregation, decide per collection whether documents become typed tables, a JSON column, or both, then run a full load and follow the change stream until cutover. Declare DATETIME(3) for every date before the first load, because MySQL rounds milliseconds away by default and says nothing when it does.
What is the best MongoDB to MySQL migration tool?
For a one-time move inside AWS, DMS supports MongoDB as a source and MySQL as a target and bills by the hour. For an ongoing sync with typed columns, a mapped sync fits better. MySQL Shell's importJson is the cheapest correct answer for a few small collections. Choose by what must land in MySQL, not by connector count.
Is there a MongoDB to MySQL converter?
Yes, of three kinds. Data converters move documents: DMS, MySQL Shell, Airbyte, Estuary and desktop tools. Query converters rewrite find() calls as SQL. Nothing converts aggregation pipelines automatically. MongoDB Relational Migrator, which people often find first, moves data in the opposite direction, from relational databases into MongoDB.
Can MySQL store MongoDB documents as JSON?
Yes. MySQL has a native JSON type stored in a binary format with direct lookups by key or array index. A document is limited by max_allowed_packet, JSON columns cannot be indexed directly, so you index a generated column, and when a key repeats MySQL keeps the last value and discards the earlier ones without an error.
How do I sync MongoDB to MySQL in real time?
Read the MongoDB change stream and apply each insert, update and delete to MySQL as an upsert or delete. Change streams exist only on replica sets and sharded clusters. Estuary and Debezium do this continuously, DMS does it until cutover, and scheduled tools such as ours apply changes every few minutes instead of every second.
Can AWS DMS migrate MongoDB to MySQL?
Yes. MongoDB is a supported DMS source and MySQL a supported target. Document mode is the default and puts each whole document into a single _doc column. Table mode turns top-level fields into columns but infers them from the first 1,000 documents by default, and fields missing from the target are not replicated.
How do I convert MongoDB to SQL tables?
Give each collection a table, each frequently queried field a typed column, and each array of subdocuments its own child table keyed on the parent id plus position. Keep the rest of the document in a JSON column. Map ObjectId to CHAR(24), dates to DATETIME(3), and Decimal128 to DECIMAL with an explicit scale.
Is MySQL a good destination for MongoDB analytics?
For operational reporting on a modest dataset, yes. For analytics at scale, Fivetran's own MySQL destination page warns that MySQL "is not appropriate as a data warehouse" and lists the destination as Beta. If the goal is BI across large history, a columnar warehouse such as Snowflake or BigQuery is the better target.
How much does a MongoDB to MySQL migration cost?
Moving the data is rarely the large line. MySQL Shell, mongoexport and Debezium cost nothing but time, DMS bills by instance hour, and managed pipelines meter rows, events or gigabytes. The real cost is designing the relational model and rewriting the queries and aggregation pipelines, which no tool on this page does for you.

For the wider vendor landscape see the best data integration tools, and for the sync that follows the migration, read how to choose MongoDB to MySQL sync software.

Land MongoDB documents in MySQL as typed tables

Declare the document paths that matter, set DATETIME(3) and DECIMAL once, send arrays to child tables, then let the same mapping sync incrementally with retries and per-record logs. From $49 a month, not metered by rows.

The live demo needs no card, and Starter is $49 a month.

Get started