Skip to content
SparkTime, home
System Design From Zero to Interview-Ready
Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
6 · Distributed Systems
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
11 · Large-Scale Case Studies
12 · Interview Readiness
Lesson 17 of 45

Lesson 17 · Databases

SQL vs NoSQL: Data Models and How to Choose the Right Database

A trade-off-driven guide to choosing databases: relational, key-value, document, wide-column, graph, search, time-series and analytical stores, what each is good and bad at, query-first modelling for NoSQL, the myths ("NoSQL scales, SQL doesn't"), polyglot persistence, and a decision process you can use in interviews.

On this page

Key takeaways

  • "SQL versus NoSQL" is the wrong question; choose by data shape, access patterns, consistency and transaction needs, scale and the team's ability to operate the store.
  • Relational databases are the right default for structured data with relationships and transactions; specialised stores earn their place for specific access patterns.
  • Many non-relational stores scale out easily because they restrict queries and transactions; model them query-first, often duplicating data per query.
  • Search engines, caches and analytical stores are usually derived copies fed from a source of truth, not the source of truth themselves.
  • Every additional database adds operations work and a synchronisation problem, so add one only when a requirement justifies it.

"Should we use SQL or NoSQL?" is one of the most common questions in design interviews, and one of the most commonly mis-answered. "NoSQL, because it scales" is a slogan, not a reason. "SQL, because it's what I know" is honest, but still not a reason. The real question is: given these access patterns, this data shape, these consistency needs and this scale, which store makes the important operations simple, fast and correct?

This article gives you a working map of the database families, the trade-offs each one makes, and a process for choosing that you can say out loud in an interview.

What you will learn

#
  • What "NoSQL" actually covers, and the main database families.
  • What each family is good at, bad at, and typically used for.
  • The common myths, and what is true behind them.
  • Query-first modelling for key-value, document and wide-column stores.
  • Polyglot persistence: using several stores, and its costs.
  • A decision process, applied to worked examples.

What "NoSQL" means

#

"NoSQL" is an umbrella term for databases that do not use the relational table model as their primary interface. It covers very different systems with different data models, guarantees and query abilities. Treating them as one category is the first mistake. A more useful map:

Family

Data model

Strong at

Weak at

Examples

Relational

Tables, rows, joins, constraints

Transactions, relationships, ad hoc queries, integrity

Scaling writes beyond one machine without extra machinery

PostgreSQL, MySQL, SQL Server, Oracle; managed versions

Key-value

A value looked up by its key

Very fast get and put at huge scale; simple

Queries other than by key

Redis, Amazon DynamoDB (key-value and document), etcd

Document

JSON-like documents grouped in collections

Flexible, nested data read as a whole; per-document atomic updates

Many-to-many relationships, cross-document joins

MongoDB, Firestore, Couchbase

Wide-column

Rows in partitions, sorted by a clustering key

Massive write throughput, large data, predictable queries by key and range

Ad hoc queries, joins, frequent cross-partition operations

Apache Cassandra, ScyllaDB, Google Bigtable, HBase

Graph

Nodes and relationships with properties

Multi-hop traversals: friends of friends, fraud rings, recommendations

Bulk aggregations over everything; very high write rates

Neo4j, Amazon Neptune

Search engine

Inverted indexes over documents

Full-text search, relevance ranking, faceting

Being the source of truth; transactions

Elasticsearch, OpenSearch, Apache Solr

Time-series

Measurements indexed by time

High-rate appends, time-window queries, downsampling, retention

General-purpose queries

TimescaleDB, InfluxDB, Prometheus

Analytical (columnar)

Columns stored separately, compressed

Aggregations over billions of rows

Many small transactional writes and point lookups

BigQuery, Amazon Redshift, ClickHouse, Snowflake

Object storage

Blobs by key

Huge files, durability, cost

Querying contents; small frequent updates

Amazon S3, Google Cloud Storage, Azure Blob Storage

The families in a little more depth

#

Key-value stores

#

The simplest model: put(key, value) and get(key). Because every operation names its key, the store can partition data by key and route each request to one node, which scales very well. Redis adds rich data structures in memory (article 09); DynamoDB adds sort keys, secondary indexes and durability as a managed service.

