Backend & systems·topic 2 of 13
Data modelling and storage engines
How rows are laid out on disk decides which queries are cheap, and no amount of application code recovers from getting it wrong. This is the layer where a decision made in week one is still costing you in year three.
Read in order · tick what you already know
- 01
your queries touch a handful of rows by key, thousands of times a second, and each one has to come back in milliseconds.
OLTP
- 02
your query reads three columns across two billion rows and nobody minds if it takes forty seconds.
OLAP
- 03
you store the customer's address in exactly one table so that changing it cannot leave four stale copies behind.
Normalisation
- 04
you copy the customer's name into the orders table so the list page stops doing a join on every render.
Denormalisation
- 05
you add an index and the query that was scanning ten million rows starts answering in a millisecond by walking a sorted tree.
B-tree index
- 06
you index the boolean column, the planner ignores it, and the reason is that half the table matches any value you look up.
Cardinality
- 07
you pick the primary key and quietly decide the physical order the rows will be stored in on disk forever.
Clustered index
- 08
you add the extra column to the index so the query is answered from the index alone and never touches the table.
Covering index
- 09
you run EXPLAIN and find out the database chose a sequential scan over the index you added last week.
Query planner
- 10
you fetch a list of fifty orders and then the ORM quietly issues fifty more queries, one per order, to load the customer.
N+1 query
- 11
you precompute the expensive aggregate into a real table and refresh it on a schedule instead of recomputing it per request.
Materialised view
- 12
you never update a record in place; you write the change to the end of a file and let readers work out the current state.
Append-only log
- 13
writes go to an in-memory table and an append-only log, and a background process keeps merging the files on disk behind you.
LSM tree
- 14
you check a small probabilistic structure first so you can skip opening a file that definitely does not contain the key.
Bloom filter
- 15
you wrote one kilobyte and the disk did fifteen kilobytes of work, because the row is in three indexes and compaction will rewrite it twice.
Write amplification
- 16
one point lookup turns into six file reads because the key could be in any of the levels the compactor has not merged yet.
Read amplification
- 17
you split the giant table into monthly chunks so the query touches one of them and dropping old data is a DDL statement instead of a delete.
Partitioning
- 18
one database stopped being enough, so you split the rows across several of them and now every query has to know which one to ask.
Sharding
- 19
you pick the column that decides which database a row lives on, and every query that does not filter on it now hits all of them.
Shard key
- 20
you have to change the shape of a table that is being read and written right now, without stopping either.
Schema migration
- 21
you added a column and now have to populate it for eighty million existing rows without locking the table or saturating the disk.
Backfill
- 22
you set a deleted flag instead of removing the row, and now every query in the codebase has to remember to filter on it.
Soft delete
- 23
you stop storing the current balance and instead store every deposit and withdrawal, deriving the balance by replaying them.
Event sourcing
- 24
you stop trying to serve reads and writes from the same model and build a separate read model shaped for the queries.
CQRS