MySQL vs MariaDB

MySQLMariaDBDatabases

Your new project needs a relational database and you've settled on the MySQL family. Maybe your framework defaults to it, maybe your hosting provider supports it, maybe you've just used it before and it works. Then someone on your team asks: "Why not MariaDB? It's the same thing, right?"

It's not quite the same thing. MariaDB forked from MySQL in 2009 after Oracle acquired Sun Microsystems, and the two have been diverging ever since. MariaDB added sequences and system-versioned tables. MySQL doubled down on JSON support and enterprise tooling. They share a wire protocol and most of the SQL syntax, but they make different tradeoffs that start to matter once your project grows past a prototype.

This post puts both side by side with runnable code so you can see exactly where they agree and where they don't.

Contents

Quick Comparison

MySQLMariaDB
LicenseGPL v2 + proprietary (Oracle)GPL v2 (MariaDB Foundation)
GovernanceOracle CorporationMariaDB Foundation (community)
SequencesNo (use AUTO_INCREMENT workarounds)Yes, first-class CREATE SEQUENCE
System-versioned tablesNoYes, built-in temporal queries
JSON supportStrong (JSON_TABLE, multi-valued indexes, JSON_SCHEMA_VALID)Basic (JSON functions, no JSON_TABLE until 11.2)
Default storage engineInnoDBInnoDB (also ships Aria, ColumnStore)
Oracle compatibility modeNoYes (SET SQL_MODE='ORACLE')
Wire protocolMySQL protocolMySQL protocol (compatible)
Node.js drivermysql2mariadb (official connector)
Cloud availabilityEvery major provider (AWS RDS, GCP Cloud SQL, Azure)Fewer first-party managed options

The shared foundation means most ORMs, admin tools, and hosting providers treat them interchangeably. The differences emerge when you need a specific feature from one side.

Set Up Both Locally

We'll run both side by side using SpinDB. No Docker, no manual installs. (What is SpinDB?)

Install SpinDB:

bash
npm i -g spindb    # npm
pnpm add -g spindb # pnpm

Create both instances. SpinDB assigns non-conflicting ports automatically:

bash
spindb create mysql1 -e mysql --start
spindb create maria1 -e mariadb --start

Verify they're running:

bash
spindb url mysql1
spindb url maria1
text
mysql://root@127.0.0.1:3306
mysql://root@127.0.0.1:3307/test

Your ports may differ. SpinDB picks the next available port if the default is taken.

Or skip local setup entirely and spin up managed instances on Layerbase Cloud: MySQL or MariaDB.

Project Setup

MySQL and MariaDB have separate Node.js packages, so we need both drivers:

bash
mkdir mysql-vs-mariadb && cd mysql-vs-mariadb
pnpm init
pnpm add mysql2 mariadb
pnpm add -D tsx typescript

Create a file called compare.ts. All the code in this post goes into that file.

Side-by-Side: The Basics

Connect to both, create the same table, insert data, query it. The SQL is nearly identical; the driver APIs differ slightly.

typescript
import mysql from 'mysql2/promise'
import mariadb from 'mariadb'

// Connect to both
const my = await mysql.createConnection({
  host: '127.0.0.1',
  port: 3306,
  user: 'root',
  database: 'test',
})

const maria = await mariadb.createConnection({
  host: '127.0.0.1',
  port: 3307,
  user: 'root',
  database: 'test',
})

console.log('Connected to MySQL and MariaDB')

Now create identical tables on both:

typescript
const createProducts = `
  CREATE TABLE IF NOT EXISTS products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    sku VARCHAR(50) NOT NULL UNIQUE,
    price DECIMAL(10, 2) NOT NULL,
    category VARCHAR(100) NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`

// Clean up from previous runs
await my.execute('DROP TABLE IF EXISTS products')
await maria.query('DROP TABLE IF EXISTS products')

await my.execute(createProducts)
await maria.query(createProducts)

console.log('Created products table on both databases')

One driver difference to note: mysql2 uses .execute() for parameterized queries, while the mariadb connector uses .query() for everything. Both support ? placeholders.

