Part 0 · 7 chapters · ~20 min

History, and why MySQL is shaped this way

Half of what looks arbitrary in MySQL is a fossil. The table that silently truncated your string, the engine that ignores foreign keys, the two separate write-ahead logs that must agree at commit — each one is the residue of a decision made under constraints that no longer exist. This part is the archaeology. Everything after it makes more sense for having read it.

1

Monty, ISAM, and “fast enough”

In 1994 Michael “Monty” Widenius was working at TcX, a Swedish consultancy building data warehousing tools. They had a data store called UNIREG and a need to serve web-scale-for-1994 queries over it. Monty first tried to wire mSQL — a small, popular SQL server of the era — on top of UNIREG's table handler. It was too slow and too limited.

So he wrote his own SQL layer over the same low-level storage code, keeping mSQL's C API almost exactly so existing tools would port easily. That storage code was an ISAM implementation: Indexed Sequential Access Method, a 1960s IBM design where records sit in a file and a separate index gives you keyed access to them.

This origin matters more than it sounds. MySQL was not designed as a database from first principles. It was designed as a SQL interface bolted onto an existing fast file-based table handler, for a workload that was overwhelmingly reads, on a single machine, where losing a little data in a crash was survivable and being slow was not.

the founding trade
Speed and simplicity were purchased with correctness guarantees. No transactions. No foreign keys. No crash safety worth the name. For the 1990s web — a read-heavy, forgiving, low-stakes workload — this was not negligence. It was the right call, and it is why MySQL won the web.

The name is domestic rather than technical: Monty's daughter is called My. (His later projects, MariaDB and the Maria storage engine, are named after another daughter, Maria.) The dolphin is called Sakila, picked from a name-the-dolphin contest in 2002.

2

MyISAM’s reign, and why a table lock was acceptable

MyISAM arrived in MySQL 3.23 (2001) as the improved successor to that original ISAM code, and it was the default engine until 5.5 shipped in 2010. For most of MySQL's cultural ascendancy — the LAMP stack, the first decade of PHP — MyISAM was MySQL.

Its defining property: table-level locking. A write to any row takes an exclusive lock on the whole table. Every other writer, and under some conditions every reader, waits.

That sounds indefensible now. In 2001 it was reasonable, because of what the typical workload looked like:

  • Reads outnumbered writes by something like 50:1 on a content site.
  • Writes were short — an INSERT of one row, not a transaction spanning twenty statements and a network round trip.
  • Concurrent connections numbered in the dozens, not the thousands.
  • The machine had one or two cores, so contention was limited by hardware anyway.

Under those conditions a table lock held for a millisecond is invisible, and you get in exchange an engine with no transaction overhead, no MVCC bookkeeping, no undo log, and a very compact on-disk footprint. MyISAM's COUNT(*) is instant because it stores the row count in the table header — it can do that precisely because it has no MVCC, so there is exactly one version of the truth.

what it cost
No transactions, no crash recovery (a killed server left indexes needing REPAIR TABLE, sometimes for hours), no foreign keys, and a concurrency ceiling you hit the moment writes became a meaningful fraction of traffic. Every one of those became fatal as the web grew up.

MyISAM is deprecated and you should never choose it. It appears in this course for two reasons: it explains inherited behavior in the server layer, and its limitations are exactly the design brief that InnoDB was written to answer.

3

Innobase, Heikki Tuuri, and Oracle’s two purchases

In 1995 a Finnish researcher named Heikki Tuuri founded Innobase Oy to build a transactional storage engine. InnoDB was folded into MySQL in 2001 (version 3.23.34a) as an option, not a default.

InnoDB was, and is, a genuinely different piece of software from the rest of MySQL. It brought the whole textbook: MVCC, row-level locking, a redo log, undo logs, a buffer pool, crash recovery, foreign keys, and the clustered index layout that determines the physical shape of your data. Much of the rest of this course is really a course about InnoDB.

Then the corporate sequence, which is genuinely important:

1995
Innobase Oy foundedHeikki Tuuri begins InnoDB.
2001
InnoDB ships inside MySQLOptional engine in 3.23. Transactions arrive.
Oct 2005
Oracle buys InnobaseOracle now owns the transactional engine inside its main open-source competitor. MySQL AB does not own its own storage layer.
2006
The Falcon projectMySQL AB starts building a replacement engine to escape the dependency. It never ships.
Jan 2008
Sun buys MySQL AB$1bn. Monty and others depart over direction.
Jan 2010
Oracle buys SunOracle now owns MySQL itself, five years after buying its engine. The EU competition review of this deal is why MySQL's licensing commitments exist.
Dec 2010
InnoDB becomes the defaultMySQL 5.5. The engine Oracle bought first becomes the engine that matters.
what a Staff engineer takes from this
The five-year gap between the two acquisitions is why MySQL has a pluggable engine architecture with a hard interface between the server layer and storage — and why that boundary is so visible in the behavior. You will meet it again in chapter 14, where the handler API explains why the optimizer sometimes makes decisions that look ignorant of what the engine knows.
4

