MySQL vs MariaDB
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
- Set Up Both Locally
- Project Setup
- Side-by-Side: The Basics
- What MariaDB Has That MySQL Doesn't
- What MySQL Has That MariaDB Doesn't
- Licensing and Governance
- Performance
- Ecosystem and Hosting
- When to Pick MySQL
- When to Pick MariaDB
- The Verdict
- Wrapping Up
Quick Comparison
| MySQL | MariaDB | |
|---|---|---|
| License | GPL v2 + proprietary (Oracle) | GPL v2 (MariaDB Foundation) |
| Governance | Oracle Corporation | MariaDB Foundation (community) |
| Sequences | No (use AUTO_INCREMENT workarounds) | Yes, first-class CREATE SEQUENCE |
| System-versioned tables | No | Yes, built-in temporal queries |
| JSON support | Strong (JSON_TABLE, multi-valued indexes, JSON_SCHEMA_VALID) | Basic (JSON functions, no JSON_TABLE until 11.2) |
| Default storage engine | InnoDB | InnoDB (also ships Aria, ColumnStore) |
| Oracle compatibility mode | No | Yes (SET SQL_MODE='ORACLE') |
| Wire protocol | MySQL protocol | MySQL protocol (compatible) |
| Node.js driver | mysql2 | mariadb (official connector) |
| Cloud availability | Every 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:
npm i -g spindb # npm
pnpm add -g spindb # pnpmCreate both instances. SpinDB assigns non-conflicting ports automatically:
spindb create mysql1 -e mysql --start
spindb create maria1 -e mariadb --startVerify they're running:
spindb url mysql1
spindb url maria1mysql://root@127.0.0.1:3306
mysql://root@127.0.0.1:3307/testYour 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:
mkdir mysql-vs-mariadb && cd mysql-vs-mariadb
pnpm init
pnpm add mysql2 mariadb
pnpm add -D tsx typescriptCreate 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.
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:
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:
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:
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)}`)
}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.99Identical 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.
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)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.
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)}`)
}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.99The 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:
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)})`)
}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, andJSON_SCHEMA_VALIDare 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 SEQUENCEbeats 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:
await my.end()
await maria.end()To manage your local instances with SpinDB:
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 instancesSpinDB 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.