{"id":"relational-databases","title":"Relational databases and scaling SQL","sections":[{"id":"overview","title":"Overview","html":"<p>This chapter answers five questions:</p>\n<ul>\n<li>What is a relational database, and what do the four ACID guarantees promise?</li>\n<li>How does copying a database to more machines (replication) add read capacity and survive failures, and what does it cost?</li>\n<li>How do you split a database that has outgrown one machine, either by feature (federation) or by rows (sharding)?</li>\n<li>When is it worth storing the same data twice to make reads faster (denormalization)?</li>\n<li>How do you find and fix slow queries before you reach for more hardware (SQL tuning)?</li>\n</ul>\n<p>We'll keep using <strong>Crumb</strong>, the recipe-sharing site from the <a href=\"/learn/scalability\">Scalability</a> chapter. Crumb stores its data in a relational database with four main tables: <code>users</code>, <code>recipes</code>, <code>comments</code> and <code>ratings</code>. Crumb now has several app servers behind a load balancer and a cache, and its one database server is starting to struggle. The Scalability chapter sketched the ways out. This chapter works through each one in detail.</p>","headings":[]},{"id":"what-a-relational-database-is","title":"What a relational database is","html":"<h3 id=\"what-a-relational-database-is\">What a relational database is</h3>\n<p>A <strong>relational database</strong> stores data in <strong>tables</strong>. A table looks like a spreadsheet:</p>\n<ul>\n<li>Each <strong>row</strong> is one item, such as one recipe.</li>\n<li>Each <strong>column</strong> is one fact about that item, such as its title or the date it was posted.</li>\n<li>The <strong>schema</strong> is the list of tables and columns, with a fixed type for each column (a number, some text, a date).</li>\n</ul>\n<p>Here is a small piece of Crumb's <code>recipes</code> table:</p>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th>id</th><th>author_id</th><th>title</th><th>posted_at</th></tr></thead><tbody><tr><td>101</td><td>7</td><td>Brown butter cookies</td><td>2026-03-02</td></tr><tr><td>102</td><td>9</td><td>Weeknight dal</td><td>2026-03-04</td></tr></tbody></table></div>\n<p>Every row has a <strong>primary key</strong>, a column whose value is unique to that row. Here it is <code>id</code>. Rows in one table point to rows in another with a <strong>foreign key</strong>. The <code>author_id</code> column holds the <code>id</code> of a row in the <code>users</code> table, so Crumb can find who wrote each recipe without copying the author's name into every recipe.</p>\n<p>You read and change the data with <strong>SQL</strong> (Structured Query Language). To show a recipe page with its author's name, Crumb asks the database to combine two tables. Combining rows from several tables by matching keys is called a <strong>join</strong>:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">SELECT</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> recipes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">title</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">users</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">display_name</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">FROM</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> recipes</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">JOIN</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> users </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">ON</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> users</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> recipes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">author_id</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">WHERE</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> recipes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 101</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span></code></pre>\n<p>Storing each fact in exactly one place, and using keys and joins to connect the pieces, is called <strong>normalization</strong>. If an author changes their display name, Crumb updates one row in <code>users</code>, and every recipe page shows the new name. Keep this idea in mind. Several scaling techniques later in this chapter work by giving it up on purpose.</p>\n<p>Well-known relational databases include PostgreSQL, MySQL, SQL Server and Oracle. The term <strong>RDBMS</strong> (relational database management system) means the software that runs one.</p>","headings":[{"id":"what-a-relational-database-is","title":"What a relational database is","depth":3}]},{"id":"transactions-and-acid","title":"Transactions and ACID","html":"<h3 id=\"transactions-and-acid\">Transactions and ACID</h3>\n<p>A <strong>transaction</strong> is a group of reads and writes that the database treats as one unit of work.</p>\n<p>Here is a concrete one. When a user gives a recipe four stars, Crumb does two things:</p>\n<ol>\n<li>It inserts a row into <code>ratings</code>: user 42 gave recipe 101 four stars.</li>\n<li>It updates two columns on the recipe's row, <code>rating_sum</code> and <code>rating_count</code>, so the recipe page can show the average without adding up every rating each time.</li>\n</ol>\n<p>Crumb wraps both steps in one transaction:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">BEGIN</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">INSERT INTO</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ratings (user_id, recipe_id, stars) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">VALUES</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">42</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">101</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">UPDATE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> recipes </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">SET</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rating_sum </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rating_sum </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 4</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, rating_count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rating_count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  WHERE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 101</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">COMMIT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span></code></pre>\n<p>Relational databases promise four things about transactions. Together they are called <strong>ACID</strong>.</p>\n<h4 id=\"atomicity-all-or-nothing\">Atomicity: all or nothing</h4>\n<p>Either every step of the transaction happens, or none of them does. If the server crashes after the <code>INSERT</code> but before the <code>UPDATE</code>, the database undoes the insert when it restarts. Crumb never ends up with a rating that isn't counted in the average.</p>\n<h4 id=\"consistency-the-rules-always-hold\">Consistency: the rules always hold</h4>\n<p>Every transaction moves the database from one valid state to another valid state. \"Valid\" means the data obeys the rules written into the schema. These rules are called <strong>constraints</strong>. Crumb's <code>ratings</code> table might have three:</p>\n<ul>\n<li><code>CHECK (stars BETWEEN 1 AND 5)</code>: a rating of 7 stars is rejected.</li>\n<li>A foreign key on <code>recipe_id</code>: you can't rate a recipe that doesn't exist.</li>\n<li><code>UNIQUE (user_id, recipe_id)</code>: each user can rate each recipe only once.</li>\n</ul>\n<p>If any step would break a rule, the whole transaction fails, and atomicity rolls back the other steps.</p>\n<p>This \"C\" is a different idea from the \"C\" in the CAP theorem. CAP's consistency is about whether every reader sees the latest write. The <a href=\"/learn/cap-and-consistency#c-consistency\">CAP chapter</a> explains the difference.</p>\n<h4 id=\"isolation-transactions-dont-see-each-others-half-finished-work\">Isolation: transactions don't see each other's half-finished work</h4>\n<p>When many transactions run at the same time, the result should be the same as if they had run one after another.</p>\n<p>Here is the kind of bug this is about. Two people rate recipe 101 at the same moment. Crumb's code reads <code>rating_count</code>, adds one in the application, and writes the new value back. Both requests read 10, and both write 11, so one rating vanishes from the count. This is a <strong>lost update</strong>. Writing <code>rating_count = rating_count + 1</code> inside the database avoids it, because the database locks the row and applies the two updates one after the other.</p>\n<h4 id=\"durability-committed-means-saved\">Durability: committed means saved</h4>\n<p>Once the database says a transaction is committed, the change survives a crash or a power cut. Databases usually do this with a <strong>write-ahead log</strong>, an append-only file that records each change before the main data files are touched. In PostgreSQL's <a href=\"https://www.postgresql.org/docs/current/wal-intro.html\" target=\"_blank\" rel=\"noopener noreferrer\">words</a>, changes to data files \"must be written only after those changes have been logged.\" After a crash, the database replays the log.</p>\n<h4 id=\"isolation-levels\">Isolation levels</h4>\n<p>Perfect isolation is expensive, because the database has to hold more transactions back to keep them apart. So databases offer several <strong>isolation levels</strong>. The SQL standard names four, defined by which oddities each one still allows:</p>\n<ul>\n<li>A <strong>dirty read</strong> sees another transaction's change before it commits.</li>\n<li>A <strong>non-repeatable read</strong> reads the same row twice and gets two different values.</li>\n<li>A <strong>phantom read</strong> runs the same search twice and gets a different set of rows.</li>\n</ul>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th>Level</th><th>Dirty read</th><th>Non-repeatable read</th><th>Phantom read</th></tr></thead><tbody><tr><td>Read uncommitted</td><td>possible</td><td>possible</td><td>possible</td></tr><tr><td>Read committed</td><td>no</td><td>possible</td><td>possible</td></tr><tr><td>Repeatable read</td><td>no</td><td>no</td><td>possible</td></tr><tr><td>Serializable</td><td>no</td><td>no</td><td>no</td></tr></tbody></table></div>\n<p>Only Serializable fully matches \"as if one at a time.\" PostgreSQL <a href=\"https://www.postgresql.org/docs/current/transaction-iso.html\" target=\"_blank\" rel=\"noopener noreferrer\">defaults to Read Committed</a>, and MySQL's InnoDB engine <a href=\"https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html\" target=\"_blank\" rel=\"noopener noreferrer\">defaults to Repeatable Read</a>. The defaults are fine for most web pages. When money or stock counts are involved, check what your level allows.</p>","headings":[{"id":"transactions-and-acid","title":"Transactions and ACID","depth":3},{"id":"atomicity-all-or-nothing","title":"Atomicity: all or nothing","depth":4},{"id":"consistency-the-rules-always-hold","title":"Consistency: the rules always hold","depth":4},{"id":"isolation-transactions-dont-see-each-others-half-finished-work","title":"Isolation: transactions don't see each other's half-finished work","depth":4},{"id":"durability-committed-means-saved","title":"Durability: committed means saved","depth":4},{"id":"isolation-levels","title":"Isolation levels","depth":4}]},{"id":"the-scaling-toolkit-at-a-glance","title":"The scaling toolkit at a glance","html":"<h3 id=\"the-scaling-toolkit-at-a-glance\">The scaling toolkit at a glance</h3>\n<p>One database server has limits. It has only so much CPU, memory and disk speed, and if it fails, everything stops. There are six standard techniques for scaling a relational database. They solve different problems:</p>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th>Technique</th><th>What it does</th><th>What it helps with</th><th>Main cost</th></tr></thead><tbody><tr><td>Primary-replica replication</td><td>Copies the whole database to read-only machines</td><td>More read capacity, survives a failed server</td><td>Replicas can lag behind</td></tr><tr><td>Primary-primary replication</td><td>Lets two servers both accept writes</td><td>Writes keep working if one server fails</td><td>Conflicting writes</td></tr><tr><td>Federation</td><td>Gives each feature its own database</td><td>Less load on each database</td><td>No joins across features</td></tr><tr><td>Sharding</td><td>Splits one table's rows across databases</td><td>Write capacity and data size</td><td>Routing, uneven shards, cross-shard queries</td></tr><tr><td>Denormalization</td><td>Stores copies of data to skip joins</td><td>Faster reads</td><td>Copies can drift apart; slower writes</td></tr><tr><td>SQL tuning</td><td>Makes each query do less work</td><td>Everything, at no hardware cost</td><td>Needs measurement and care</td></tr></tbody></table></div>\n<p>A sensible order is to tune first, then add replicas, and to split the data only when you must. Tuning is cheap and makes every later step go further. Splitting data is hard to undo.</p>","headings":[{"id":"the-scaling-toolkit-at-a-glance","title":"The scaling toolkit at a glance","depth":3}]},{"id":"replication-copies-of-the-whole-database","title":"Replication: copies of the whole database","html":"<h3 id=\"replication-copies-of-the-whole-database\">Replication: copies of the whole database</h3>\n<p><strong>Replication</strong> means keeping a full copy of the database on more than one server. Each change made on one server is sent to the others and applied there.</p>\n<p>Older documentation calls the server that accepts writes the <strong>master</strong> and the copies the <strong>slaves</strong>. Many projects now say <strong>primary</strong> (or <strong>source</strong>) and <strong>replica</strong>. MySQL, for example, <a href=\"https://dev.mysql.com/doc/refman/8.0/en/replica-connection-thread-states.html\" target=\"_blank\" rel=\"noopener noreferrer\">renamed</a> \"master\" to \"source\" and \"slave\" to \"replica\" in version 8.0.26. This chapter uses primary and replica.</p>\n<h4 id=\"primary-replica-replication\">Primary-replica replication</h4>\n<p>In this setup, one server is the <strong>primary</strong>. It handles all writes, and it can handle reads too. It sends every change to one or more <strong>replicas</strong>. The replicas accept only reads.</p>\n<p>A replica can also pass changes on to other replicas, which forms a tree. PostgreSQL calls this <a href=\"https://www.postgresql.org/docs/current/warm-standby.html\" target=\"_blank\" rel=\"noopener noreferrer\">cascading replication</a>, and says it reduces \"the number of direct connections to the primary.\" For example, the primary sends each change across the ocean once, to one replica in Europe, and that replica feeds the other European replicas.</p>\n<figure class=\"diagram\" data-text=\"Crumb app servers -> Primary (writes); Crumb app servers -> Replica 1 (reads); Primary -> Replica 1 (changes); Primary -> Replica 2 (changes); Replica 1 -> Replica 3 (relays changes)\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 0 800 323\" width=\"800\" role=\"img\" aria-label=\"Crumb&#x27;s writes go to the primary. Replicas serve reads, and one replica feeds another.\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"135.4\" x2=\"327\" y2=\"69.4\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"151\" y1=\"188.6\" x2=\"327\" y2=\"254.6\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"400\" y1=\"74\" x2=\"400\" y2=\"248\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"471\" y1=\"42\" x2=\"647\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"471\" y1=\"282\" x2=\"647\" y2=\"282\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-box\" x=\"12\" y=\"136\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"158.5\">Crumb app</tspan><tspan x=\"80\" y=\"174.5\">servers</tspan></text><path class=\"dg-node dg-store\" d=\"M332,20 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M332,20 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"49.5\">Primary</tspan></text><path class=\"dg-node dg-store\" d=\"M332,260 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M332,260 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"289.5\">Replica 1</tspan></text><path class=\"dg-node dg-store\" d=\"M652,20 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M652,20 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"720\" y=\"49.5\">Replica 2</tspan></text><path class=\"dg-node dg-store\" d=\"M652,260 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M652,260 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"720\" y=\"289.5\">Replica 3</tspan></text><rect class=\"dg-edge-bg\" x=\"215.4\" y=\"93.4\" width=\"47.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"239\" y=\"106.4\" text-anchor=\"middle\">writes</text><rect class=\"dg-edge-bg\" x=\"218.5\" y=\"212.6\" width=\"41\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"239\" y=\"225.6\" text-anchor=\"middle\">reads</text><rect class=\"dg-edge-bg\" x=\"373.3\" y=\"152\" width=\"53.4\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"400\" y=\"165\" text-anchor=\"middle\">changes</text><rect class=\"dg-edge-bg\" x=\"532.3\" y=\"33\" width=\"53.4\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"559\" y=\"46\" text-anchor=\"middle\">changes</text><rect class=\"dg-edge-bg\" x=\"510.6\" y=\"273\" width=\"96.8\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"559\" y=\"286\" text-anchor=\"middle\">relays changes</text></svg></div><figcaption>Crumb's writes go to the primary. Replicas serve reads, and one replica feeds another.</figcaption></figure>\n<p>Crumb gets two things from this:</p>\n<ul>\n<li><strong>Read capacity.</strong> Most of Crumb's traffic is people reading recipes. Each replica can take a share of those reads, so the primary has more room for writes.</li>\n<li><strong>A spare server.</strong> If the primary fails, a replica already holds nearly all the data.</li>\n</ul>\n<p><strong>What happens when the primary fails.</strong> The replicas keep answering reads. So Crumb can stay up in <strong>read-only mode</strong>: people can browse recipes, but they can't post comments or ratings. Writes come back once one of two things happens:</p>\n<ul>\n<li>a replica is <strong>promoted</strong>, meaning it is turned into the new primary, or</li>\n<li>a new primary is set up from scratch.</li>\n</ul>\n<h5 id=\"disadvantage-promotion-needs-extra-logic\">Disadvantage: promotion needs extra logic</h5>\n<p>In PostgreSQL, the promote command itself is short (<code>pg_ctl promote</code>). The hard part is everything around it:</p>\n<ol>\n<li><strong>Decide the primary is really dead.</strong> A slow network looks the same as a dead server.</li>\n<li><strong>Pick the replica with the most data,</strong> since replicas may lag by different amounts.</li>\n<li><strong>Point the other replicas and the app's writes at the new primary.</strong></li>\n<li><strong>Stop the old primary from coming back as a primary.</strong></li>\n</ol>\n<p>PostgreSQL's <a href=\"https://www.postgresql.org/docs/current/warm-standby-failover.html\" target=\"_blank\" rel=\"noopener noreferrer\">failover documentation</a> says it \"does not provide the system software required to identify a failure on the primary.\" It also warns that if a restarted old primary doesn't know it was replaced, both servers may act as primary, which \"will lead to confusion and ultimately data loss.\" So teams add a separate failover tool, and that tool needs testing like any other code.</p>\n<h4 id=\"synchronous-asynchronous-and-semi-synchronous-replication\">Synchronous, asynchronous and semi-synchronous replication</h4>\n<p>When the primary commits a write, does it wait for the replicas? There are three answers, and each trades speed against safety.</p>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th>Mode</th><th>Primary waits for</th><th>Committed data lost if the primary dies</th><th>Extra time per commit</th></tr></thead><tbody><tr><td>Asynchronous</td><td>nothing</td><td>the last moment of writes</td><td>none</td></tr><tr><td>Semi-synchronous</td><td>one replica to receive and log the change</td><td>none</td><td>at least one round trip</td></tr><tr><td>Synchronous</td><td>a replica to save the change</td><td>none, unless both servers die</td><td>at least one round trip</td></tr></tbody></table></div>\n<ul>\n<li><strong>Asynchronous</strong> is the default in both <a href=\"https://dev.mysql.com/doc/refman/8.4/en/replication-semisync.html\" target=\"_blank\" rel=\"noopener noreferrer\">MySQL</a> and <a href=\"https://www.postgresql.org/docs/current/warm-standby.html\" target=\"_blank\" rel=\"noopener noreferrer\">PostgreSQL</a>. PostgreSQL says the delay before a replica sees a change is \"typically under one second\" if the replica keeps up.</li>\n<li><strong>Semi-synchronous</strong> is MySQL's middle option. The replica only has to confirm it received the change, not that it applied it. MySQL says the cost is \"at least the TCP/IP roundtrip time\" on every commit.</li>\n<li><strong>Synchronous</strong> replication, PostgreSQL notes, \"necessarily increases the response time.\" The farther apart the servers are, the longer every write waits.</li>\n</ul>\n<p>The diagram below has a primary and two replicas. Send writes, read from a replica before it has caught up, then fail the primary and watch which acknowledged writes survive when a replica is promoted. Switch between asynchronous and semi-synchronous mode to compare. The delays are illustrative.</p>\n<div class=\"widget-slot\" data-widget=\"replication\"></div>\n<h4 id=\"primary-primary-replication\">Primary-primary replication</h4>\n<p>In <strong>primary-primary replication</strong> (older name: <strong>master-master</strong>), two servers both accept reads and writes. They send each other their changes and coordinate on writes. If either one fails, the other keeps handling both reads and writes, so the site doesn't drop into read-only mode.</p>\n<figure class=\"diagram\" data-text=\"Crumb app servers -> Primary A (reads and writes); Crumb app servers -> Primary B (reads and writes); Primary A <-> Primary B (changes both ways)\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 0 640 263\" width=\"640\" role=\"img\" aria-label=\"Primary-primary replication. Both servers accept writes and copy changes to each other.\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"284\" y1=\"69\" x2=\"125.3\" y2=\"188\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"356\" y1=\"69\" x2=\"514.7\" y2=\"188\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"153\" y1=\"222\" x2=\"487\" y2=\"222\" marker-end=\"url(#dg-arrow)\" marker-start=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-box\" x=\"252\" y=\"18\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"320\" y=\"46.5\">Crumb app servers</tspan></text><path class=\"dg-node dg-store\" d=\"M12,200 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M12,200 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"229.5\">Primary A</tspan></text><path class=\"dg-node dg-store\" d=\"M492,200 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M492,200 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"560\" y=\"229.5\">Primary B</tspan></text><rect class=\"dg-edge-bg\" x=\"150.1\" y=\"119.5\" width=\"109.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"204.7\" y=\"132.5\" text-anchor=\"middle\">reads and writes</text><rect class=\"dg-edge-bg\" x=\"380.7\" y=\"119.5\" width=\"109.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"435.3\" y=\"132.5\" text-anchor=\"middle\">reads and writes</text><rect class=\"dg-edge-bg\" x=\"262.3\" y=\"213\" width=\"115.4\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"320\" y=\"226\" text-anchor=\"middle\">changes both ways</text></svg></div><figcaption>Primary-primary replication. Both servers accept writes and copy changes to each other.</figcaption></figure>\n<p>The catch is that two servers can now change the same data at the same moment. Say a Crumb author edits her recipe's title on Primary A while a moderator edits the same title on Primary B. Unless something resolves the clash, the two servers end up with different titles.</p>\n<h5 id=\"disadvantages-of-primary-primary-replication\">Disadvantages of primary-primary replication</h5>\n<ul>\n<li><strong>Something must choose where each write goes.</strong> You need a load balancer, or code in the app, that picks a primary for each write. Sending all writes for one recipe to the same primary reduces clashes.</li>\n<li><strong>You give up either consistency or speed.</strong> Most primary-primary systems pick one of two compromises. Some are <strong>loosely consistent</strong>: each server commits on its own and they reconcile later, so for a while they disagree. That breaks the isolation and consistency that ACID promises. Others coordinate before every commit, so they stay in agreement, but each write is slower.</li>\n<li><strong>Conflicts multiply.</strong> More servers that accept writes, and more network delay between them, mean more writes that overlap in time, so conflict resolution matters more as the setup grows.</li>\n</ul>\n<p>Even IDs need care. If both servers hand out auto-incrementing IDs, both may create a comment with ID 5,001. MySQL's <a href=\"https://dev.mysql.com/doc/refman/8.4/en/replication-options-source.html\" target=\"_blank\" rel=\"noopener noreferrer\"><code>auto_increment_increment</code> and <code>auto_increment_offset</code></a> settings exist for this case: with an increment of 2, one server uses odd IDs and the other even IDs.</p>\n<p>Some systems resolve conflicts with a fixed rule. In MySQL Group Replication's multi-primary mode, if two servers update the same row at once, the transaction ordered first commits and the other is rolled back. The <a href=\"https://dev.mysql.com/doc/refman/8.4/en/group-replication-summary.html\" target=\"_blank\" rel=\"noopener noreferrer\">documentation</a> calls this \"a distributed first commit wins rule.\"</p>\n<h4 id=\"costs-that-apply-to-all-replication\">Costs that apply to all replication</h4>\n<p>These drawbacks apply to both primary-replica and primary-primary setups.</p>\n<ul>\n<li><strong>Recent writes can be lost.</strong> With asynchronous replication, the primary may commit a write and crash before sending it to any replica. When a replica is promoted, that write is gone. PostgreSQL's <a href=\"https://www.postgresql.org/docs/current/warm-standby.html\" target=\"_blank\" rel=\"noopener noreferrer\">documentation</a> says the amount lost is \"proportional to the replication delay at the time of failover.\"</li>\n<li><strong>Every replica must replay every write.</strong> Besides answering reads, each replica applies every change the primary makes. During a burst of writes, replicas spend much of their capacity replaying them and have less left for reads. Replicas add read capacity, never write capacity.</li>\n<li><strong>More replicas mean more lag.</strong> The primary sends each change to every replica attached to it. More replicas means more sending work, so changes take longer to arrive everywhere. Cascading replication spreads this work out.</li>\n<li><strong>Replicas may apply changes more slowly than the primary makes them.</strong> On the primary, many connections write in parallel. On some systems, a replica applies changes one at a time, on a single thread, and under heavy writes it falls further and further behind. MySQL 8.4 avoids this by default: <a href=\"https://dev.mysql.com/doc/refman/8.4/en/replication-options-replica.html\" target=\"_blank\" rel=\"noopener noreferrer\"><code>replica_parallel_workers</code></a> defaults to 4 applier threads. Set it to 1 and transactions \"are applied sequentially.\"</li>\n<li><strong>More hardware and more moving parts.</strong> Each replica is another server to pay for, monitor, patch and back up.</li>\n</ul>\n<h4 id=\"replication-lag-and-reading-your-own-writes\">Replication lag and reading your own writes</h4>\n<p>The delay between a write on the primary and its arrival on a replica is called <strong>replication lag</strong>. It causes a familiar bug. A Crumb user saves an edit to her recipe, the page reloads from a replica that hasn't received the edit yet, and the old version appears.</p>\n<p>The fix is to guarantee <strong>read-your-writes</strong>: a user always sees their own changes. A simple way is to send a user's reads to the primary for a few seconds after they write. Another is to remember how far along the primary was at the user's write, and read from a replica only once it has caught up to that point. The <a href=\"/learn/cap-and-consistency#guarantees-for-one-user\">CAP chapter</a> covers this and related guarantees.</p>","headings":[{"id":"replication-copies-of-the-whole-database","title":"Replication: copies of the whole database","depth":3},{"id":"primary-replica-replication","title":"Primary-replica replication","depth":4},{"id":"synchronous-asynchronous-and-semi-synchronous-replication","title":"Synchronous, asynchronous and semi-synchronous replication","depth":4},{"id":"primary-primary-replication","title":"Primary-primary replication","depth":4},{"id":"costs-that-apply-to-all-replication","title":"Costs that apply to all replication","depth":4},{"id":"replication-lag-and-reading-your-own-writes","title":"Replication lag and reading your own writes","depth":4}]},{"id":"federation-one-database-per-feature","title":"Federation: one database per feature","html":"<h3 id=\"federation-one-database-per-feature\">Federation: one database per feature</h3>\n<p>Replicas spread out reads, but every write still goes to one primary. Crumb's next step is <strong>federation</strong>, also called <strong>functional partitioning</strong>. It splits the database by feature. Instead of one database holding every table, Crumb runs three:</p>\n<ul>\n<li>a <strong>users</strong> database (accounts, profiles, sign-in),</li>\n<li>a <strong>recipes</strong> database,</li>\n<li>a <strong>comments and ratings</strong> database.</li>\n</ul>\n<figure class=\"diagram\" data-text=\"Crumb app servers -> Users DB (accounts); Crumb app servers -> Recipes DB (recipes); Crumb app servers -> Comments and ratings DB (comments, ratings)\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 0 560 325\" width=\"560\" role=\"img\" aria-label=\"Federation. Each feature gets its own database.\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"140.7\" x2=\"407\" y2=\"63.9\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"151\" y1=\"162\" x2=\"407\" y2=\"162\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"151\" y1=\"183.3\" x2=\"407\" y2=\"260.1\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-box\" x=\"12\" y=\"136\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"158.5\">Crumb app</tspan><tspan x=\"80\" y=\"174.5\">servers</tspan></text><path class=\"dg-node dg-store\" d=\"M412,20 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M412,20 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"480\" y=\"49.5\">Users DB</tspan></text><path class=\"dg-node dg-store\" d=\"M412,140 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M412,140 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"480\" y=\"169.5\">Recipes DB</tspan></text><path class=\"dg-node dg-store\" d=\"M412,258 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M412,258 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"480\" y=\"281.5\">Comments and</tspan><tspan x=\"480\" y=\"297.5\">ratings DB</tspan></text><rect class=\"dg-edge-bg\" x=\"249.2\" y=\"93.3\" width=\"59.6\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"279\" y=\"106.3\" text-anchor=\"middle\">accounts</text><rect class=\"dg-edge-bg\" x=\"252.3\" y=\"153\" width=\"53.4\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"279\" y=\"166\" text-anchor=\"middle\">recipes</text><rect class=\"dg-edge-bg\" x=\"221.3\" y=\"212.7\" width=\"115.4\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"279\" y=\"225.7\" text-anchor=\"middle\">comments, ratings</text></svg></div><figcaption>Federation. Each feature gets its own database.</figcaption></figure>\n<p>Each database can have its own replicas. Federation helps in three ways:</p>\n<ul>\n<li><strong>Less traffic per database.</strong> Each database handles only its feature's reads and writes. Fewer writes per primary means less to replicate, so replication lag drops too.</li>\n<li><strong>More of each database fits in memory.</strong> A database is much faster when the data it needs is already in RAM. In a smaller database, more of the data fits there, so more reads skip the disk. This is a better <strong>cache hit rate</strong>.</li>\n<li><strong>Writes in parallel.</strong> With one primary, every write passes through one server. With three, a burst of new comments doesn't compete with sign-ups, so total write throughput goes up.</li>\n</ul>\n<p>Some teams call this <strong>vertical partitioning</strong> or <strong>vertical sharding</strong>. Be careful with the first name: it also means splitting one table's columns into two tables, which comes up under SQL tuning.</p>\n<h4 id=\"disadvantages-of-federation\">Disadvantages of federation</h4>\n<ul>\n<li><strong>It doesn't help with one huge feature.</strong> Federation only splits by feature. If Crumb's <code>comments</code> table alone outgrows a server, putting it in its own database doesn't fix that. You need sharding.</li>\n<li><strong>The app must know which database to use.</strong> Every query now has to go to the right place. That logic lives in Crumb's code or in a database proxy.</li>\n<li><strong>Joins across databases get hard.</strong> \"Show each comment with the commenter's display name\" used to be one join. Now <code>comments</code> and <code>users</code> are on different servers. You can join them in app code with two queries, or use a feature that lets one database query another, called a <strong>server link</strong> (SQL Server calls it a linked server). In PostgreSQL, the <a href=\"https://www.postgresql.org/docs/current/postgres-fdw.html\" target=\"_blank\" rel=\"noopener noreferrer\"><code>postgres_fdw</code></a> extension lets one server \"access data stored in external PostgreSQL servers.\" Both options are slower and more fragile than a local join.</li>\n<li><strong>More hardware and more complexity.</strong> Three databases, each with replicas, means more servers to run, back up and monitor.</li>\n</ul>\n<h4 id=\"a-real-example-github-splits-its-main-database\">A real example: GitHub splits its main database</h4>\n<p>GitHub <a href=\"https://github.blog/engineering/infrastructure/partitioning-githubs-relational-databases-scale/\" target=\"_blank\" rel=\"noopener noreferrer\">described its federation project</a> in 2021. Much of its core data, including user profiles, repositories, issues and pull requests, lived in one main MySQL cluster called <code>mysql1</code>. An incident on <code>mysql1</code> could hit every feature at once.</p>\n<p>The team worked in two stages:</p>\n<ol>\n<li><strong>Split the tables in the code first.</strong> They grouped tables into <strong>schema domains</strong>, sets of tables used together, such as <code>gists</code>, <code>gist_comments</code> and <code>starred_gists</code>. A query linter flagged any query that touched more than one domain, and a transaction linter did the same for transactions. Engineers rewrote those joins as separate queries or joined the results in application code.</li>\n<li><strong>Then move the tables.</strong> Once a domain stood alone, it could move to its own cluster. One move took 130 of the busiest tables off <code>mysql1</code> at once.</li>\n</ol>\n<p>In 2019, <code>mysql1</code> served about 950,000 queries a second: 900,000 on replicas and 50,000 on the primary. By 2021 the same tables served about 1.2 million queries a second across several clusters, and the average load on each database host had halved.</p>\n<p>The lesson for Crumb: moving tables is mechanical. The real work is finding and removing every join and transaction that crosses a feature boundary.</p>","headings":[{"id":"federation-one-database-per-feature","title":"Federation: one database per feature","depth":3},{"id":"disadvantages-of-federation","title":"Disadvantages of federation","depth":4},{"id":"a-real-example-github-splits-its-main-database","title":"A real example: GitHub splits its main database","depth":4}]},{"id":"sharding-splitting-one-table-across-databases","title":"Sharding: splitting one table across databases","html":"<h3 id=\"sharding-splitting-one-table-across-databases\">Sharding: splitting one table across databases</h3>\n<p>Federation splits tables by feature. <strong>Sharding</strong> splits the <em>rows</em> of one table across several databases. Each database, called a <strong>shard</strong>, holds only part of the rows, and has the same tables as the other shards. Sharding is also called <strong>horizontal partitioning</strong>.</p>\n<p>The column that decides which shard a row belongs to is the <strong>shard key</strong>. Suppose Crumb shards by user. Every user, with that user's recipes, lives on exactly one shard. As Crumb gains users, it adds more shards.</p>\n<figure class=\"diagram\" data-text=\"Crumb app servers -> Shard router (user_id); Shard router -> Shard 1; Shard router -> Shard 2; Shard router -> Shard 3; Shard 1 -> Replica; Shard 2 -> Replica; Shard 3 -> Replica\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 0 800 323\" width=\"800\" role=\"img\" aria-label=\"Sharding by user. Each shard holds some users and has its own replica.\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"162\" x2=\"167\" y2=\"162\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"294\" y1=\"135\" x2=\"412\" y2=\"76\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"311\" y1=\"162\" x2=\"407\" y2=\"162\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"294\" y1=\"189\" x2=\"412\" y2=\"248\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"551\" y1=\"42\" x2=\"647\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"551\" y1=\"162\" x2=\"647\" y2=\"162\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"551\" y1=\"282\" x2=\"647\" y2=\"282\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-box\" x=\"12\" y=\"136\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"158.5\">Crumb app</tspan><tspan x=\"80\" y=\"174.5\">servers</tspan></text><rect class=\"dg-node dg-box\" x=\"172\" y=\"138\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"166.5\">Shard router</tspan></text><path class=\"dg-node dg-store\" d=\"M412,20 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M412,20 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"480\" y=\"49.5\">Shard 1</tspan></text><path class=\"dg-node dg-store\" d=\"M412,140 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M412,140 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"480\" y=\"169.5\">Shard 2</tspan></text><path class=\"dg-node dg-store\" d=\"M412,260 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M412,260 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"480\" y=\"289.5\">Shard 3</tspan></text><path class=\"dg-node dg-store\" d=\"M652,20 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M652,20 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"720\" y=\"49.5\">Replica</tspan></text><path class=\"dg-node dg-store\" d=\"M652,140 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M652,140 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"720\" y=\"169.5\">Replica</tspan></text><path class=\"dg-node dg-store\" d=\"M652,260 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M652,260 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"720\" y=\"289.5\">Replica</tspan></text><rect class=\"dg-edge-bg\" x=\"132.3\" y=\"114\" width=\"53.4\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"159\" y=\"127\" text-anchor=\"middle\">user_id</text></svg></div><figcaption>Sharding by user. Each shard holds some users and has its own replica.</figcaption></figure>\n<p>The <strong>shard router</strong> in the diagram is whatever maps a shard key to a shard. It can be a library inside the app, a proxy, or a lookup table.</p>\n<h4 id=\"what-sharding-buys\">What sharding buys</h4>\n<p>Sharding has the same benefits as federation. Each shard gets less traffic and has less to replicate, and more of its data fits in memory. Ten shards means ten primaries taking writes in parallel. It adds two more:</p>\n<ul>\n<li><strong>Smaller indexes.</strong> An index on a shard covers only that shard's rows. A smaller index is quicker to search and more likely to stay in memory, so queries usually get faster.</li>\n<li><strong>A failure stays contained.</strong> If shard 2 goes down, users on shards 1 and 3 don't notice. Users on shard 2 lose access, though, so each shard still needs its own replicas to avoid losing data.</li>\n</ul>\n<h4 id=\"choosing-a-shard-key\">Choosing a shard key</h4>\n<p>Two common ways to shard a <code>users</code> table are by the first letter of the user's last name, or by where the user lives. Both are easy to understand, and both show what can go wrong.</p>\n<ul>\n<li><strong>By last-name initial.</strong> Shard 1 holds names A to F, shard 2 holds G to M, and so on. But letters are not equally common as initials, so some shards end up much larger than others. And a user who changes her name has to move shards.</li>\n<li><strong>By geography.</strong> European users go on a shard in Europe and American users on one in the US. Data sits close to its users, which cuts network delay. But regions are rarely the same size, and users move.</li>\n<li><strong>By a hash of the user's ID.</strong> A <strong>hash function</strong> turns a value into a number that looks random, and the same input always gives the same output. Mapping <code>hash(user_id)</code> to a shard spreads users evenly, which is why many systems use hashing or a lookup table.</li>\n</ul>\n<p>Whatever you choose, keep data that is used together on the same shard. A recipe page needs the recipe plus all its comments, so Crumb should store each recipe's comments on the recipe's shard, not the commenter's.</p>\n<h4 id=\"disadvantages-of-sharding\">Disadvantages of sharding</h4>\n<ul>\n<li><strong>The app must know about shards.</strong> Every query has to reach the right shard. A query that doesn't include the shard key, such as \"find all recipes with 'miso' in the title,\" has to ask every shard and merge the answers. SQL that used to be one statement becomes several statements plus code.</li>\n<li><strong>Shards can become lopsided.</strong> Suppose a celebrity chef joins Crumb and two million people follow her. Her recipes, and every comment and rating on them, live on one shard. That shard now gets far more traffic than the others. An overloaded shard like this is called a <strong>hot shard</strong>.</li>\n<li><strong>Rebalancing is hard.</strong> Fixing a hot shard, or adding shards as Crumb grows, means moving rows between live databases without losing writes. With plain <code>hash(user_id) % N</code>, changing N moves most rows to a new shard. <strong>Consistent hashing</strong> is a different way to map keys to shards. When a shard is added, it moves only a small share of the keys. The <a href=\"/learn/consistent-hashing#the-problem-with-hash-mod-n\">consistent hashing chapter</a> shows why.</li>\n<li><strong>Joins across shards are hard.</strong> A query that needs rows from two shards can't be one SQL join. The app must query each shard and combine the results. A transaction that changes rows on two shards needs a protocol such as two-phase commit, covered in the <a href=\"/learn/distributed-transactions\">distributed transactions chapter</a>.</li>\n<li><strong>More hardware and more complexity.</strong> Each shard needs its own replicas, backups, monitoring and upgrades.</li>\n</ul>\n<p><a href=\"/learn/database-internals#sharding\">Database internals</a> goes deeper on all of this: range, hash and directory partitioning, hot keys, queries that must ask every shard (scatter-gather), cross-shard joins, resharding, and distributed SQL databases such as Spanner that shard for you.</p>\n<h4 id=\"a-real-example-notion-shards-postgresql\">A real example: Notion shards PostgreSQL</h4>\n<p>Notion stores every piece of content as a \"block\" in PostgreSQL. In 2021 it <a href=\"https://www.notion.com/blog/sharding-postgres-at-notion\" target=\"_blank\" rel=\"noopener noreferrer\">described</a> moving from one large database to a sharded one.</p>\n<ul>\n<li><strong>Why.</strong> After \"four orders of magnitude of growth,\" on-call engineers \"often woke up to database CPU spikes,\" and PostgreSQL's cleanup process, <code>VACUUM</code>, \"began to stall consistently.\"</li>\n<li><strong>The shard key.</strong> Every block belongs to exactly one workspace, and users usually work in one workspace at a time. So Notion sharded by workspace ID, and most queries need only one shard.</li>\n<li><strong>The layout.</strong> Notion created 480 <strong>logical shards</strong> spread across 32 physical databases, 15 per database. A logical shard is a self-contained set of tables that can later move to another server whole. Notion chose 480 because it is \"divisible by a lot of numbers,\" so it can grow from 32 servers to 40 or 48 and still spread shards evenly.</li>\n<li><strong>The move.</strong> New writes went to both the old and new databases while a backfill copied existing data, which took about three days. After a verification step, the final switch needed five minutes of scheduled downtime.</li>\n</ul>\n<p>Notion's own top lesson was to shard earlier, before the overloaded database limited its options.</p>","headings":[{"id":"sharding-splitting-one-table-across-databases","title":"Sharding: splitting one table across databases","depth":3},{"id":"what-sharding-buys","title":"What sharding buys","depth":4},{"id":"choosing-a-shard-key","title":"Choosing a shard key","depth":4},{"id":"disadvantages-of-sharding","title":"Disadvantages of sharding","depth":4},{"id":"a-real-example-notion-shards-postgresql","title":"A real example: Notion shards PostgreSQL","depth":4}]},{"id":"denormalization","title":"Denormalization","html":"<h3 id=\"denormalization\">Denormalization</h3>\n<p><strong>Denormalization</strong> means storing extra copies of data so that common reads don't need joins. It makes reads faster at the cost of some write speed.</p>\n<p>Take Crumb's recipe page. In a fully normalized design, it needs:</p>\n<ul>\n<li>the recipe row,</li>\n<li>a join to <code>users</code> for the author's display name,</li>\n<li>a count over <code>comments</code> for the number of comments,</li>\n<li>an average over <code>ratings</code> for the star rating.</li>\n</ul>\n<p>A denormalized design stores <code>author_name</code>, <code>comment_count</code>, <code>rating_sum</code> and <code>rating_count</code> directly on the recipe row. One row read now builds the page. You already saw this idea in the ratings transaction earlier in this chapter.</p>\n<h4 id=\"why-it-pays-off\">Why it pays off</h4>\n<p><strong>Reads usually far outnumber writes.</strong> A common rule of thumb is 100 reads per write, or even 1,000. Facebook's TAO, the system that serves its social graph, reports \"<a href=\"https://www.usenix.org/conference/atc13/technical-sessions/presentation/bronson\" target=\"_blank\" rel=\"noopener noreferrer\">a billion reads and millions of writes each second</a>.\" When reads outnumber writes that heavily, an expensive join on every read costs more than a little extra work on every write. A complex join can spend much of its time reading from disk.</p>\n<p><strong>Splitting data makes joins harder.</strong> After federation or sharding, the data a join needs may live on different servers, or even in different data centers. Denormalizing can remove the need for those joins. If each comment row stores the commenter's display name, Crumb can show comments without asking the users database at all.</p>\n<h4 id=\"materialized-views\">Materialized views</h4>\n<p>Some databases can manage the extra copy for you. A <strong>view</strong> is a saved query that runs each time you read it. A <strong>materialized view</strong> saves the query's <em>results</em> as a table, so reading it is as fast as reading a table.</p>\n<ul>\n<li>In <strong>PostgreSQL</strong>, a materialized view is <a href=\"https://www.postgresql.org/docs/current/rules-materializedviews.html\" target=\"_blank\" rel=\"noopener noreferrer\">updated only when you run</a> <code>REFRESH MATERIALIZED VIEW</code>. The documentation's example is a sales summary rebuilt each night for a dashboard. Crumb could use one for a \"top recipes this week\" page that is fine being an hour out of date.</li>\n<li><strong>Oracle</strong> can refresh a materialized view <a href=\"https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-MATERIALIZED-VIEW.html\" target=\"_blank\" rel=\"noopener noreferrer\">as part of each commit</a> to the underlying tables, using <code>REFRESH ... ON COMMIT</code>. Its documentation warns this \"may increase the time taken to complete the commit.\"</li>\n</ul>\n<h4 id=\"keeping-copies-in-sync\">Keeping copies in sync</h4>\n<p>Each copy must change when the original changes. If an author renames herself, every recipe row that holds her old name must change too. The options are:</p>\n<ul>\n<li><strong>Update both in the same transaction,</strong> as Crumb does with <code>rating_count</code>.</li>\n<li><strong>Use a trigger,</strong> a piece of code the database runs automatically when a row changes.</li>\n<li><strong>Use constraints.</strong> For example, a foreign key on <code>(author_id, author_name)</code> that references <code>users (id, display_name)</code> with <code>ON UPDATE CASCADE</code> makes the database copy each name change into every recipe. This needs a unique constraint on the pair in <code>users</code>.</li>\n</ul>\n<p>Constraints and triggers keep copies correct, but they make the schema harder to understand and change.</p>\n<h4 id=\"disadvantages-of-denormalization\">Disadvantages of denormalization</h4>\n<ul>\n<li><strong>Data is duplicated.</strong> It takes more storage, and any bug in the sync logic leaves copies that disagree.</li>\n<li><strong>Keeping copies in sync adds complexity.</strong> Constraints and triggers help, but every extra rule makes the database design harder to follow.</li>\n<li><strong>It can be slower under heavy writes.</strong> Each write now updates several places. Worse, popular rows become contention points. If thousands of people comment on one viral recipe, every comment must lock that recipe's row to bump <code>comment_count</code>. Under a heavy write load, a denormalized database can perform worse than the normalized one.</li>\n</ul>","headings":[{"id":"denormalization","title":"Denormalization","depth":3},{"id":"why-it-pays-off","title":"Why it pays off","depth":4},{"id":"materialized-views","title":"Materialized views","depth":4},{"id":"keeping-copies-in-sync","title":"Keeping copies in sync","depth":4},{"id":"disadvantages-of-denormalization","title":"Disadvantages of denormalization","depth":4}]},{"id":"sql-tuning","title":"SQL tuning","html":"<h3 id=\"sql-tuning\">SQL tuning</h3>\n<p><strong>SQL tuning</strong> means making each query do less work. Whole books cover it, but it always starts with measurement, because guessed fixes often miss. <strong>Benchmarking</strong> creates heavy load on purpose, so you see how the system behaves before real users do. <strong>Profiling</strong> watches the system under load to find which queries use the most time.</p>\n<h4 id=\"benchmark-create-the-load-yourself\">Benchmark: create the load yourself</h4>\n<p><a href=\"https://httpd.apache.org/docs/2.4/programs/ab.html\" target=\"_blank\" rel=\"noopener noreferrer\"><code>ab</code></a>, Apache's HTTP benchmarking tool, sends many requests to a URL and reports how many requests per second the server handled. <code>-n</code> sets the total number of requests and <code>-c</code> sets how many run at once:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span>ab -n 10000 -c 50 https://staging.crumb.example/recipes/101</span></span></code></pre>\n<p><code>ab</code> tests the whole page, app and database together. To load the database directly, PostgreSQL ships <a href=\"https://www.postgresql.org/docs/current/pgbench.html\" target=\"_blank\" rel=\"noopener noreferrer\"><code>pgbench</code></a>, which runs your SQL from many simulated clients and reports transactions per second.</p>\n<h4 id=\"profile-find-the-slow-queries\">Profile: find the slow queries</h4>\n<p>MySQL's <a href=\"https://dev.mysql.com/doc/refman/8.4/en/slow-query-log.html\" target=\"_blank\" rel=\"noopener noreferrer\">slow query log</a> records every statement that takes longer than <code>long_query_time</code> seconds. It is off by default, and the default threshold of 10 seconds is far too high for a website, so Crumb might set it to 0.2. The <code>mysqldumpslow</code> tool then groups similar queries so you can see which cost the most in total. Total time is what matters: a 50 ms query that runs 2,000 times a minute costs more than a 5-second query that runs once an hour.</p>\n<p>Once you know which query is slow, <code>EXPLAIN</code> shows how the database plans to run it. In PostgreSQL's <a href=\"https://www.postgresql.org/docs/current/using-explain.html\" target=\"_blank\" rel=\"noopener noreferrer\">example</a>, a query with no filter gets a <code>Seq Scan</code>, which reads every row. A lookup on an indexed column gets an <code>Index Scan</code>, which jumps straight to the matching rows. <code>EXPLAIN ANALYZE</code> actually runs the query and adds real times and row counts.</p>\n<p>These measurements usually point to one of the fixes below.</p>\n<h4 id=\"tighten-up-the-schema\">Tighten up the schema</h4>\n<p>Databases store tables on disk in fixed-size blocks called <strong>pages</strong>. InnoDB's default page is <a href=\"https://dev.mysql.com/doc/refman/8.4/en/innodb-limits.html\" target=\"_blank\" rel=\"noopener noreferrer\">16 KB</a>, and PostgreSQL's is <a href=\"https://www.postgresql.org/docs/current/storage-page-layout.html\" target=\"_blank\" rel=\"noopener noreferrer\">usually 8 kB</a>. Reading one row means reading its whole page. Smaller rows mean more rows per page, so each disk read brings in more useful data, and more of the table fits in memory. That is why column types matter.</p>\n<p><strong><code>CHAR</code> for fixed-length values.</strong> <code>CHAR(4)</code> always takes 4 bytes: MySQL <a href=\"https://dev.mysql.com/doc/refman/8.4/en/char.html\" target=\"_blank\" rel=\"noopener noreferrer\">pads shorter values with spaces</a>. <code>VARCHAR(4)</code> stores a length prefix plus the actual characters, so <code>'ab'</code> takes 3 bytes. Because every fixed-length value is the same size, the database can jump straight to one without first reading the lengths of the ones before it. In MySQL's MyISAM engine, a table with no variable-length columns uses a <a href=\"https://dev.mysql.com/doc/refman/8.4/en/data-size.html\" target=\"_blank\" rel=\"noopener noreferrer\">fixed-size row format</a> that is \"faster but may waste some space.\" So <code>CHAR</code> suits values that always have the same length, such as a two-letter country code. This doesn't hold everywhere: PostgreSQL's <a href=\"https://www.postgresql.org/docs/current/datatype-character.html\" target=\"_blank\" rel=\"noopener noreferrer\">documentation</a> says its <code>character(n)</code> \"is usually the slowest of the three\" string types.</p>\n<p><strong><code>TEXT</code> for long text.</strong> Use <code>TEXT</code> for large blocks of text such as recipe instructions. InnoDB can store long values <a href=\"https://dev.mysql.com/doc/refman/8.4/en/innodb-row-format.html\" target=\"_blank\" rel=\"noopener noreferrer\">off-page</a>, leaving only \"a 20-byte pointer\" in the row, which keeps rows small. <code>TEXT</code> columns can also have a <a href=\"https://dev.mysql.com/doc/refman/8.4/en/fulltext-search.html\" target=\"_blank\" rel=\"noopener noreferrer\">full-text index</a>, which supports <strong>boolean searches</strong>: <code>'+miso -pork'</code> in <code>IN BOOLEAN MODE</code> finds recipes that mention miso but not pork.</p>\n<p><strong><code>INT</code> for whole numbers up to about 4 billion.</strong> An <a href=\"https://dev.mysql.com/doc/refman/8.4/en/integer-types.html\" target=\"_blank\" rel=\"noopener noreferrer\"><code>INT</code> takes 4 bytes</a> (32 bits). It holds up to 2,147,483,647, or 4,294,967,295 (2^32 − 1) when declared <code>UNSIGNED</code>. Use <code>BIGINT</code> (8 bytes) for IDs that might pass that.</p>\n<p><strong><code>DECIMAL</code> for money.</strong> <code>FLOAT</code> and <code>DOUBLE</code> store <a href=\"https://dev.mysql.com/doc/refman/8.4/en/floating-point-types.html\" target=\"_blank\" rel=\"noopener noreferrer\">approximate values</a>. In double-precision floating point, 0.1 + 0.2 comes out as 0.30000000000000004. That is wrong for a subscription price. <code>DECIMAL</code> stores <a href=\"https://dev.mysql.com/doc/refman/8.4/en/fixed-point-types.html\" target=\"_blank\" rel=\"noopener noreferrer\">exact values</a>, and MySQL names \"monetary data\" as its main use.</p>\n<p><strong>Keep large files out.</strong> Large binary values (<strong>BLOBs</strong>), such as Crumb's recipe photos, bloat the tables, the backups and the replication stream. Store the file in object storage and keep only its location, such as a URL, in the database.</p>\n<p><strong>What <code>VARCHAR(255)</code> is about.</strong> One byte (8 bits) can count up to 255. MySQL gives a <code>VARCHAR</code> a one-byte length prefix when its values need at most 255 <em>bytes</em>, and two bytes above that. So in some databases, 255 is the longest a column can be while making full use of a one-byte prefix. Two details limit how much this matters. The limit counts bytes, not characters, and in MySQL's <code>utf8mb4</code> character set one character can take 4 bytes. And in PostgreSQL the <a href=\"https://www.postgresql.org/docs/current/datatype-character.html\" target=\"_blank\" rel=\"noopener noreferrer\">one-byte header</a> covers strings up to 126 bytes, so 255 means nothing special there. Pick lengths from what the data needs.</p>\n<p><strong>Use <code>NOT NULL</code> where you can.</strong> MySQL's <a href=\"https://dev.mysql.com/doc/refman/8.4/en/data-size.html\" target=\"_blank\" rel=\"noopener noreferrer\">advice</a> is that <code>NOT NULL</code> \"makes SQL operations faster, by enabling better use of indexes and eliminating overhead for testing whether each value is NULL.\" If a value truly can be missing, <code>NULL</code> is fine. Just don't allow it everywhere by default.</p>\n<h4 id=\"use-good-indexes\">Use good indexes</h4>\n<p>An <strong>index</strong> is a separate, sorted structure that lets the database find rows without reading the whole table, like the index at the back of a book.</p>\n<p><strong>Which columns to index.</strong> Columns used in <code>WHERE</code>, <code>JOIN</code>, <code>ORDER BY</code> and <code>GROUP BY</code> are usually faster with an index, and columns a query only reads in its <code>SELECT</code> can be added to an index so the query never visits the table. Crumb's recipe page runs this query on every view:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">SELECT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> author_id, body, created_at </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">FROM</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> comments</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">WHERE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> recipe_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 101</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ORDER BY</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> created_at </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">DESC</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> LIMIT</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 20</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span></code></pre>\n<p>Without an index, the database scans every comment on the site and then sorts the matches. An index on <code>(recipe_id, created_at)</code> lets it jump to recipe 101's comments, already in date order, and stop after 20.</p>\n<p><strong>How indexes work.</strong> Most indexes are <strong>B-trees</strong>: shallow trees that keep keys sorted and rebalance themselves as rows come and go. Finding, inserting or deleting a key takes <strong>logarithmic time</strong>, so a table a thousand times larger needs only a few more steps per lookup. <a href=\"/learn/database-internals#indexes-getting-query-performance-right\">Database internals</a> covers B-trees, covering indexes and why column order matters.</p>\n<p><strong>What indexes cost.</strong> An index takes space, and to be fast it must stay in memory, where it competes with the table. Writes get slower too, because every insert, update or delete must also update each index.</p>\n<p><strong>Loading lots of data.</strong> During a big load, it can be faster to drop the indexes, load the data, then rebuild them. PostgreSQL's <a href=\"https://www.postgresql.org/docs/current/populate.html\" target=\"_blank\" rel=\"noopener noreferrer\">documentation</a> agrees, and warns that queries run slower while the index is missing.</p>\n<h4 id=\"avoid-expensive-joins\">Avoid expensive joins</h4>\n<p>A join between a few rows on indexed keys is cheap. A join across large tables on every page view can dominate a database's load. Where profiling shows such a join is the bottleneck, <a href=\"/learn/relational-databases#denormalization\">denormalize</a> the data it needs or cache the result.</p>\n<h4 id=\"partition-tables\">Partition tables</h4>\n<p>Some parts of a table are much busier than others. Crumb's <code>recipes</code> row holds long instructions that rarely change, and a <code>view_count</code> that changes on every page view. Moving the busy column into a small table, <code>recipe_stats (recipe_id, view_count)</code>, keeps the hot data compact, so it stays in memory.</p>\n<p>Databases can also split a table by rows inside one server, called <strong>table partitioning</strong>. Crumb could partition <code>comments</code> by month, so the busy recent months stay small. PostgreSQL's <a href=\"https://www.postgresql.org/docs/current/ddl-partitioning.html\" target=\"_blank\" rel=\"noopener noreferrer\">documentation</a> says this helps most \"when most of the heavily accessed rows of the table are in a single partition or a small number of partitions.\"</p>\n<h4 id=\"pool-connections\">Pool connections</h4>\n<p>In PostgreSQL, each connection gets its own <a href=\"https://www.postgresql.org/docs/current/connect-estab.html\" target=\"_blank\" rel=\"noopener noreferrer\">server process</a>. If each of Crumb's app servers opens dozens of connections, the database spends memory on hundreds of mostly idle processes. A <strong>connection pool</strong> keeps a few open connections and lends them to requests as needed. In the transaction mode of <a href=\"https://www.pgbouncer.org/features.html\" target=\"_blank\" rel=\"noopener noreferrer\">PgBouncer</a>, a common pooling proxy, a client holds a real connection \"only during a transaction.\"</p>\n<h4 id=\"the-query-cache-and-why-its-gone\">The query cache, and why it's gone</h4>\n<p>MySQL used to have a built-in <strong>query cache</strong>. It <a href=\"https://web.archive.org/web/20260811114045/https://dev.mysql.com/doc/refman/5.7/en/query-cache.html\" target=\"_blank\" rel=\"noopener noreferrer\">stored the text of each <code>SELECT</code> together with its result</a> and answered identical queries from it. Any change to a table threw away every cached result that used it.</p>\n<p>It didn't scale. Every query passed through one shared cache, so busy servers with many cores queued up on it. MySQL switched it off by default in 5.6 (2013), and in 2017 its team <a href=\"https://dev.mysql.com/blog-archive/mysql-8-0-retiring-support-for-the-query-cache/\" target=\"_blank\" rel=\"noopener noreferrer\">wrote</a> that it \"is known to not scale with high-throughput workloads on multi-core machines.\" Percona's <a href=\"https://www.percona.com/blog/mysql-5-7-performance-tuning-immediately-after-installation/\" target=\"_blank\" rel=\"noopener noreferrer\">2016 tuning advice</a> also kept it off. It was deprecated in MySQL 5.7.20, and MySQL 8.0's <a href=\"https://dev.mysql.com/doc/refman/8.0/en/mysql-nutshell.html\" target=\"_blank\" rel=\"noopener noreferrer\">list of changes</a> says: \"The query cache was removed.\"</p>\n<p>So on current MySQL there is nothing to tune. Cache results in the application instead, where you control what is stored and when it expires. <a href=\"/learn/cache#keeping-the-cache-consistent-with-the-database\">Caching</a> covers keeping such a cache in step with the database.</p>","headings":[{"id":"sql-tuning","title":"SQL tuning","depth":3},{"id":"benchmark-create-the-load-yourself","title":"Benchmark: create the load yourself","depth":4},{"id":"profile-find-the-slow-queries","title":"Profile: find the slow queries","depth":4},{"id":"tighten-up-the-schema","title":"Tighten up the schema","depth":4},{"id":"use-good-indexes","title":"Use good indexes","depth":4},{"id":"avoid-expensive-joins","title":"Avoid expensive joins","depth":4},{"id":"partition-tables","title":"Partition tables","depth":4},{"id":"pool-connections","title":"Pool connections","depth":4},{"id":"the-query-cache-and-why-its-gone","title":"The query cache, and why it's gone","depth":4}]},{"id":"check-yourself","title":"Check yourself","html":"<h3 id=\"check-yourself\">Check yourself</h3>\n<details>\n<summary>Crumb uses asynchronous primary-replica replication. At 7 p.m. the primary's disk fails. What do users see in the next few minutes, what data might be lost, and what has to happen before comments work again?</summary>\n<p>Reads keep working because the replicas are still up, so people can browse recipes, but posting a comment or rating fails: Crumb is in read-only mode. Writes the primary committed but hadn't yet sent are lost. That is usually under a second of writes, more if replicas were lagging. To restore writes, Crumb must confirm the primary is really dead, promote the most up-to-date replica, point the app and other replicas at it, and make sure the old primary can't return as a second primary. Semi-synchronous or synchronous replication would prevent the loss, at the cost of a round trip on every commit.</p>\n</details>\n<details>\n<summary>A teammate proposes sharding Crumb's users by the first letter of their last name, with four shards. Name two problems, and suggest a better key.</summary>\n<p>Letters are not equally common as initials, so the shards will be uneven. And a user who changes her name would have to move to another shard. A hash of the user ID spreads users evenly and never changes. Use consistent hashing or a lookup table, so that adding a fifth shard moves only a small share of users.</p>\n</details>\n<details>\n<summary>Crumb's recipe page joins four tables on every view, and recipes are read about 500 times for every comment or rating. What would you denormalize, how would you keep it correct, and when would this backfire?</summary>\n<p>Store <code>author_name</code>, <code>comment_count</code>, <code>rating_sum</code> and <code>rating_count</code> on the recipe row, so one row read builds the page. Update the counts in the same transaction that inserts the comment or rating, and keep the author name in sync with a cascading foreign key or a trigger. At 500 reads per write, saving a four-table join on every read is worth a little extra work on each write. It backfires when one recipe gets a flood of writes. Then every new comment has to lock the same recipe row to bump the count, and those writes queue up behind each other.</p>\n</details>\n<details>\n<summary>The slow query log shows the comment list query on recipe pages taking 300 ms. How would you confirm the cause, what index would you add, and what does that index cost?</summary>\n<p>Run <code>EXPLAIN ANALYZE</code> on the query. A sequential scan over <code>comments</code> followed by a sort confirms that no useful index exists. Add an index on <code>(recipe_id, created_at)</code>. The database can then jump to one recipe's comments, which are already in date order, and stop after 20 rows. The costs: the index takes disk space and memory, and every new comment must now also update the index, so writes get a little slower.</p>\n</details>\n<details>\n<summary>Crumb's write traffic is growing. Someone suggests primary-primary replication between the US and Europe to double write capacity. What's wrong with that reasoning?</summary>\n<p>It doesn't double write capacity, because every write still has to be applied on both servers: each primary replays the other's writes as well as its own. It also adds conflicts. If the servers coordinate before each commit, every write waits for a transatlantic round trip. If they don't, the two copies disagree for a while, and ACID's guarantees no longer hold across them. For more write capacity, Crumb should look at federation or sharding, where each write goes to only one primary.</p>\n</details>\n<details>\n<summary>A new engineer reads an old tuning guide and wants to turn on MySQL's query cache. What do you tell them?</summary>\n<p>On MySQL 8.0 and later there is nothing to turn on, because the query cache was removed. It was deprecated in 5.7.20 and had been off by default since 5.6. It didn't scale on busy multi-core servers, and any change to a table threw away every cached result for that table. If the goal is to avoid repeating expensive queries, cache the results in the application or in a cache like Redis, where you control what is stored and when it expires.</p>\n</details>\n<h3 id=\"further-reading\">Further reading</h3>\n<p>Classic references on these techniques (all were reachable when this chapter was written):</p>\n<ul>\n<li><a href=\"https://www.slideshare.net/jboner/scalability-availability-stability-patterns\" target=\"_blank\" rel=\"noopener noreferrer\">Scalability, availability &#x26; stability patterns</a>: Jonas Bonér's slide deck. Its diagrams of master-slave and master-master replication are still a clear overview.</li>\n<li><a href=\"https://en.wikipedia.org/wiki/Multi-master_replication\" target=\"_blank\" rel=\"noopener noreferrer\">Multi-master replication</a>: Wikipedia's overview of primary-primary replication and its trade-offs.</li>\n<li><a href=\"https://www.youtube.com/watch?v=kKjm4ehYiMs\" target=\"_blank\" rel=\"noopener noreferrer\">Scaling up to your first 10 million users</a>: an AWS re:Invent 2019 talk on changing an app's architecture step by step as its users grow.</li>\n<li><a href=\"https://stackoverflow.com/questions/5145637/querying-data-by-joining-two-tables-in-two-database-on-different-servers\" target=\"_blank\" rel=\"noopener noreferrer\">Querying data by joining two tables in two databases on different servers</a>: a Stack Overflow thread showing cross-server joins with linked servers.</li>\n<li><a href=\"https://highscalability.com/an-unorthodox-approach-to-database-design-the-coming-of-the/\" target=\"_blank\" rel=\"noopener noreferrer\">An unorthodox approach to database design: the coming of the shard</a>: a 2009 High Scalability article on early sharding at Flickr and others.</li>\n<li><a href=\"https://en.wikipedia.org/wiki/Shard_(database_architecture)\" target=\"_blank\" rel=\"noopener noreferrer\">Shard (database architecture)</a>: Wikipedia's overview of sharding and its history.</li>\n<li><a href=\"https://www.paperplanes.de/2011/12/9/the-magic-of-consistent-hashing.html\" target=\"_blank\" rel=\"noopener noreferrer\">The simple magic of consistent hashing</a>: Mathias Meyer's short explanation of consistent hashing, with Riak as the main example.</li>\n<li><a href=\"https://en.wikipedia.org/wiki/Denormalization\" target=\"_blank\" rel=\"noopener noreferrer\">Denormalization</a>: Wikipedia's overview of denormalization and its trade-offs.</li>\n<li><a href=\"https://httpd.apache.org/docs/2.4/programs/ab.html\" target=\"_blank\" rel=\"noopener noreferrer\">ab, the Apache HTTP server benchmarking tool</a>: the reference for every <code>ab</code> option, including its known limits.</li>\n<li><a href=\"https://dev.mysql.com/doc/refman/8.4/en/slow-query-log.html\" target=\"_blank\" rel=\"noopener noreferrer\">The slow query log</a>: MySQL's reference for turning on and reading the slow query log.</li>\n<li><a href=\"https://stackoverflow.com/questions/1217466/is-there-a-good-reason-i-see-varchar255-used-so-often-as-opposed-to-another-l\" target=\"_blank\" rel=\"noopener noreferrer\">Is there a good reason I see VARCHAR(255) used so often?</a>: the Stack Overflow thread behind the length-prefix and index-limit explanations.</li>\n<li><a href=\"https://stackoverflow.com/questions/1017239/how-do-null-values-affect-performance-in-a-database-search\" target=\"_blank\" rel=\"noopener noreferrer\">How do NULL values affect performance in a database search?</a>: answers on how nullable columns interact with indexes in different databases.</li>\n<li><a href=\"https://web.archive.org/web/20230328150100/https://aiddroid.com/10-tips-optimizing-mysql-queries-dont-suck/\" target=\"_blank\" rel=\"noopener noreferrer\">10 tips for optimizing MySQL queries (that don't suck)</a>: Jesse Farmer's 2008 list (archived repost). Its core advice, profile first, still holds.</li>\n<li><a href=\"https://web.archive.org/web/20260811114045/https://dev.mysql.com/doc/refman/5.7/en/query-cache.html\" target=\"_blank\" rel=\"noopener noreferrer\">The MySQL query cache</a>: MySQL 5.7's description of the query cache (archived copy), with the deprecation notice at the top.</li>\n<li><a href=\"https://www.percona.com/blog/mysql-5-7-performance-tuning-immediately-after-installation/\" target=\"_blank\" rel=\"noopener noreferrer\">MySQL 5.7 performance tuning immediately after installation</a>: Percona's 2016 checklist of settings for a new MySQL 5.7 server.</li>\n</ul>\n<p>Official documentation worth reading in full:</p>\n<ul>\n<li><a href=\"https://dev.mysql.com/doc/refman/8.4/en/replication-semisync.html\" target=\"_blank\" rel=\"noopener noreferrer\">MySQL semisynchronous replication</a>: a clear comparison of asynchronous, semi-synchronous and fully synchronous replication.</li>\n<li><a href=\"https://www.postgresql.org/docs/current/warm-standby.html\" target=\"_blank\" rel=\"noopener noreferrer\">PostgreSQL: log-shipping standby servers</a>: how PostgreSQL replicas, cascading replication and synchronous commit work.</li>\n<li><a href=\"https://www.postgresql.org/docs/current/using-explain.html\" target=\"_blank\" rel=\"noopener noreferrer\">PostgreSQL: using EXPLAIN</a>: a careful tutorial on reading query plans.</li>\n<li><a href=\"https://dev.mysql.com/doc/refman/8.4/en/data-size.html\" target=\"_blank\" rel=\"noopener noreferrer\">MySQL: optimizing data size</a>: short, practical advice on column types, <code>NOT NULL</code> and row formats.</li>\n</ul>\n<p>Real systems:</p>\n<ul>\n<li><a href=\"https://github.blog/engineering/infrastructure/partitioning-githubs-relational-databases-scale/\" target=\"_blank\" rel=\"noopener noreferrer\">Partitioning GitHub's relational databases to handle scale</a>: GitHub's 2021 account of splitting <code>mysql1</code> by schema domain, including its query and transaction linters.</li>\n<li><a href=\"https://www.notion.com/blog/sharding-postgres-at-notion\" target=\"_blank\" rel=\"noopener noreferrer\">Herding elephants: lessons learned from sharding Postgres at Notion</a>: Notion's 2021 write-up of moving to 480 logical shards.</li>\n<li><a href=\"https://www.usenix.org/conference/atc13/technical-sessions/presentation/bronson\" target=\"_blank\" rel=\"noopener noreferrer\">TAO: Facebook's distributed data store for the social graph</a>: the 2013 USENIX paper on a read-heavy system built on MySQL.</li>\n</ul>\n<p>Related chapters on this site: <a href=\"/learn/database-internals\">Database internals</a> for storage engines, indexes and sharding strategies; <a href=\"/learn/nosql-databases\">NoSQL and choosing a database</a> for the alternatives to relational databases; and <a href=\"/learn/cap-and-consistency\">CAP and consistency</a> for what replication lag means for consistency.</p>","headings":[{"id":"check-yourself","title":"Check yourself","depth":3},{"id":"further-reading","title":"Further reading","depth":3}]}]}