Notes
What it is
MySQL is an open-source relational database. Under the SQL layer sits a pluggable storage engine, and today that is InnoDB, which gives you transactions, row-level locks and crash recovery. It is offered by every cloud, including Aurora and PlanetScale. Unlike Postgres it uses a thread per connection, which is lighter, though a pooler still helps at scale.
- open-source relational DB
- owned by Oracle; MariaDB is
- a community fork
- storage engine: InnoDB
- (transactions, row locks)
- managed: RDS, Aurora MySQL,
- Cloud SQL, PlanetScale
- thread per connection
Clustered index
The most important internal: InnoDB stores the table itself as a B plus tree ordered by primary key. The leaf pages hold whole rows. A secondary index, say on email, stores the email and the primary key, not a row pointer. So a lookup by email walks two trees: first the email index, then the primary key tree. Lookups by primary key are one walk, and range scans by primary key read neighbouring rows.
primary key B+tree (the table): leaf = PK + the whole row secondary index on email: leaf = email + PK SELECT * WHERE email = ? 1. find PK in email index 2. find row in PK tree = two B+tree lookups
Primary keys
That layout makes the primary key choice matter. With an auto-increment or time-ordered id, every insert goes to the right edge of the tree, pages fill up neatly and stay in memory. With random version four UUIDs, inserts land all over the tree, pages split, and more of the index must be cached. Every secondary index also carries the key, so a long key makes them all bigger. Concepts episode 32 covers ordered ids.
- auto-increment or time-ordered
- (UUIDv7, Snowflake-style id)
- → inserts append to the right
- edge of the tree, pages fill
- random UUIDv4
- → inserts land anywhere,
- page splits, cold pages,
- bigger secondary indexes
- see Concepts episode 32 (IDs)
Write path
On a write, InnoDB records three things. The redo log holds page changes, replayed after a crash. The undo log holds the previous row versions, used for rollback and so readers can see an older snapshot. And the server writes the change to the binlog as a row event, which replicas and change data capture tools consume. On commit, redo and binlog are flushed together so they never disagree.
- redo log: physical page
- changes, replayed after crash
- undo log: old row versions
- for rollback and MVCC reads
- binlog: logical row events
- for replicas and CDC
- commit flushes redo + binlog
- (two-phase, kept in sync)
Isolation
Isolation is a classic interview detail. MySQL defaults to repeatable read, stricter than the Postgres default. Plain selects read a snapshot without locking. But a locking read, like select for update, also locks the gaps between index entries, so nobody can insert into that range. That prevents phantoms but causes more lock waits and deadlocks on hot tables. InnoDB detects the deadlock and rolls one transaction back, so your code must retry.
- default: REPEATABLE READ
- (Postgres: READ COMMITTED)
- plain SELECT: snapshot read,
- no locks
- SELECT ... FOR UPDATE:
- row + gap ("next-key") locks
- gap locks block inserts into
- a range: more deadlocks on
- busy tables, retry them
Replication
Replication is MySQL's long-time strength. Each replica pulls the binlog and replays it, now in parallel. It is asynchronous by default, so replicas can lag. Semi-synchronous mode waits until at least one replica has received the event before commit returns, which shrinks the loss window. Global transaction ids make it easy to repoint replicas during failover. A proxy such as ProxySQL sends writes to the primary and spreads reads across replicas.
- replica pulls binlog, applies
- it (parallel apply by default)
- async: default, may lag
- semi-sync: one replica acks
- receipt before commit returns
- GTIDs: each txn has a global
- id, failover is simpler
- Group Replication / InnoDB
- Cluster: consensus-based
Read your writes
Replica lag creates a classic bug. A user saves their profile, the next page reads from a replica that is half a second behind, and the old profile appears. Three fixes. Send that user's reads to the primary for a few seconds after a write. Or wait until the replica has applied the transaction id of the write. Or serve the fresh value from a cache you just updated. Concepts episode 26 calls this read your writes.
- user saves profile → reads
- from a lagging replica →
- sees the old profile
- fixes:
- • read from primary for N
- seconds after a write
- • wait for the replica to reach
- the write’s GTID
- • cache the fresh value
Sharding
When writes outgrow one primary, MySQL teams shard. The simple way is application routing by user id across many databases. The well-known tool is Vitess, which grew out of a large video site's MySQL fleet. A gateway parses each query, routes it by the sharding key, pools connections and can reshard online. Each shard is still an ordinary MySQL primary with replicas.
- app-level: route by user_id
- to one of N databases
- Vitess: VTGate routes SQL by
- a sharding key (vindex),
- reshards online, pools
- connections; PlanetScale
- runs it as a service
- each shard: primary + replicas
Great at
MySQL shines for read-heavy web applications: a primary plus a fleet of replicas carries a lot of traffic. Primary key lookups and range scans are fast because rows sit in key order. The tooling is mature, including online schema change tools and instant column adds. And with Vitess there is a proven path to very large horizontal scale.
- • read-heavy web apps with
- many replicas
- • fast PK lookups and PK range
- scans (clustered layout)
- • mature replication, tooling,
- online schema change
- (gh-ost, pt-osc, INSTANT DDL)
- • Vitess for horizontal scale
- • huge hiring pool
Where it hurts
Where it hurts. One write primary per shard, like any classic relational database. Fewer advanced features than Postgres: fewer index types, a smaller extension ecosystem and weaker JSON. Gap locks cause deadlocks under contention. Random keys slow inserts. Replicas lag. And once you shard, joins and transactions across shards are something you design around, not something you get.
- • single primary per shard
- • fewer advanced features than
- Postgres (index types,
- extensions, rich JSON)
- • gap locks → deadlocks
- • random PKs hurt inserts
- • replica lag, stale reads
- • big ALTERs need a tool
- • cross-shard joins: avoid
Use it when
Use MySQL for read-heavy transactional apps, especially when the team already operates it, or when you expect to shard and want Vitess. Prefer Postgres when you need rich features like geo queries, vectors or advanced indexes. Use a column store for heavy analytics. And neither is the natural fit for active writes in several regions at once.
- USE when
- read-heavy OLTP web app
- team already knows MySQL
- plan to shard (Vitess)
- AVOID when
- heavy analytics, geo, vectors
- (Postgres or a column store)
- multi-region active writes
- schemaless, nested records
Numbers
Numbers to remember. InnoDB pages are sixteen kilobytes. The default isolation is repeatable read and the default connection limit is one hundred and fifty-one. Give the buffer pool about seventy to eighty percent of memory on a dedicated server. A single node does tens of thousands of primary-key reads a second, and simple writes in the thousands to tens of thousands. Replication lag is usually milliseconds, but spikes under heavy writes.
InnoDB page size 16 KB default isolation REPEATABLE READ default max_connections 151 PK point reads/node 10k-100k+/s simple writes/node 5k-30k+/s buffer pool ~70-80% of RAM replicas per primary often 5-10+ replica lag ms to seconds
Say it like this
How to say it. MySQL with InnoDB, with ordered big integer ids so inserts append to the tree. One primary and three replicas behind a proxy that splits reads from writes, and a user's reads go to the primary for a few seconds after they write. At ten times the writes, shard with Vitess by user id, so each user's rows live on one shard and most queries never cross shards.
- "MySQL with InnoDB. Ordered
- bigint ids so inserts append.
- One primary, three replicas
- behind ProxySQL; reads after
- a write go to the primary for
- a few seconds. At 10x writes,
- Vitess sharded by user_id so
- a user’s rows stay together."