SQL vs NoSQL: choosing the store
In one line
Pick the store from your access patterns, consistency needs and scale numbers, not from fashion: a relational database is the default, and a NoSQL store earns its place when a specific pattern (huge write volume, simple key lookups, flexible documents) makes it clearly better.
What it is
SQL (relational) databases such as PostgreSQL and MySQL store data in tables with a fixed schema, support joins and secondary indexes, and give ACID transactions across many rows and tables. You describe what you want with SQL and the query planner decides how to get it.
'NoSQL' is not one thing. It is a family of stores that each give up some relational features to win somewhere else: key-value stores (Redis, DynamoDB) for very fast lookups by key; wide-column stores (Cassandra, ScyllaDB, HBase) for huge write volumes partitioned by key; document stores (MongoDB, Couchbase) for nested JSON-like records; graph databases (Neo4j) for many-hop relationship queries.
The line has blurred. PostgreSQL has JSONB columns with GIN indexes, MongoDB has multi-document transactions, DynamoDB has transactions on up to 100 items, and 'NewSQL' systems (Google Spanner, CockroachDB, TiDB, YugabyteDB) give SQL and ACID transactions while sharding automatically. So the real question is not 'SQL or NoSQL' but 'which access patterns, consistency and scale does this data have, and which store fits them best'.
Why interviewers ask
Almost every design interview has a moment where you choose a database, and interviewers listen for the reason. 'Cassandra because it scales' is a weak answer; 'Cassandra because we write 200,000 location points per second, always read by (driver_id, time range), and never need joins' is a strong one. They also check that you know what you give up: transactions, joins, ad-hoc queries, or strong consistency.
How it works
- 1List the entities and, for each, the exact queries: 'get order by id', 'list a user's orders newest first', 'count orders per restaurant per hour'. Access patterns come before the store.
- 2Check the relationships and transactions: if a single business action must update several entities atomically (debit wallet, create payment, update order), a relational database with ACID transactions is the natural fit.
- 3Check the numbers: a single well-tuned PostgreSQL primary on a large machine handles roughly 10,000 to 50,000 simple writes per second and several TB of data; read replicas add read capacity. If your estimates fit, you do not need NoSQL for scale.
- 4If writes are far beyond one machine (hundreds of thousands per second) and every read uses a known key, a partitioned wide-column or key-value store (Cassandra, DynamoDB) fits: you design one table per query, keyed by the partition key.
- 5If records are nested and their shape varies per item (product attributes differ for phones and sarees), a document store or a JSONB column fits; prefer JSONB inside Postgres when the rest of the data is relational.
- 6If the core query is 'friends of friends who like X' with many hops, a graph store fits; for one or two hops, SQL joins or an adjacency table is usually enough.
- 7Consider operations: who runs it, backups, managed offerings (RDS, Aurora, Cloud SQL, DynamoDB, Atlas) and team familiarity. An unfamiliar store is a real cost.
- 8Use more than one store when patterns differ (polyglot persistence): Postgres for orders and payments, Redis for sessions and counters, Elasticsearch for search, S3 for media, a column store for analytics. Keep one system as the source of truth for each piece of data and sync the others from it.
Worked example · Choosing stores for a food delivery app
- Orders: 3M orders a day is about 35 per second on average, maybe 500 per second at dinner peak. An order touches the order row, its line items, a payment record and a coupon redemption, all of which must succeed together. Choice: PostgreSQL, with ACID transactions.
- Order data size: 3M orders x 2 KB = 6 GB per day, about 2 TB per year. One primary with read replicas handles this for years; partition the table by month for easier archiving.
- Courier location: 300,000 active couriers sending a ping every 4 seconds = 75,000 writes per second, always read as 'latest position of courier X' or 'pings for courier X between t1 and t2'. Choice: latest position in Redis (overwritten in memory), history in Cassandra with partition key (courier_id, day) and clustering key timestamp.
- Menus: each restaurant has a nested menu (categories, items, variants, add-ons) that is read as a whole and edited rarely. Choice: JSONB in Postgres or a document store; cached in Redis or a CDN because reads are about 100 times writes.
- Restaurant search by dish name and cuisine: Elasticsearch, fed from Postgres by change data capture. Not the source of truth, rebuilt if it is lost.
- Summary to say aloud: 'Postgres is the source of truth for money and orders; each other store exists because one access pattern needs it, and each is fed from Postgres or from the event stream.'
Picture it
Choosing a database is like choosing a vehicle. A car (relational database) handles almost every trip well and should be your default. A truck (Cassandra) moves enormous loads on known routes but is awkward for errands. A scooter (Redis) is the fastest through traffic but carries very little. You buy the truck when you actually have truck-sized loads, not because trucks look impressive.
Reference code
-- Same access pattern, two models
-- PostgreSQL: normalised, any query possible, joins at read time
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL,
total_paise BIGINT NOT NULL
);
CREATE INDEX ON orders (user_id, created_at DESC);
-- Cassandra (CQL): one table per query, partitioned by user
CREATE TABLE orders_by_user (
user_id bigint,
created_at timestamp,
order_id bigint,
total_paise bigint,
restaurant_name text, -- denormalised copy, no joins
PRIMARY KEY ((user_id), created_at, order_id)
) WITH CLUSTERING ORDER BY (created_at DESC, order_id ASC);Key patterns
Relational by default
When: Business data with relationships, money, inventory, or unknown future queries
How: Start with PostgreSQL or MySQL; add indexes, replicas and partitioning; only shard or move a table out when numbers demand it.
Query-first modelling for NoSQL
When: Using Cassandra or DynamoDB
How: Write down each query, then create one table (or one item type) per query with the right partition and sort key. Denormalise and duplicate data instead of joining.
Polyglot persistence with one source of truth
When: Different parts of the data have very different access patterns
How: Keep the authoritative copy in one store and feed derived stores (search, cache, analytics) through an outbox or CDC so they can be rebuilt.
JSONB escape hatch
When: A few attributes vary per row but the rest is relational
How: Store variable attributes in a JSONB column with a GIN index instead of moving the whole entity to a document database.
Real-world use
Discord: MongoDB to Cassandra to ScyllaDB
Discord moved message storage from MongoDB to Cassandra around 2017 once messages outgrew one replica set, modelling messages by (channel_id, time bucket). In 2022-23 it migrated trillions of messages to ScyllaDB to reduce latency spikes caused by garbage collection and compaction.
Amazon DynamoDB
Amazon's key-value service, built from lessons of the 2007 Dynamo paper, serves shopping cart and many internal workloads. Its design guidance is single-table modelling around partition and sort keys, which only works when access patterns are known upfront.
Uber: Schemaless on MySQL
Uber built Schemaless, an append-only key-value layer on top of sharded MySQL, for trip data. It shows that 'NoSQL' behaviour can be built on a relational engine when the team knows it well.
Instagram on PostgreSQL
Instagram scaled its core data (users, media, likes) on sharded PostgreSQL, generating IDs inside the database with a time-plus-shard scheme, and used Cassandra for some high-write features.
Zerodha on PostgreSQL
Zerodha has publicly described running large reporting and back-office workloads on PostgreSQL with heavy partitioning, rather than adopting a distributed NoSQL store.
In a project
A startup building a stock-trading app for India stored everything in MongoDB 'to move fast'. Now order placement sometimes leaves a user's funds blocked without an order record, and the compliance team cannot run ad-hoc reports.
- List access patterns: place order (blocks funds and creates order atomically), list orders by user and day, daily reports joining users, orders and trades.
- Recognise that funds and orders need a multi-entity atomic update and ad-hoc reporting, which is exactly what a relational database does well.
- Move ledgers, funds and orders to PostgreSQL with foreign keys and a single transaction for 'block funds + insert order'.
- Keep market-data ticks (hundreds of thousands per second during market open) out of Postgres: they go to an in-memory pub/sub for live prices and a time-series store for charts.
- Keep user preferences and watchlists, which are nested and per-user, as JSONB columns.
- Feed a column store (ClickHouse) for compliance reports through CDC, so heavy reports never load the primary.
Impact: Blocked-funds mismatches stop because both writes commit or roll back together, compliance runs SQL reports on the column store without touching production, and the team runs two well-understood stores instead of forcing one store to do everything.
Trade-offs
Use it when
- SQL: data has relationships and multi-row transactions (payments, orders, inventory, bookings)
- SQL: you need ad-hoc queries, reporting or constraints such as unique and foreign keys
- SQL: the write rate and data size fit one primary plus replicas, which covers most products
- NoSQL: very high write throughput with simple key-based reads (events, messages, location pings, IoT)
- NoSQL: you need automatic horizontal scaling and multi-region replication with tunable consistency
- NoSQL: items are self-contained documents read and written as a whole
Avoid it when
- SQL: a single table must absorb hundreds of thousands of writes per second with no natural sharding plan
- NoSQL: you will need joins, aggregations or new query patterns you cannot predict today
- NoSQL: correctness depends on atomic updates across many entities, such as double-entry ledgers
- Any new store the team cannot operate, back up and debug at 3 a.m.
Common mistakes
- Saying 'NoSQL scales and SQL does not'. Relational databases scale reads with replicas and writes with sharding; NoSQL stores just make partitioning the default.
- Choosing a store before listing the access patterns and estimating writes per second and data size.
- Modelling Cassandra or DynamoDB like a relational database with normalised tables and then needing joins or scans.
- Putting money or inventory in an eventually consistent store without a plan for conflicts and double spends.
- Using five different databases in a 45-minute design without saying which one is the source of truth.
- Forgetting that 'schemaless' still has a schema; it is just enforced in application code, which makes migrations harder to see.
Interview questions
Quick revision
- Access patterns + transactions + numbers decide the store; fashion does not.
- Default: PostgreSQL or MySQL. One primary handles roughly 10k-50k simple writes/s and multi-TB data.
- Key-value (Redis, DynamoDB): lookups by key, very low latency.
- Wide-column (Cassandra, ScyllaDB): huge append-heavy writes, read by partition key + range.
- Document (MongoDB, JSONB): nested records read and written whole.
- Graph (Neo4j): many-hop relationship queries.
- NewSQL (Spanner, CockroachDB, TiDB): SQL + ACID + automatic sharding, higher write latency.
- NoSQL modelling = one table per query, denormalise, no joins.
- Polyglot is fine, but name the source of truth and sync others via outbox or CDC.
Check yourself
1. A bank transfer must debit one account and credit another atomically. Which store fits best?
2. When designing a Cassandra table, what should you decide first?
3. Your app writes 3,000 orders per second at peak and needs reports joining orders and users. What is the most sensible starting point?
4. Which statement about 'schemaless' document stores is true?
See it in a full design
These walkthroughs use this lesson.
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.