Sun, the fork, and Percona

When Oracle acquired Sun, Monty left and forked MySQL into MariaDB, on the argument that a database owned by the vendor of the leading commercial database could not be trusted to stay open. He campaigned publicly against the acquisition during the EU review.

The fork has diverged much further than people assume. It is not a drop-in replacement any more:

AreaMySQL 8.xMariaDB 10/11
Data dictionaryTransactional, InnoDB-backed, no .frmKept .frm files far longer; different design
OptimizerHash join, histograms, volcano executorDifferent cost model, own optimizer switches
ReplicationGTIDs (server_uuid:seq)Different GTID format — not interoperable
JSONBinary storage typeAlias over LONGTEXT with a CHECK
Extra engines—Aria, ColumnStore, Spider
ThreadingThread pool in Enterprise onlyThread pool in the community build

Percona occupies a third position: a services company shipping Percona Server, a drop-in MySQL build with extra instrumentation and performance patches, plus the tooling that most serious MySQL operators actually use — pt-query-digest, pt-online-schema-change, XtraBackup. When this course reaches production operations, several of the tools are Percona's, not Oracle's.

this course’s position
We teach MySQL 8.4 LTS with InnoDB, because that is what you will meet in production and in interviews. MariaDB and Percona differences are flagged inline where they would change your answer, not taught as a parallel track.
5

5.5 → 8.4: what actually changed, and why

Version trivia is worthless; knowing which release changed a behavior you depend on is not. This is the filtered list — the changes that alter how you design or debug.

ReleaseWhat changedWhy you care
5.5
2010
InnoDB becomes default; multiple buffer pool instancesThe moment MySQL becomes a real transactional database by default.
5.6
2013
Online DDL, GTIDs, index condition pushdown, MRR, full-text in InnoDB, persistent statsThe release that made schema changes and replication manageable.
5.7
2015
Native JSON type, generated columns, Group Replication, sys schema, strict mode on by defaultStrict mode is the big one: silent truncation of bad data stopped being the default. Still widely deployed.
8.0
2018
Transactional data dictionary (.frm gone), CTEs, window functions, descending indexes, invisible indexes, histograms, hash join (8.0.18), utf8mb4 default, atomic DDL, query cache removed, instant ADD COLUMNThe largest release in MySQL's history. Nearly everything modern in this course is 8.0+.
8.0.30
2022
Dynamic redo log resizing (innodb_redo_log_capacity)Resizing redo no longer needs a restart — changes a whole class of tuning procedure.
8.4 LTS
2024
Long-term support line; replication defaults modernized; mysql_native_password off by default; several deprecated features removedThe version to target. The auth default breaks old drivers — a real migration hazard.
9.x
2024→
Innovation releases: vector type, JavaScript stored programs (Enterprise)Short-lived releases. Don't run them in production; know they exist.
the 8.4 trap
mysql_native_password is disabled by default in 8.4. An older client library that cannot speak caching_sha2_password will fail to connect with an authentication error that looks like bad credentials. Chapter 8 covers the handshake in detail; for now, know that this is the single most common 8.4 upgrade failure.
run it
-- What are you actually running? Check before you trust any advice.
SELECT VERSION();

-- The defaults that changed most recently and bite hardest.
SHOW VARIABLES WHERE Variable_name IN (
  'default_authentication_plugin',   -- gone in 8.4, replaced below
  'authentication_policy',
  'sql_mode',                          -- STRICT_TRANS_TABLES since 5.7
  'character_set_server',             -- utf8mb4 since 8.0
  'default_storage_engine',
  'innodb_redo_log_capacity'          -- 8.0.30+ only
);
6

The pluggable storage engine API

This is the architectural decision that defines MySQL. The server is split in two, with a hard interface between the halves:

Everything above the line — connection handling, parsing, privileges, the optimizer, stored programs, the binary log — is engine-agnostic. Everything below it is the engine's business: how rows are physically stored, whether there are transactions, what locking granularity exists, how indexes work.

The interface itself is a C++ class called handler, with roughly a hundred virtual methods. An engine implements the ones it supports: rnd_next() to scan, index_read() to seek, write_row(), update_row(), delete_row(), plus a bitmask advertising its capabilities.