Use when access is almost always by a known key: sessions, user preferences, shopping carts, feature flags, idempotency records, rate-limit counters.

Document stores

#

Each record is a document, often JSON, which can nest arrays and sub-objects. An order and its items can live in one document, read and written as a unit. MongoDB's data-modelling guidance discusses the central choice: embed related data in one document when it is read together, or reference it in separate documents when it is large, shared or grows without bound (MongoDB: Data Modeling).

"Flexible schema" means the database does not force every document to have the same fields. The schema still exists; it lives in your application code and must be versioned there ("schema on read").

Use for: content and catalogue data with varied attributes, user profiles with nested settings, data naturally shaped as self-contained documents.

Wide-column stores

#

Data lives in partitions identified by a partition key; within a partition, rows are sorted by a clustering key. Reads are efficient when they name the partition and a range of the clustering key ("the latest 50 messages of conversation X"). These systems descend from Google's Bigtable, described as a distributed storage system for structured data designed to scale to petabytes across thousands of machines (Bigtable), and from Amazon's Dynamo. Apache Cassandra's documentation explains how it adopts Dynamo-style partitioning with consistent hashing, replication and tunable consistency (Cassandra: Dynamo).

Use for: very high write volumes with predictable queries: messages, activity feeds, event logs, IoT readings, time-ordered data per entity.

Graph databases

#

When the important questions are about connections ("how is A connected to B within 3 hops?", "accounts sharing devices with this flagged account"), relational joins become deep and slow. Graph databases store relationships as first-class data, so traversals follow pointers instead of joining tables. Neo4j's documentation introduces this model of nodes, relationships and properties (What is a graph database?).

Use for: social graphs with multi-hop queries, fraud detection, recommendation paths, knowledge graphs. Not for simple "who do I follow" lookups, which a relational or key-value table handles perfectly well.

Search engines

#

A search engine builds an inverted index: a map from each term to the documents containing it, with information for ranking. Elastic's documentation describes how documents are stored in indices and made searchable (Index basics). Search engines answer "find products matching 'red running shoes', ranked by relevance, filtered by size and price" far better than a relational LIKE query.

They are usually derived stores: the source of truth lives in a database, and an indexing pipeline copies changes into the search engine, so search results can lag by seconds. Articles 36 and 44 build on this.

Time-series and analytical stores

#

Time-series stores optimise for appending measurements and querying windows of time, with automatic rollups and retention. Columnar analytical stores store each column separately and compress it, so "sum revenue by city for last year" reads only two columns from billions of rows. Both are typically fed from operational systems; they do not replace them.

This is the OLTP versus OLAP split: online transaction processing (many small reads and writes for users, which relational and NoSQL operational stores do) versus online analytical processing (large scans and aggregations for reports, which warehouses and columnar stores do).

Myths and what is actually true

#

Myth

What is actually true

"NoSQL scales; SQL does not."

Relational databases scale reads with replicas and can be sharded (articles 15 and 16), and distributed SQL databases exist. Many NoSQL stores make scale-out easier by restricting what you can do: no joins, limited transactions, queries mostly by key. That restriction is the trade, not magic.

"NoSQL is schemaless."

The schema moves from the database into application code. You still need to version and migrate it.

"NoSQL has no transactions."

Several popular non-relational databases now support transactions, often with limits on scope, size or performance. Read the documentation for the exact guarantees.

"Relational databases are slow."

For indexed point lookups and short transactions they are very fast. Slowness usually comes from missing indexes, poor queries or the wrong access pattern.

"Use NoSQL for big data."

The volume alone does not decide. Access patterns, consistency and query needs do.

An honest summary: a distributed database such as Google Spanner shows that strong consistency and horizontal scale can be combined, using carefully engineered clock infrastructure (Google documents how its TrueTime system supports external consistency (Spanner: TrueTime and external consistency)). The costs move into latency, complexity or price; they do not disappear.

Modelling query-first

#

Relational modelling starts from entities and normalises. Key-value, document and especially wide-column modelling start from queries: for each important query, design a structure that answers it with one lookup or one range read, even if that duplicates data.

