MariaDB10 min read

MariaDB vs MySQL for Operators: The Differences That Actually Bite

MariaDB and MySQL forked over a decade ago and the operational differences are now real: incompatible GTID formats, JSON as a LONGTEXT alias, system-versioned tables, a bundled thread pool, and replication gotchas.

Ask whether MariaDB and MySQL are compatible and you will get answers rooted in 2012, when the honest answer was "basically yes." That was then. The two databases forked at MySQL 5.5, took separate paths through 5.6 and 5.7, and by the time MySQL 8.0 and MariaDB 10.6/11.x are on your estate they are different products that happen to share a wire protocol and a lot of syntax. Most days the differences are invisible. Then you try to replicate between them with GTID, or you rely on MySQL's binary JSON performance, or a distro swap quietly replaces one with the other, and the fork becomes the only thing that matters.

I run both. This is the list I give operators who have to do the same: not a feature checklist, but the differences that have actually cost me or my teams time. Treat this post as the standing reference; if you are in the middle of an actual move rather than operating both sides day to day, the companion runbook on migrating from MySQL to MariaDB walks the same ground as a step-by-step checklist.

GTID: same idea, mutually unintelligible formats

Both servers have global transaction identifiers, and they are not compatible. MySQL's GTID is built on the server UUID and looks like 3e11fa47-71ca-11e1-9e33-c80aa9429562:1-105. MariaDB invented its own format — domain_id-server_id-sequence, like 0-1-4237 — with its own semantics: the domain id exists so multi-source replication and multi-primary topologies can keep transaction streams distinct, and gtid_strict_mode enforces ordering within a domain. Neither server can consume the other's GTID stream. If you are replicating between MySQL and MariaDB, you are doing it with old-style file-and-position replication, and you are tracking positions by hand during failover like it is 2010.

-- MariaDB's GTID world:
SHOW GLOBAL VARIABLES LIKE 'gtid%';
SELECT @@gtid_current_pos, @@gtid_binlog_pos, @@gtid_slave_pos;

-- A MariaDB replica connecting by GTID:
CHANGE MASTER 'analytics' TO
  MASTER_HOST = '10.0.3.17',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'secret',
  MASTER_USE_GTID = current_pos;
START REPLICA 'analytics';
SHOW REPLICA 'analytics' STATUSG

Two footnotes worth knowing. MariaDB supports named replication connections (the 'analytics' argument above) for multi-source replication out of the box, which MySQL added differently via channels. And since MariaDB 10.5 the REPLICA terminology is native (START REPLICA, SHOW REPLICA STATUS) with the old SLAVE spellings kept as aliases — scripts written for one engine mostly parse on the other, but not always, and the GTID output columns are not among the compatible parts.

JSON: a real type vs a very good alias

This is the divergence with the most performance consequence. MySQL 5.7 introduced a native binary JSON type: documents are stored in a parsed binary form, and functions like JSON_EXTRACT read paths without re-parsing the whole document, with support for partial in-place updates of large documents. MariaDB looked at that implementation and declined it. In MariaDB, JSON is an alias for LONGTEXT with the utf8mb4_bin collation. Declare a column JSON and SHOW CREATE TABLE will show you longtext with a CHECK (json_valid(...)) constraint tacked on automatically. Every JSON function call parses text.

For typical payloads — a few kilobytes of metadata, extracted occasionally — you will never feel the difference, and MariaDB's JSON function coverage is broad (JSON_EXTRACT, JSON_QUERY, JSON_VALUE, JSON_TABLE for shredding documents into relational rows, and so on). For document-heavy workloads with hot paths doing thousands of path extractions per second against large documents, the difference is measurable and it does not flatter the text approach. The operational advice cuts both ways: do not cargo-cult MySQL's JSON performance assumptions onto MariaDB, and do not write off MariaDB's JSON as a toy — for the majority of schemas it is entirely adequate, and JSON_TABLE is genuinely pleasant.

-- What MariaDB actually created when you said JSON:
SHOW CREATE TABLE api_events;

-- Shredding a JSON array into rows with JSON_TABLE (MariaDB 10.6+):
SELECT e.id, jt.status, jt.amount
FROM api_events e
JOIN JSON_TABLE(
  e.payload,
  '$.items[*]' COLUMNS (
    status VARCHAR(20) PATH '$.status',
    amount DECIMAL(10,2) PATH '$.amount'
  )
) AS jt
WHERE e.created_at >= '2026-07-01';

Temporal tables: MariaDB has them, MySQL does not

MariaDB 10.3 shipped SQL-standard system-versioned tables. Add WITH SYSTEM VERSIONING to a table and every UPDATE and DELETE preserves the old row version transparently; you query history with FOR SYSTEM_TIME AS OF, BETWEEN, or ALL. It is the audit-trail, point-in-time-correction, and "what did this row look like when the invoice was generated" feature, implemented in the engine rather than in a tangle of triggers.

