MariaDB7 min read

Migrating from MySQL to MariaDB: What Actually Breaks

The MySQL-to-MariaDB path is a logical migration, not an in-place upgrade. What breaks — auth plugins, JSON semantics, GTID, collations — and the checklist to run before you switch.

Teams arrive at MariaDB by two roads. Some plan the migration for quarters. Others wake up to it: a distro upgrade swapped the packages, and the thing answering on port 3306 is MariaDB now. I have been paged for both. The surprises are the same set either way, and the "drop-in replacement" story — roughly true in the 5.5 era — is not true between MySQL 8.0 or 8.4 and MariaDB 10.6, 10.11, or the 11.x series. The fork is old, and the two databases are different products now.

Here is what actually breaks, ordered by how often I have seen each one bite: the migration path itself, authentication, JSON semantics, GTID and replication, defaults and collations, and system tables — plus the pre-flight checklist I run before any switch. If you want the wider engine-by-engine picture first, the comparison pages are a good map; the day-two operational differences between the two engines get their own piece, while this one stays a migration runbook.

First truth: no in-place upgrade from MySQL 8.0

You cannot point MariaDB at a MySQL 8.0 data directory. The redo log format differs, MySQL 8.0's transactional data dictionary is its own world, and even the implementations of instant ADD COLUMN diverged years ago. The supported path is logical: dump with mariadb-dump or mysqldump — or mydumper and myloader when the data is large enough that single-threaded restore time matters — and load into MariaDB. Rehearse it with production-scale data; the restore time you measure is your cutover window.

If that window is too big, the escape hatches are application-level dual writes or a change-data-capture pipeline. Standing MariaDB up as a replica of MySQL 8.0 failed for years, for reasons the GTID section covers; only recent maintenance releases restored a restricted form of that path, and even there it is a migration bridge, not a topology.

Authentication is the first thing that breaks

MySQL 8.0 made caching_sha2_password the default authentication plugin, and most of the MariaDB line has no answer to it: the 10.6 and 10.11 LTS releases do not implement it, and only the recent 11.4 series added a compatible plugin. Unless you have verified your exact target version and driver stack, on MariaDB you recreate users with mysql_native_password or ed25519. Client drivers that added sha2 support still speak native auth fine, but anything that introspects plugins — user-management scripts, some proxy configurations — needs adjusting.

Two packaged-MariaDB traps deserve special mention. First, on Debian and Ubuntu, root@localhost is set up as unix_socket or mysql_native_password with a deliberately invalid stored password: you are root if you are the root OS user, and no password works until an administrator sets one with ALTER USER. Automation that logs in as root with a password fails the night of the swap. Second, since MariaDB 10.4, mysql.user is a view over mysql.global_priv. Scripts that INSERT or UPDATE the grant tables directly break; use CREATE USER, ALTER USER, and GRANT, which is what you should have been doing anyway.

-- inventory on the MySQL side:
SELECT user, host, plugin FROM mysql.user ORDER BY user, host;

-- recreate explicitly on MariaDB:
CREATE USER IF NOT EXISTS 'app'@'10.0.%'
  IDENTIFIED VIA mysql_native_password USING PASSWORD('change-me');
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app'@'10.0.%';

JSON is not the same JSON

MySQL has a native binary JSON type that normalizes documents on the way in: keys are sorted, duplicates removed, whitespace discarded. MariaDB's JSON is an alias for LONGTEXT with a utf8mb4_bin collation plus an automatic CHECK constraint using json_valid — the document is stored verbatim, as text. Both give you JSON_EXTRACT and JSON path expressions, but MariaDB has no equivalent of MySQL's inline path operators (-> and ->>); queries written with them need rewriting around JSON_EXTRACT and JSON_UNQUOTE. Everything looks compatible until the differences surface.

Three of them matter. First, information_schema reports the column as longtext, so ORMs and schema-diff tools see type drift where none was intended. Second, equality semantics differ: MySQL compares normalized binary documents, so two textual variants of the same JSON are equal; MariaDB compares the stored text, so a document with an extra space is a different value. Queries that lean on whole-document equality — joins, DISTINCT, GROUP BY over JSON values — silently change meaning. Third, function coverage and performance differ by version — JSON_VALUE has existed since 10.2, JSON_OVERLAPS arrived in 10.9 — and MariaDB parses text on every access where MySQL walks a binary structure. Find your JSON columns before you move:

SELECT table_schema, table_name, column_name, data_type
FROM information_schema.columns
WHERE data_type IN ('json', 'longtext')
  AND table_schema NOT IN ('mysql', 'sys', 'information_schema', 'performance_schema')
ORDER BY table_schema, table_name, ordinal_position;

GTID and replication: plan a cutover, not a chain

The GTID formats are incompatible. MySQL identifies transactions as UUID plus sequence intervals; MariaDB uses domain-server-sequence triplets like 0-1-548921. There is no supported GTID replication between them. File-and-position replication from MySQL 8.0 also failed outright for years, and only recent maintenance releases — 10.6.21, 10.11.11, 11.4.5 and later — reintroduced it, with documented restrictions around authentication, JSON events, and binlog compression. Useful as a cutover bridge on a verified version, but the plan stays the same: treat the migration as a dump-and-restore cutover with a rehearsal and a rollback plan — keep the MySQL side intact until MariaDB has survived a full business cycle — and use CDC or dual writes if your downtime budget is smaller than your restore time.

Defaults and collations that quietly change results

Both databases are strict by default now, but their default sql_mode lists are not identical. The subtle one: MySQL enables ONLY_FULL_GROUP_BY and MariaDB's default does not. Queries that MySQL rejected as ambiguous run on MariaDB with any-value-per-group semantics — that is wrong results, not errors, which is the worst kind of difference. Set sql_mode explicitly and identically on both sides during rehearsal.

Collations bite in the dump itself. utf8mb4_0900_ai_ci, MySQL 8.0's default, does not exist on the LTS versions most migrations target — MariaDB added UCA-9.0 compatibility collations, mapped onto its UCA-14.0 implementation, in 11.4.5, but on 10.6, 10.11, and earlier 11.x a dump full of it fails at restore. Map to utf8mb4_general_ci or the UCA-1400 collations (available since MariaDB 10.10, so absent from the 10.6 LTS), then re-check anything whose correctness leaned on comparison rules: unique indexes over case-insensitive text, ordering-dependent features. And check explicit_defaults_for_timestamp on both sides before assuming your TIMESTAMP columns will behave the same way.

SELECT @@global.sql_mode;
SELECT @@global.explicit_defaults_for_timestamp;

SELECT collation_name, is_default
FROM information_schema.collations
WHERE collation_name LIKE 'utf8mb4%'
ORDER BY collation_name;

The pre-flight checklist

  • Rehearse the restore with production-scale data and time every phase; that is your cutover window and your rollback deadline.
  • Diff SHOW GLOBAL VARIABLES between source and target, and set explicitly whatever your application implicitly depends on.
  • Inventory plugins with SHOW PLUGINS. Password validation, audit logging, and keyring plugins have different names and different maturity on each side.
  • Recreate users and grants with DDL, never by copying grant tables.
  • Map the 0900 collations in the dump before restore, and spot-check sort order in the application.
  • Exercise the application against the restored copy: JSON comparisons, GROUP BY behavior, TIMESTAMP defaults, stored routines and events with DEFINER clauses.
  • Keep the MySQL side untouched until the MariaDB side has survived a full business cycle, including its busiest day.

After the move: MonPG (coming soon)

A migration like this one is not finished at cutover; it is finished when the new engine has survived its busiest day with numbers to prove it. That post-migration baselining problem is one of the use cases driving MonPG's MariaDB support: query-level evidence before and after the switch — changed plans, collation costs, lock behavior — rather than dashboards of vanity metrics. The honest status: MonPG monitors PostgreSQL today, and the MariaDB side is still being built. The MariaDB monitoring (coming soon) page says where that work stands. Until it ships, capture a baseline on the MySQL side before you cut over — top queries, latency distributions, error rates — so the comparison exists when you need it. And if part of the estate is moving to PostgreSQL instead, that path is covered today; start at the PostgreSQL overview.