Insert the same data into both:

typescript
const products = [
  { name: 'Mechanical Keyboard', sku: 'KB-001', price: 129.99, category: 'peripherals' },
  { name: '27" 4K Monitor', sku: 'MN-001', price: 449.99, category: 'displays' },
  { name: 'Standing Desk', sku: 'DK-001', price: 599.99, category: 'furniture' },
  { name: 'Wireless Mouse', sku: 'MS-001', price: 39.99, category: 'peripherals' },
  { name: 'Noise-Canceling Headphones', sku: 'HP-001', price: 249.99, category: 'audio' },
]

for (const p of products) {
  const params = [p.name, p.sku, p.price, p.category]
  const sql = 'INSERT INTO products (name, sku, price, category) VALUES (?, ?, ?, ?)'
  await my.execute(sql, params)
  await maria.query(sql, params)
}

console.log(`Inserted ${products.length} products into both databases`)

Query both and compare results:

typescript
const myProducts = (await my.execute(
  'SELECT name, price, category FROM products ORDER BY price DESC',
))[0] as Array<{ name: string; price: number; category: string }>

const mariaProducts = await maria.query(
  'SELECT name, price, category FROM products ORDER BY price DESC',
)

console.log('\nMySQL results:')
for (const row of myProducts) {
  console.log(`  ${row.name}: $${Number(row.price).toFixed(2)}`)
}

console.log('\nMariaDB results:')
for (const row of mariaProducts) {
  console.log(`  ${row.name}: $${Number(row.price).toFixed(2)}`)
}
text
MySQL results:
  Standing Desk: $599.99
  27" 4K Monitor: $449.99
  Noise-Canceling Headphones: $249.99
  Mechanical Keyboard: $129.99
  Wireless Mouse: $39.99

MariaDB results:
  Standing Desk: $599.99
  27" 4K Monitor: $449.99
  Noise-Canceling Headphones: $249.99
  Mechanical Keyboard: $129.99
  Wireless Mouse: $39.99

Identical output. For basic CRUD, JOINs, and aggregations, they're interchangeable. The differences show up when you reach for more advanced features.

What MariaDB Has That MySQL Doesn't

Sequences

MySQL limits you to AUTO_INCREMENT on a single column per table. MariaDB has standalone sequences: named counters that live independently of any table. Useful for order numbers, invoice IDs, or any sequential value shared across tables.

typescript
await maria.query('DROP SEQUENCE IF EXISTS invoice_seq')

await maria.query(`
  CREATE SEQUENCE invoice_seq
    START WITH 5000
    INCREMENT BY 1
`)

const invoiceNumbers = []
for (let i = 0; i < 4; i++) {
  const [row] = await maria.query('SELECT NEXT VALUE FOR invoice_seq AS num')
  invoiceNumbers.push(row.num)
}

console.log('\nMariaDB sequence-generated invoice numbers:', invoiceNumbers)
text
MariaDB sequence-generated invoice numbers: [ 5000, 5001, 5002, 5003 ]

In MySQL, you'd create a dedicated single-row table, lock it with SELECT ... FOR UPDATE, increment, and commit. A lot of ceremony for something that should be trivial.

System-Versioned Tables

This is MariaDB's strongest differentiator, and I think it's genuinely underrated. System versioning lets the database automatically track every historical version of every row. No triggers, no audit tables, no application code.

typescript
await maria.query('ALTER TABLE products ADD SYSTEM VERSIONING')

// Record the timestamp before changes
const [{ now: beforeChanges }] = await maria.query('SELECT NOW(6) AS now')

// Simulate a price increase on peripherals
await maria.query(`UPDATE products SET price = price * 1.15 WHERE category = 'peripherals'`)

// Query the table as it was before the update
const before = await maria.query(
  `SELECT name, price FROM products FOR SYSTEM_TIME AS OF ?
   WHERE category = 'peripherals' ORDER BY name`,
  [beforeChanges],
)

