All chapters in this track
- 01Chapter 1: Why Database Choices Matter
- 02Chapter 2: Relational Fundamentals: Tables, Keys, Constraints, Normalization
- 03Chapter 3: ACID Properties and Transactions
- 04Chapter 4: Isolation Levels and the Anomalies Each One Prevents
- 05Chapter 5: SQL Essentials and Query Optimization
- 06Chapter 6: Indexing Strategies: B-Tree, Hash, Composite, Covering
- 07Chapter 7: NoSQL Categories: Document, Key-Value, Wide-Column, Graph
- 08Chapter 8: Schema Design and Data Modeling (Relational and NoSQL Access Patterns)
- 09Chapter 9: The CAP Theorem and Consistency Models
- 10Chapter 10: Replication: Leader-Follower and Multi-Leader
- 11Chapter 11: Sharding and Partitioning Strategies
- 12Chapter 12: Caching Layers and Invalidation Strategies
- 13Chapter 13: Evolving Schemas Safely in Production
- 14Chapter 14: Diagnosing and Fixing a Slow Query, End to End
- 15Chapter 15: Schema Design Interview Exercises
- 16Chapter 16: Database Concepts in System Design and Production Debugging
Chapter 8 of 16
Chapter 8: Schema Design and Data Modeling (Relational and NoSQL Access Patterns)
Why this matters
Chapter 2 covered relational schema fundamentals in isolation; Chapter 7 covered NoSQL categories in isolation. This chapter closes Part 2 by covering the actual design process — the single biggest divide between good and bad schema design across both worlds is whether the schema was designed around the data's structure alone, or around how the data will actually be accessed.
Relational modeling: design for structure, query flexibly later
[BEGINNER] Relational schema design generally follows a structure-first process: identify entities, normalize (Chapter 2) to eliminate redundancy, and trust that a well-normalized schema can support a wide range of queries decided later, via SQL's general-purpose query flexibility (joins, aggregations, subqueries) — you don't need to know every future query pattern at design time, because the relational model is designed to be queried flexibly after the fact.
-- A reasonably normalized e-commerce schema:
CREATE TABLE customers (id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE);
CREATE TABLE products (id SERIAL PRIMARY KEY, name VARCHAR(255), price_cents INTEGER);
CREATE TABLE orders (id SERIAL PRIMARY KEY, customer_id INTEGER REFERENCES customers(id));
CREATE TABLE order_items (
order_id INTEGER REFERENCES orders(id),
product_id INTEGER REFERENCES products(id),
quantity INTEGER
);
-- This structure can answer queries nobody anticipated at design time:
-- "top-selling products this month," "customers who bought X and Y,"
-- "average order value by customer" -- all answerable via SQL joins/aggregations
-- without changing the schema itself.
NoSQL modeling: design around the access pattern, upfront
[SENIOR INSIGHT] This is the single most important mental shift moving from relational to NoSQL schema design, and the mistake most commonly made by engineers experienced only with relational modeling: NoSQL schema design (especially document and wide-column stores) should start from "what queries will this need to answer," not from "what does this data structurally look like." Designing a NoSQL schema the way you'd design a relational one — normalized, structure-first, assuming flexible querying later — frequently produces a schema that performs badly, because NoSQL databases generally don't offer the same general-purpose join/query flexibility relational databases do.
A concrete example — modeling a blog's posts and comments:
Relational approach (structure-first, queried flexibly later):
CREATE TABLE posts (id SERIAL PRIMARY KEY, title TEXT, body TEXT);
CREATE TABLE comments (id SERIAL PRIMARY KEY, post_id INTEGER REFERENCES posts(id), body TEXT);
-- Query: get a post with all its comments -- a straightforward JOIN, decided at query time.
SELECT posts.*, comments.body AS comment_body
FROM posts LEFT JOIN comments ON comments.post_id = posts.id
WHERE posts.id = 1;
MongoDB approach — if the actual access pattern is "always fetch a post with all its comments together," embedding is the right, access-pattern-driven design:
// One document per post, comments EMBEDDED directly -- because the real access
// pattern is "read a post and its comments together," not "query comments
// independently of their post."
{
"_id": 1,
"title": "My First Post",
"body": "...",
"comments": [
{ "author": "Alice", "body": "Great post!" },
{ "author": "Bob", "body": "Thanks for sharing" }
]
}
// Fetching a post with all its comments is now ONE document read -- no join needed at all.
db.posts.findOne({ _id: 1 })
But if comments are also frequently queried independently (e.g., "show all of Alice's comments across every post," a very different access pattern), embedding them inside posts makes that query awkward and inefficient — a referencing approach (storing comments as separate documents with a post_id field, closer to the relational shape) would fit that specific access pattern better instead:
// Referencing, when comments need to be queried independently of their post:
db.comments.find({ author: "Alice" }) // efficient, IF comments are a separate collection
[IN PRACTICE] This is exactly why the correct MongoDB schema for "the same logical data" (posts and comments) genuinely depends on the actual access pattern — there's no single "correct" NoSQL schema independent of how the data will be queried, unlike a well-normalized relational schema, which is largely access-pattern-independent by design.
Key-value modeling: designing the key itself
[BEGINNER] In a key-value store like Redis, "schema design" is largely about designing a good key naming convention — since the only way to retrieve data is by exact key, the key itself has to encode enough structure to support the actual access patterns needed:
user:42:profile -- a user's profile data
user:42:sessions -- a set of that user's active session IDs
leaderboard:2026-01 -- a monthly leaderboard, naturally scoped by key
rate_limit:api:user:42 -- a rate-limit counter, scoped by user and resource
A consistent, hierarchical naming convention (entity:id:attribute) makes a key-value store's data organized and navigable despite having no schema enforcement or query language at all — the convention itself is the schema, informally.
Common mistakes
- Applying relational normalization instincts directly to NoSQL schema design — normalizing a MongoDB schema into many small, cross-referenced collections (mimicking relational tables) usually defeats the entire performance benefit document stores offer for access patterns that actually want denormalized, embedded data.
- Choosing to embed or reference in a document database without first identifying the actual, real access pattern — this is the single decision point that determines which choice is correct, and it can't be made correctly by looking at the data's structure alone.
- Designing a wide-column or key-value schema without a clear, specific query pattern in mind, then discovering the chosen key/row structure can't efficiently answer a genuinely important query — a costly mistake to discover after the system is in production with real data.
- Using an inconsistent or ad hoc key-naming convention in Redis, making the "schema" impossible for other engineers to understand or extend without reading through scattered application code.
Checklist: designing schemas for the right model
- For relational schemas, normalize first (Chapter 2), and trust SQL's join/aggregation flexibility to handle queries decided later — you don't need to know every future query pattern upfront.
- For NoSQL schemas (especially document and wide-column), identify the actual, real access patterns FIRST, and design the data's shape (embedded vs. referenced, which fields belong in which key) around those patterns specifically — not around the data's abstract structure alone.
- When a NoSQL access pattern changes significantly after initial design, expect to need real schema/data-model changes — this is a normal, expected part of working with access-pattern-driven models, not a sign the original design was wrong.
- Establish and document a consistent key-naming convention for any key-value store, treating it as the closest equivalent to a schema that a keyspace with no formal structure actually has.
Discussion / interview questions
-
"What's the fundamental difference in approach between designing a relational schema and designing a MongoDB document schema?" Strong answer: Relational schemas are designed structure-first (normalize, then query flexibly later via SQL); NoSQL document schemas should be designed access-pattern-first (identify the actual queries needed, then shape the document structure — embedded vs. referenced — specifically around them), because document databases lack general-purpose join flexibility.
-
"When would you embed related data in a MongoDB document versus referencing it as a separate document?" Strong answer: Embed when the related data is always accessed together with its parent (e.g., a post's comments, if always fetched together); reference when the related data needs to be queried independently of its logical parent (e.g., all of one author's comments across many posts) — the decision depends entirely on the actual access pattern, not the data's abstract relationship structure.
-
"Why might a well-normalized relational schema handle unanticipated future queries better than a well-designed NoSQL schema for the same data?" Strong answer: SQL's join and aggregation capabilities let a normalized relational schema answer a wide range of queries decided after the schema was designed; NoSQL schemas (especially document/wide-column) are typically optimized around specific, anticipated access patterns, so an unanticipated new query pattern often requires restructuring the data model rather than simply writing new SQL.
-
"How is 'schema design' expressed in a key-value store like Redis, which has no formal schema at all?" Strong answer: Through a consistent, hierarchical key-naming convention (e.g.,
entity:id:attribute) that encodes enough structure to support the needed access patterns — since retrieval only works by exact key, the naming convention itself functions as the informal schema.
Chapter summary / cheat sheet
- Relational schema design: structure-first (normalize), trusting SQL's flexibility to handle future queries — largely access-pattern-independent by design.
- NoSQL schema design (document, wide-column especially): access-pattern-first — identify real queries needed, then shape the data (embed vs. reference, key structure) specifically around them.
- The embed-vs-reference decision in a document database depends entirely on whether related data is typically accessed together or queried independently — not on the data's abstract relationship shape.
- A key-value store's "schema" is really its key-naming convention — establish and document one consistently.
- Applying relational, structure-first instincts directly to NoSQL design is one of the most common and costly schema design mistakes when transitioning between the two paradigms.