Example: chat messages in a wide-column store

#

Access pattern: "show the latest 50 messages of a conversation, then older ones as the user scrolls."

SQL
-- Cassandra Query Language (CQL), which looks like SQL but follows wide-column rulesCREATE TABLE messages_by_conversation (    conversation_id  uuid,    month_bucket     text,        -- for example '2026-10'; keeps partitions from growing forever    message_seq      bigint,      -- per-conversation sequence number    sender_id        bigint,    body             text,    sent_at          timestamp,    PRIMARY KEY ((conversation_id, month_bucket), message_seq)) WITH CLUSTERING ORDER BY (message_seq DESC);
  • The partition key (conversation_id, month_bucket) puts a conversation's messages for a month together on one set of replicas.
  • The clustering key message_seq DESC stores them newest first, so "latest 50" is one sequential read.
  • The month bucket prevents one very long-lived conversation from becoming one enormous partition.
  • A second query such as "all messages sent by user X" is not served by this table; if it were needed, you would write the same message to a second table keyed by sender. Duplication per query is normal here.

Example: an order as a document

#
JSON
{  "_id": "ord_77",  "customer_id": 42,  "status": "shipped",  "items": [    {"product_id": 9, "name": "Running shoes", "qty": 1, "unit_price_paise": 349900},    {"product_id": 15, "name": "Socks", "qty": 2, "unit_price_paise": 19900}  ],  "shipping_address": {"city": "Chennai", "pin": "600001"},  "created_at": "2026-10-04T18:22:07Z"}

Items and the address are embedded because they are always read with the order and belong to it. The product catalogue is referenced by product_id because products are shared and change independently.

Polyglot persistence

#

Large systems often use several stores, each for what it does best: a relational database as the source of truth, Redis for caching and counters, object storage for files, a search engine for search, a warehouse for analytics. This is called polyglot persistence.

The costs are real:

  • Synchronisation. Derived stores must be updated when the source changes, usually through events or change data capture, and they lag. A product renamed in the database may show the old name in search for a few seconds (articles 21 and 27).
  • Consistency across stores. There is no transaction spanning PostgreSQL and Elasticsearch.
  • Operations. Each store needs monitoring, backups, upgrades, capacity planning, security and people who understand it.

Keep the number of stores small, and know which one is the source of truth for each piece of data.

A decision process

#

Work through these questions in order. Each narrows the choice.

  1. What is the data's shape? Structured with many relationships, self-contained documents, simple key-values, time-ordered events, a graph, or large blobs?
  2. What are the hot access patterns? By key? By key and time range? Ad hoc filtering and joins? Full-text? Multi-hop traversals? Aggregations?
  3. What consistency and transactions are required? Must several records change atomically? Must reads always see the latest write?
  4. What is the scale? Data size, write rate, read rate (from your estimates). Can one well-provisioned relational primary handle it, with replicas and caching?
  5. What can the team operate? A managed service the team understands beats an exotic store nobody can run at 3 a.m.

Situation

Reasonable choice

Why

Users, orders, payments, inventory

Relational

Relationships, constraints, transactions

Sessions, carts, rate limits, idempotency keys

Key-value (often Redis or DynamoDB)

Access by key, high rate, simple

Product catalogue with varied attributes

Document store, or relational with JSON columns

Flexible attributes read as a unit

Chat messages, activity logs at very high write volume

Wide-column

Partition by entity, range by time, huge writes

"People you may know", fraud rings

Graph (often alongside a relational source of truth)

Multi-hop traversals

Product and content search

Search engine fed from the source of truth

Relevance and full-text

Metrics and monitoring data

Time-series store

Appends, windows, retention

Business reporting

Columnar warehouse fed by pipelines

Large aggregations without hurting OLTP

When in doubt, start with a relational database, model it well, and move specific access patterns to specialised stores when estimates or measurements show the need. That is a mature, defensible default, not a lack of imagination.

Worked example 1: Pixly's stores

#

Data

Store

Reasoning

Users, photos metadata, follows