const after = await maria.query(
  `SELECT name, price FROM products WHERE category = 'peripherals' ORDER BY name`,
)

console.log('\nMariaDB temporal query - peripherals before price change:')
for (const row of before) {
  console.log(`  ${row.name}: $${Number(row.price).toFixed(2)}`)
}

console.log('\nPeripherals after price change:')
for (const row of after) {
  console.log(`  ${row.name}: $${Number(row.price).toFixed(2)}`)
}
text
MariaDB temporal query - peripherals before price change:
  Mechanical Keyboard: $129.99
  Wireless Mouse: $39.99

Peripherals after price change:
  Mechanical Keyboard: $149.49
  Wireless Mouse: $45.99

The FOR SYSTEM_TIME AS OF clause lets you query any point in the past. MySQL has no equivalent. You'd have to build a separate audit table, write triggers on every INSERT/UPDATE/DELETE, and write custom queries to reconstruct past states. MariaDB gives you this with a single ALTER TABLE statement.

This matters for compliance (GDPR audit trails, financial record-keeping), debugging ("what did the config look like yesterday at 3pm?"), and recovery ("someone deleted the wrong rows, can we get them back?"). I've seen teams spend weeks building audit infrastructure that MariaDB gives you for free.

What MySQL Has That MariaDB Doesn't

Advanced JSON Operations

Both support JSON columns and basic JSON functions, but MySQL goes further here with features MariaDB is still catching up on.

JSON_TABLE converts JSON arrays into relational rows you can JOIN against:

typescript
await my.execute('DROP TABLE IF EXISTS orders')

await my.execute(`
  CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    customer VARCHAR(100) NOT NULL,
    items JSON NOT NULL
  )
`)

await my.execute(
  `INSERT INTO orders (customer, items) VALUES
   ('Alice', '["KB-001", "MS-001"]'),
   ('Bob', '["MN-001", "DK-001", "HP-001"]')`,
)

const [orderDetails] = await my.execute(`
  SELECT
    o.customer,
    jt.sku,
    p.name,
    p.price
  FROM orders o,
  JSON_TABLE(o.items, '$[*]' COLUMNS (sku VARCHAR(50) PATH '$')) AS jt
  JOIN products p ON p.sku = jt.sku
  ORDER BY o.customer, p.price DESC
`)

console.log('\nMySQL JSON_TABLE - orders with product details:')
for (const row of orderDetails as Array<{ customer: string; sku: string; name: string; price: number }>) {
  console.log(`  ${row.customer}: ${row.name} ($${Number(row.price).toFixed(2)})`)
}
text
MySQL JSON_TABLE - orders with product details:
  Alice: Mechanical Keyboard ($129.99)
  Alice: Wireless Mouse ($39.99)
  Bob: Standing Desk ($599.99)
  Bob: 27" 4K Monitor ($449.99)
  Bob: Noise-Canceling Headphones ($249.99)

MySQL also supports multi-valued indexes on JSON arrays (index individual elements for fast lookups) and JSON_SCHEMA_VALID for validating JSON data against a schema at write time.

In MariaDB, you'd extract JSON values with JSON_EXTRACT and handle the rest in application code, or normalize the JSON into a proper relational table. Honestly, normalization is often the better design anyway, but sometimes you need the flexibility.

Licensing and Governance

This is where opinions get strong.

MySQL is dual-licensed: GPL v2 for the community edition, proprietary for enterprise. Oracle controls the roadmap, the release schedule, and which features land in community vs. enterprise. Community MySQL is fully functional, but Thread Pool, audit logging, and MySQL Router for high availability are paid.

MariaDB is GPL v2, period. The MariaDB Foundation oversees it as a genuine open-source effort. No proprietary fork with hidden features. The MariaDB Corporation (a separate entity) sells commercial support and the MaxScale proxy, but the database itself is fully open.

If your organization cares about software freedom or long-term license stability, MariaDB's governance model is more predictable. If you want Oracle-backed enterprise support and don't mind the dual-license model, MySQL Enterprise is a polished product.

Performance

