Caching & Databases · lesson 6 of 8 · about 26 min
SQL vs NoSQL
How to choose a database from the shape of your data and your access patterns: the five families and what each trades away, embedding versus joining, why queries that do not include the shard key are slow, what a graph store really saves, how much a single node can carry, and the cost of picking for scale you do not have. Tap any for a plain-language card.
“SQL or NoSQL?” is the wrong question. There are five shapes of database, and each is good at something different.
A useful rule: pick a relational database by default (it handles surprisingly large workloads and is the easiest to change your mind about), and pick something else when a specific access pattern or scale demands it. Tap each family to see its data model, where it shines and where it hurts.
Look at what each family cannot do: the weakness is usually the reason it can do the other thing so well.
Data model
Tables of rows with a fixed schema; relationships through foreign keys; a declarative query language.
Examples
PostgreSQL, MySQL, SQL Server
Typical query
SELECT … JOIN … WHERE … GROUP BY
Good at
Ad-hoc queries and joins, transactions across rows, constraints that protect the data, a mature ecosystem.
Weak at
Scaling writes beyond one machine needs sharding you build yourself; schema changes on huge tables need care.
Relational (SQL): Ad-hoc queries and joins, transactions across rows, constraints that protect the data, a mature ecosystem. The price: scaling writes beyond one machine needs sharding you build yourself; schema changes on huge tables need care.
A summary of how these systems describe themselves; real products blur the lines (PostgreSQL stores JSON documents, DynamoDB offers transactions).
Joins versus embedding
Putting the line items inside the order makes the read trivial, and the update of a product name a nightmare.
The trade is that embedded copies must be kept consistent. Changing a product's name in a relational database updates one row. If thousands of order documents each embed that name, either you update them all (a huge fan-out write that is not atomic) or you accept that old orders keep the old name. For orders that is often correct (an invoice should keep the price paid), but for a user's display name shown on every comment it is a bug. Choose per field.
With 50,000 orders embedding a product, one rename means 50,000 document writes; in SQL it is one row.
Read an order: relational (join)
7 row/index reads
Read an order: document (embedded)
1 document read
Rename a product: relational
1 row
Rename a product: embedded documents
50,000 documents
Reads to show an order7 vs 1
Writes to rename a product1 vs 50,000
An order with 5 items takes about 7 lookups with a join and 1 with an embedded document, a real win for read-heavy pages. Renaming a product that appears in 50,000 orders is 1 write in SQL and 50,000 in the document model, so embed data that is read together and rarely changes, and reference data that changes often.
Counts of operations, not times. Relational read = orders lookup + items index lookup + one row per item (a simplification).
Key-value and wide-column
Get a user by id: one shard, one hop. Find all users in Pune: ask every shard and wait for the slowest.
That slowest-of-N effect is the tail-latency problem from the latency lesson: with 50 shards you meet the p99 of a single shard almost every time. The remedies are in the data model: design the key around the queries you need (a composite partition key and a sort order, as in Cassandra), maintain a second table or index for each additional query, and accept that you must know your queries in advance.
At one shard a query takes about 5 ms. At 200 shards the same query takes nearly double: the median is the single shard's p99.
1 shard
median 4.9 ms · p99 12.3 ms
10 shards
median 9.0 ms · p99 17.1 ms
50 shards
median 12.0 ms · p99 20.3 ms
200 shards
median 14.7 ms · p99 23.5 ms
Query by key (1 shard): median5.1 ms
Query by anything else (50 shards): median12.1 ms
…and 99th percentile20.5 ms
By key, one shard answers in about 5.1 ms. A query that must visit 50 shards waits for the slowest: 12.1 ms at the median and 20.5 ms at p99, and it also costs 50 requests of cluster capacity for one user question.
8,000 seeded queries: each shard's time is log-normal (σ = 0.4) around the median; scatter-gather takes the maximum over the shards visited.
Graph databases
“Friends of friends of friends” is a fine query for a graph and an uncomfortable one for tables.
Be honest about the size of the win, though: it is a constant factor per hop (the number of index levels), not an exponential. The number of people you reach grows as bdepth either way (50 friends at 6 hops is fifteen billion paths), and that explosion, not the storage model, is what you must tame with limits and pruning. Graph stores earn their keep for recommendations, fraud rings, access-control hierarchies and dependency graphs, where depth and shape vary.
Compare depth 2 with depth 5 at 50 friends each: the graph is always about five times fewer reads, but both climb by a factor of 50 per hop.
Relational: index lookup per hop
625 thousand page reads
Graph: pointer per edge
125 thousand hops
Paths to follow125 thousand
Index levels per lookup (relational)4
Relational ÷ graph5×
50 friends over 3 hops means 125 thousand paths. A relational database does about 625 thousand page reads for them, a graph store about 125 thousand pointer hops: 5× fewer. At this size either approach is fast.
Cost model: a relational hop = index descent of ⌈log₂₀₀(n)⌉ pages plus the row; a graph hop = one pointer. Constant factors only; it ignores caching.
Scale
“It has to scale” is the most common reason to choose a database, and the one most often wrong.
The honest question is arithmetic: how much data, how many queries per second, how fast is growth, and what does one machine of the kind you can rent actually handle (measure it with your workload; the figures below are slider assumptions, not benchmarks). If the answer is one node plus replicas, use the simplest database that fits. When it is not, you will be or choosing a store built to scale out, and you will know why.
2 TB and 5,000 queries a second need a single node. Push to 30 TB and 100,000 queries a second and you need a fleet.
Nodes needed for the data
1
Nodes needed for the query rate
1
Nodes you need1
The limiting factorneither: one node is enough
2 TB and 5,000 queries a second fit on one node (limits 4 TB and 20,000 q/s). A single relational database with replicas is the simplest design that works, and keeps joins and transactions.
Nodes = max(⌈data ÷ per-node data⌉, ⌈rate ÷ per-node rate⌉). The per-node limits are sliders for your own benchmark results, not universal numbers.
Which store?
Six workloads, four stores.
Pick the family whose strengths match the access pattern, not the one with the best marketing. Each answer is worth 20 XP.
Ask: how is it read, how is it written, do records change together, and how big will it get?
Question 1 / 6
Web session data looked up by session id, millions of times a second, expiring after 30 minutes.
Tip: press 1 to 4 to answer, Enter for the next one.
Who lives by this
From Dynamo to a sharded PostgreSQL.
Four published systems and the shapes they chose.
Always writableAmazon Dynamo (2007)
a key-value store for the shopping cart
The Dynamo paper described a key-value store built for availability: any node can accept a write, versions are reconciled later, and the access pattern (get and put by primary key) is simple enough to partition by hashing. Amazon used it for services such as the shopping cart.
Sorted by row keyGoogle Bigtable (2006)
a wide-column store
Bigtable stores a sparse, sorted map from (row key, column, timestamp) to value, split into tablets and written through an LSM-style log. Applications such as web indexing, Maps and Gmail used it, designing their row keys around the scans they needed.
Graph on top of MySQLFacebook TAO (2013)
a distributed graph store
Facebook's TAO paper describes a graph API (objects and associations) for the social graph, served from a caching tier in front of sharded MySQL. Even a graph workload ended up on a relational store underneath, with the graph shape provided by the API.
A relational database that scaledInstagram
sharded PostgreSQL
Instagram's engineering blog described sharding its PostgreSQL databases logically across many servers with its own ID scheme, rather than moving to a different type of store, to serve a very large user base.
Interview angle
What interviewers actually ask.
Six questions, each worth 20 XP. 'Why this database?' is one of the most common design-interview questions.
Start from access patterns and consistency needs, say what you give up, and do the capacity arithmetic.
Question 1 / 6
When unsure, which is the safest default?
Tip: press 1 to 3 to answer, Enter for the next one.
Break it
The team chose a key-value store “for scale”. A year later the code is doing joins and transactions by hand.
The second cost is correctness. A business action that touches two records (create the order and reduce the stock) needs a transaction; without one, a crash between the two writes leaves them disagreeing, and the application grows hand-written compensation code, background reconcilers and a dashboard of mismatches. These are all features a relational database already has. Move the sliders to see both costs.
With 50 orders on a page and a 2 ms round trip, the loop takes about 100 ms against 4.5 ms for one SQL join. Then raise the crash rate.
Join done in the application
102 ms (51 round trips)
One SQL join
4.5 ms (1 round trip)
Page latency in the app-side join102 ms
Page latency with a SQL join4.5 ms
Order/stock mismatches per million actions50
Listing 50 orders with an application-side join costs 51 sequential round trips: 102 ms, against 4.5 ms for a single join. And with 1 crashes per 10,000 actions between two related writes, about 50 actions per million leave the order and the stock disagreeing. A relational database does both jobs for free.
N+1 latency = (1 + N) × round trip; a join is one round trip plus a little work per row. Mismatches = crash chance × 1,000,000 × 0.5 (half of the crashes fall between the two writes).