PostgreSQL (primary, standby, read replicas)

Relationships, constraints (unique usernames, one follow per pair), transactions; a few TB per year fits (article 07)

Photo files

Object storage plus CDN

Petabytes of blobs

Cache, sessions, like counters, precomputed feeds

Redis

Key-based, very hot, tolerant of brief loss (with persistence for sessions if needed)

Likes at large scale (if the relational table becomes too write-heavy)

A wide-column store keyed by photo_id, or a partitioned relational table

1,700 writes per second at peak today is fine for PostgreSQL; revisit if it grows 10 times

Search (later)

OpenSearch or Elasticsearch fed by photo events

Captions and hashtags, relevance

Analytics

A warehouse fed from database changes and events

Reports without touching the primary

Notice that Pixly does not start with Cassandra "for scale". The estimates show one relational primary is enough today; the design names exactly what would trigger a change.

Worked example 2: a chat app's message store

#

Estimates from articles 06 and 07: about 92,000 messages per second at peak, about 1.2 PB per two years before replication; access is almost always "messages of one conversation, newest first".

  • Relational, sharded by conversation: workable, but you would be building and operating sharding, rebalancing and routing yourself.
  • Wide-column (Cassandra-like) with partition key (conversation, time bucket): matches the access pattern exactly, scales writes by adding nodes, and replicates across nodes with tunable consistency. Users, contacts and group membership still live in a relational database, where relationships and constraints matter.

Choice: wide-column for messages, relational for accounts and groups, Redis for presence and connection routing (article 34). Each store matches one access pattern.

Failure modes and operational surprises

#

Problem

Where it bites

Mitigation

Eventual consistency surprises

Reads after writes in leaderless or replicated stores

Choose consistency levels per query; read-your-writes routing (article 19)

Unbounded partitions

Wide-column partitions that grow forever (one huge chat)

Time buckets in the partition key

Hot partitions

Celebrity keys, time-based keys

Better keys, salting, caching (article 16)

Schema drift

Documents with many versions of their shape

Version fields, migration jobs, validation rules

Derived store out of sync

Search shows deleted items

Reliable change pipelines, periodic reconciliation

Using a cache or search engine as the only copy

Data loss on restart or reindex

Keep a durable source of truth

Trade-offs

#

Choice

You gain

You pay

Relational

Integrity, transactions, flexible queries, mature tooling

Write scale-out needs sharding or a distributed SQL product

Key-value

Speed and simple scale-out

Few query options

Document

Natural shapes, flexible fields

Relationships and cross-document consistency are harder

Wide-column

Write throughput and size

Query-first modelling, duplicated data, fewer ad hoc queries

Graph

Fast traversals

Narrow purpose, separate operations

Several stores

Best tool per job

Synchronisation, consistency and operations costs

Common mistakes

#
  • Choosing NoSQL "because it scales" without estimates or access patterns.
  • Choosing a document store for heavily relational data with many-to-many relationships and strong integrity needs.
  • Using a search engine or a cache as the system of record.
  • Modelling a wide-column store like a relational one and then needing joins.
  • Adding a new database for every feature.
  • Ignoring who will operate the store.

In an interview

#
  • Do not open with a product name. Open with the access pattern: "messages are written at 90,000 per second and read by conversation, newest first."
  • Then name the family and one example: "a wide-column store partitioned by conversation and time bucket, such as Cassandra; or sharded PostgreSQL if the team knows it better."
  • Say what you give up (ad hoc queries, joins) and why that is acceptable.
  • Name the source of truth when several stores hold related data, and how they stay in sync.

Exercises

#

Exercise 1

Choose a store

#

Choose a database family (and say why) for:

  1. A bank's account balances and transfers.
  2. Driver location pings every 4 seconds from 500,000 drivers, queried as "this driver's path over the last hour".
  3. A recipe site where each recipe has a different set of attributes, read as a whole page.
  4. Detecting rings of accounts that share phone numbers and devices.
  5. Autocomplete for 10 million product names.

Exercise 2: Model it query-first

#