CREATE TABLE contracts (
  id        BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  customer  VARCHAR(200) NOT NULL,
  monthly   DECIMAL(10,2) NOT NULL
) WITH SYSTEM VERSIONING;

UPDATE contracts SET monthly = 189.00 WHERE id = 42;

SELECT id, monthly, row_start, row_end
FROM contracts FOR SYSTEM_TIME ALL
WHERE id = 42
ORDER BY row_start;

MySQL has no equivalent as of 8.4. If an application grows to depend on system versioning, that dependency is a one-way door: moving to MySQL later means reimplementing history by hand. Storage growth is the other side of the coin — every update copies the old row into history, and without a pruning strategy (partitioning the history by row_end, or periodic purges) the table grows forever. Temporal tables are one of my favorite MariaDB features and I still make teams write the retention plan before the CREATE TABLE.

Thread pool: bundled vs paywalled

MariaDB ships a real thread pool in the community server: set thread_handling=pool-of-threads and connections share a pool of worker threads (sized by thread_pool_size, defaulting to core count) instead of the one-thread-per-connection model. On workloads with thousands of mostly-idle connections — which is to say, most web applications behind a busy proxy tier — it measurably smooths latency by removing thread churn and scheduling storms. MySQL's equivalent thread pool plugin is Enterprise Edition only; community MySQL operators approximate it with ProxySQL multiplexing or by simply not having it. If high connection counts are part of your life, this is one of the strongest purely operational arguments for MariaDB, and it costs nothing but a config change and a rehearsal.

The accumulating small differences

None of these is individually dramatic, and together they are why "drop-in replacement" stopped being a safe sentence:

  • Sequences. MariaDB has CREATE SEQUENCE — real, first-class sequence objects. MySQL does not. Handy for gap-tolerant id schemes, and another portability door.
  • Authentication defaults. MySQL 8.0 defaults to caching_sha2_password; MariaDB defaults to mysql_native_password, commonly uses unix_socket auth for local root, and ships ed25519. Client libraries and connection strings that work against one can fail against the other, especially older connectors.
  • The sys schema. Both servers bundle it these days — MySQL since 5.7, MariaDB since 10.6 — but MariaDB's port tracks the MySQL 5.7-era version. The handy digest summaries like sys.statement_analysis work on both; views added in MySQL 8.0, and anything leaning on instrumentation MariaDB's Performance Schema does not have, will be missing. Check the runbook against the target server before you trust it.
  • InnoDB diverged too. MariaDB maintains its own InnoDB fork. Encryption is configured differently (MariaDB's innodb_encrypt_tables and key rotation plugins vs MySQL's keyring-based TDE), and instant ADD COLUMN is implemented with different on-disk formats — one of several reasons you cannot binary-copy datadirs between the two and expect success.
  • Tooling names. MariaDB 10.5+ renamed the client tools with mariadb- prefixes (mariadb-dump, mariadb-upgrade, mariadb-binlog) with symlinks for the old names. Automation that hard-codes mysql_upgrade mostly survives; automation that parses version strings sometimes does not.
  • sql_mode and defaults drift. Both are strict by default now, but not identically, and older applications sometimes lean on quirks that only one side kept.

Replication between them: possible, not casual

You can replicate MySQL to MariaDB (and often back) with file-and-position replication, and teams do it as a migration bridge all the time. But treat it as a bridge, not a topology. Beyond the GTID incompatibility: binlog event details drift with each release, features like MySQL's instant DDL or new JSON functions have no counterpart on the other side, and the further the versions diverge, the more you are relying on luck. My rule is that cross-engine replication gets a tested exit plan and a deadline, and the data path that matters goes through a logical dump or a change-data-capture tool that understands both dialects — never through an assumption of binlog compatibility in perpetuity.

The same caution applies to distro-driven surprises: more than one team has "upgraded MySQL" and discovered they now run MariaDB because the distribution switched packages. The check is one command — SELECT VERSION() — and it belongs in your inventory automation, because every difference in this post arrives unannounced with that swap.

Watching both sides honestly

Whichever side of the fork you operate, the failure signals are engine-specific: GTID position drift on MariaDB, binary JSON hot spots on MySQL, thread pool stalls on one and connection storms on the other. Generic "is the port open" monitoring misses all of it.

If this post reads like an argument for knowing exactly which engine you run and trending its engine-specific signals, that is because it is one. On the monitoring question I will be exact: MonPG monitors PostgreSQL today. It does not monitor MariaDB or MySQL yet — MariaDB support is in active development, coming soon, and tracked on the status page, and posts like this one are field notes from the research that is shaping it. If PostgreSQL is part of your estate, that workflow exists now — start with the PostgreSQL overview and the engine comparison, and find the rest of the series on the blog.