What this buys, and what it costs

The benefit is obvious: one SQL layer, many storage strategies. You can put an in-memory engine, an archive engine, a federated engine and a full transactional engine behind the same SQL.

The cost is subtler and shows up constantly once you know to look for it. The server layer has to make decisions using only what the interface exposes, which means:

  • The optimizer costs a plan using estimates the engine hands over through a narrow interface. It cannot see the actual page layout. This is why EXPLAIN row counts are often wildly wrong — you meet this properly in chapter 78.
  • There are two write-ahead logs: InnoDB's redo log below the line, and the server's binary log above it. They must agree on whether a transaction committed, which requires an internal XA two-phase commit on every single commit. That is chapter 90, and it is a real throughput cost.
  • Some engines silently ignore what they cannot do. Declare a foreign key on a MyISAM table and it is parsed, accepted, and discarded.
say this out loud in an interview
“MySQL's server layer and storage layer are separated by the handler API. That is why it has two logs that need XA to stay consistent, why optimizer statistics are estimates handed up through a thin interface, and why engine capabilities differ so visibly at the SQL level.” That one sentence covers three separate deep topics and signals you know where the seams are.
run it
-- Which engines does this server have, and what can they do?
SHOW ENGINES;

-- Watch the seam: the parser accepts a foreign key that
-- the engine will quietly throw away.
CREATE TABLE parent (id INT PRIMARY KEY) ENGINE=MyISAM;
CREATE TABLE child (
  id  INT PRIMARY KEY,
  pid INT,
  FOREIGN KEY (pid) REFERENCES parent(id)
) ENGINE=MyISAM;

SHOW CREATE TABLE child\G
-- The FOREIGN KEY clause is absent. It was accepted and dropped.
-- Now do the same with ENGINE=InnoDB and diff the output.

DROP TABLE child, parent;
architecture
server layer ↔ storage engine
swipe the figure sideways, or tap expand for full screen
1/5
server layer
Above the line: connection handling, parsing, the optimizer, the executor and the binary log. None of it knows how rows are stored.
7

MySQL and Postgres as two philosophies

You will be asked to compare these. Most answers are a list of features, which is the Senior answer. The Staff answer is that they made two different foundational choices, and nearly every observable difference follows from those choices.

Choice one: where the row lives

InnoDB clusters the table into the primary key's B+tree. The leaf node is the row. There is no separate heap.

Postgres stores rows in an unordered heap and every index, including the primary key, points into it.

Consequences you can derive rather than memorize:

  • In InnoDB, primary key order is physical order. Sequential keys append to one page; random keys (UUIDv4) scatter writes across the whole tree. That is chapter 31, and it is one of the highest-impact schema decisions you make.
  • In InnoDB, a secondary index stores the primary key, not a row pointer — so a wide primary key inflates every secondary index, and a non-covering lookup costs two tree descents (chapter 26).
  • In Postgres, an UPDATE writes a whole new tuple and leaves the old one behind, which is why VACUUM and bloat are Postgres's defining operational concern, and why InnoDB's equivalent concern is instead the undo history list growing under a long transaction (chapter 45).

Choice two: where old row versions go

Both do MVCC; they put the old versions in different places. Postgres keeps them in the heap alongside live rows. InnoDB keeps them in a separate undo log and rebuilds old versions on demand by walking a pointer chain backwards.

InnoDBPostgreSQL
Row storageClustered in PK B+treeUnordered heap
Old versionsUndo log, rebuilt on readIn the heap, cleaned by VACUUM
Secondary index entryPoints to the PK valuePoints to a physical tuple id
Defining ops problemLong transactions grow undo historyBloat and transaction id wraparound
Connection modelThread per connectionProcess per connection
Write path logsRedo and binlog, coordinated by XAOne WAL
ExtensibilityPluggable storage enginesExtensions, types, index access methods
the answer that lands
“They differ in where the row lives and where old versions go. InnoDB clusters rows into the PK tree and puts old versions in undo; Postgres uses a heap and keeps old versions in it. Almost everything else — why UUID primary keys hurt MySQL more, why Postgres has VACUUM, why MySQL secondary indexes get fat — falls out of those two choices.”

You return to this in chapter 120 with the full argument in both directions.

That is the archaeology. Part 1 follows a single SELECT from the socket all the way to the bytes on disk, and every part after it zooms into one stage of that path.

storage layout
clustered vs heap
swipe the figure sideways, or tap expand for full screen
InnoDB's leaf pages hold the rows themselves. Postgres keeps rows in a heap and points at them.