A ride-hailing app needs: (a) a rider's trip history, newest first; (b) a driver's trip history, newest first. Using a wide-column store, design the table or tables, with partition and clustering keys, and explain why one table is not enough.

Exercise 3: Critique the design

#

A candidate proposes: "We'll use Elasticsearch as our only database for the e-commerce site, since it handles search and scales well. Orders, payments and inventory all go in it." List the problems and propose a better split.

Exercise 4: Embed or reference?

#

In a document store for a blogging platform, decide whether to embed or reference, and why:

  1. A post's tags.
  2. A post's comments, where popular posts get 50,000 comments.
  3. The author's profile (name, avatar, bio).
  4. Revision history of a post.
Show solutionHide solution
  1. Relational. Transfers must be atomic across rows, balances must never be wrong, and auditing needs constraints and history (articles 14 and 39).
  2. Wide-column or time-series, partitioned by driver_id plus a time bucket, clustered by timestamp. Very high write rate, simple "one driver, a time range" queries. Live "drivers near me" queries need a separate geospatial index (article 41).
  3. Document store, or relational with a JSON column for the variable attributes. Each recipe is a self-contained document read as one unit.
  4. Graph database (fed from the source of truth). The question is multi-hop: account to device to account to phone to account.
  5. A search engine or a purpose-built prefix index (article 36), built from the product database, with caching for popular prefixes.

Exercise 2

Show solutionHide solution

Two tables, one per query, because each query has a different partition key:

SQL
CREATE TABLE trips_by_rider (    rider_id     bigint,    year_month   text,    requested_at timestamp,    trip_id      uuid,    driver_id    bigint,    fare_paise   bigint,    PRIMARY KEY ((rider_id, year_month), requested_at, trip_id)) WITH CLUSTERING ORDER BY (requested_at DESC, trip_id ASC);CREATE TABLE trips_by_driver (    driver_id    bigint,    year_month   text,    requested_at timestamp,    trip_id      uuid,    rider_id     bigint,    fare_paise   bigint,    PRIMARY KEY ((driver_id, year_month), requested_at, trip_id)) WITH CLUSTERING ORDER BY (requested_at DESC, trip_id ASC);

A single table partitioned by rider would make "driver's history" a scan of every partition; partitioned by driver, the rider query has the same problem. Each trip is written to both tables (and ideally to a relational or event store that remains the source of truth). The year_month bucket keeps a frequent rider's or driver's partition bounded; trip_id in the clustering key makes rows unique when two trips share a timestamp.

Exercise 3

Show solutionHide solution

Problems:

  • Transactions: placing an order must atomically reserve stock and record payment state; search engines are not designed as transactional systems of record.
  • Durability and correctness: search clusters are typically rebuilt and reindexed; treating them as the only copy risks losing orders.
  • Consistency: indexing is near-real-time, so a just-placed order may not be visible immediately, which is unacceptable for "my orders" and payment flows.
  • Constraints: no foreign keys or unique constraints to prevent duplicates and orphans.

Better split: a relational database for orders, payments and inventory (source of truth with transactions); a search engine for product search, fed by product-change events; a cache for product pages; object storage for images. Search results can be seconds stale; orders cannot.

Exercise 4

Show solutionHide solution
  1. Embed. Tags are small, bounded and always shown with the post.
  2. Reference (a separate comments collection keyed by post id and time). 50,000 comments would make the document huge, exceed size limits, and turn every new comment into a rewrite of a large document. Paginate comments separately.
  3. Reference, possibly with a small embedded copy of the display name and avatar URL for fast rendering. The profile is shared across all of the author's posts and changes independently; if you embed a copy, accept that old posts may show an old avatar or update them asynchronously.
  4. Reference (a revisions collection). History grows without bound and is rarely read with the post.

Where to go next

#

Sources & further reading

  1. mongodb.com
  2. cassandra.apache.org
  3. research.google
  4. neo4j.com
  5. elastic.co
  6. docs.cloud.google.com
  • Guide · Technology

    SQL and PostgreSQL Fundamentals for Java Developers

  • Guide · Technology

    Inheritance, Overriding and Overloading

  • Guide · Career

    A LinkedIn Profile That Gets Recruiter Messages