For most workloads, performance shouldn't be your deciding factor. Both use InnoDB. Both handle concurrent reads and writes well. Both support connection pooling, query caching, and the usual tuning knobs.

Where they diverge:

  • Thread pool: MySQL gates this behind Enterprise. MariaDB includes it in the community edition, which can improve performance under high concurrency
  • Optimizer: MariaDB's query optimizer has diverged from MySQL's and sometimes produces different execution plans. In some benchmarks MariaDB is faster for complex JOINs; in others MySQL wins on simple lookups. Benchmark your actual queries
  • Storage engines: MariaDB ships Aria (crash-safe MyISAM replacement) and ColumnStore (for analytics). These don't affect InnoDB performance but give you more options for specialized workloads

Ecosystem and Hosting

MySQL wins on ecosystem breadth. It's not particularly close.

Every major cloud provider offers managed MySQL: AWS RDS, Google Cloud SQL, Azure Database for MySQL, DigitalOcean, PlanetScale. MariaDB has fewer first-party managed options. AWS offers MariaDB on RDS, but it's not as universally available.

WordPress, Drupal, Magento, and most PHP-era CMSes were built on MySQL. They generally work with MariaDB (many Linux distributions have actually switched their default to MariaDB), but MySQL is what the documentation targets.

ORMs and database tools support both through the shared wire protocol. Prisma, Drizzle, TypeORM, Sequelize, Knex, TablePlus: they all connect to either one. You rarely need to change application code when switching.

Layerbase Cloud supports both MySQL and MariaDB as managed instances with TLS, so you can try either one without committing to a local install.

When to Pick MySQL

  • Your hosting provider offers managed MySQL but not MariaDB. Don't fight your platform.
  • You lean heavily on JSON. JSON_TABLE, multi-valued indexes, and JSON_SCHEMA_VALID are genuinely useful if your schema mixes relational and document data.
  • You want Oracle-backed enterprise support. MySQL Enterprise includes commercial SLAs, Oracle Cloud integration, and HeatWave analytics.
  • Your existing stack assumes MySQL. WordPress, Laravel Forge, PlanetScale, Vitess: dozens of tools are MySQL-first. If you're deep in that ecosystem, stick with it.
  • You value the largest possible hiring pool. More developers have MySQL on their resume than MariaDB.

When to Pick MariaDB

  • You need temporal data. System-versioned tables alone justify MariaDB for audit-heavy applications. Building the equivalent in MySQL takes significant custom infrastructure.
  • You want sequences. CREATE SEQUENCE beats the MySQL workaround of a dedicated table with row-level locking. Not even close.
  • Open-source governance matters. If you need certainty that your database will stay fully open, MariaDB's Foundation model provides that.
  • You're migrating from Oracle DB. Oracle compatibility mode (SET SQL_MODE='ORACLE') eases the transition.
  • You want thread pooling without paying for Enterprise. MariaDB includes it out of the box.

The Verdict

If you're starting a new project and don't have specific requirements pulling you one way, here's the practical answer:

Pick MySQL if you want the largest ecosystem, the most managed hosting options, and strong JSON support. It's the safe default with the widest safety net.

Pick MariaDB if you need system-versioned tables, care about open-source licensing, or want features like sequences and thread pooling that MySQL locks behind Enterprise. MariaDB gives you more database for fewer constraints.

Both are production-grade with active communities and long track records. You won't regret either choice for a standard web application. The differences matter at the margins. This post should help you figure out which margins matter to you.

Wrapping Up

Close your connections:

typescript
await my.end()
await maria.end()

To manage your local instances with SpinDB:

bash
spindb stop mysql1    # Stop MySQL
spindb stop maria1    # Stop MariaDB
spindb start mysql1   # Start MySQL again
spindb start maria1   # Start MariaDB again
spindb list           # See all your database instances

SpinDB supports 20+ database engines from one CLI, so you can run MySQL and MariaDB side by side without juggling separate installs. Layerbase Desktop is available if you prefer a GUI on macOS.

Something not working?