Relational vs document data models
You’ll see the “users and posts” example everywhere, because it highlights when normalized tables with joins shine vs when flexible documents are better.
Relational model: normalized tables
In a relational (SQL) design, you split data into related tables and connect them with keys.
Example schema:
sqlCREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Usage example: “get the latest posts with their authors”:
sqlSELECT p.id, p.title, p.created_at, u.username
FROM posts p
JOIN users u ON p.user_id = u.id
ORDER BY p.created_at DESC
LIMIT 20;
Why this works well
- Normalization: user data (email, username) is stored once, in
users.- Update email in one row → all related queries see the change.
- Joins: you combine tables on keys at query time.
- You don’t duplicate user info into every post.
- Consistency: foreign keys ensure every
posts.user_idpoints to a real user. - Strong querying: you can easily add more related tables later (
comments,likes, etc.) and join them.
Where it can be awkward
- Very document-like fields (deeply nested JSON, lots of optional properties) can be clumsy to model as many small tables.
- Heavy cross-table joins at massive scale can become a bottleneck if not indexed well.
- Schema changes (adding columns, changing types) are more controlled, sometimes slower to roll out at huge scale.
Document model: one document, nested data
In a document database (like MongoDB), you often store a user and related posts together, or at least use flexible nested structures.
Example 1: embed small posts inside user:
json{
"_id": "user_123",
"username": "alice",
"email": "alice@example.com",
"created_at": "2024-01-01T10:00:00Z",
"posts": [
{
"post_id": "p_1",
"title": "Hello",
"body": "First post",
"created_at": "2024-01-02T09:00:00Z"
},
{
"post_id": "p_2",
"title": "Another",
"body": "More text",
"created_at": "2024-01-03T10:00:00Z"
}
]
}
Usage example: “get one user and all their posts”:
jsdb.users.findOne({ _id: "user_123" })
That’s a single document read, no join.
Example 2: separate users and posts collections, but keep posts flexible:
// users collection
{
"_id": "user_123",
"username": "alice",
"email": "alice@example.com"
}
// posts collection
{
"_id": "p_1",
"user_id": "user_123",
"title": "Hello",
"body": "First post",
"tags": ["intro", "fun"],
"metadata": {
"likes": 10,
"views": 200,
"language": "en"
}
}
You can add metadata.language, tags, or any new field without a schema migration.
Pros of document style
- Flexible schema: each document can have slightly different fields (
metadata,settings, etc.). - Good for aggregates you almost always load together (user profile + preferences + small list of recent posts).
- Fewer joins: data that naturally “belongs together” can live together.
Cons / tradeoffs
- Duplication risk: if each post embeds a full copy of the user profile, updating user info means touching many documents.
- Atomicity scope: a single document is typically the atomic unit; multi-document transactions are possible in some systems but more complex/slower.
- Complex queries that join many different entity types can be harder or less efficient.
When normalization and joins vs documents matter
Use normalized relational tables when:
- Many entities reuse shared data (one user, many posts, many comments).
- You care strongly about referential integrity (
posts.user_idmust always point to a real user). - You run lots of different ad-hoc queries (analytics, reporting) over many relationships.
Use documents when:
- Each “thing” is mostly self-contained (a user profile with settings, a post with comments limited in number).
- The structure evolves often and you don’t want frequent schema migrations.
- Your main access pattern is “load a whole aggregate by id” (one read, no joins).
Think in terms of aggregates you read/write together: if most requests touch multiple tables at once, consider a document; if many requests combine many different entities in many ways, consider normalization.