Section 3: Data Modeling

Denormalization in Cassandra

Master the art of data duplication! Learn why Cassandra requires denormalization, how to design tables for specific queries, and avoid common anti-patterns with visual examples.

Advertisement

📖 How Instagram Handles 500+ Million Posts Daily

When you open Instagram and scroll your feed, you see posts from people you follow. Behind this simple interaction lies a critical design question: How do we efficiently serve your personalized feed?

❌ The Traditional SQL Approach (Doesn't Scale)

In a normalized SQL database, you might have:

-- SQL Tables (Normalized)
Users Table:        Posts Table:           Follows Table:
user_id | name     post_id | user_id    follower_id | following_id
--------|------    --------|--------    ------------|-------------
123     | Alice    1       | 456        123         | 456
456     | Bob      2       | 789        123         | 789

To show Alice's feed, SQL must:

  • JOIN Follows table to find who Alice follows (456, 789...)
  • JOIN Posts table to get posts from those users
  • Sort by timestamp, apply filters, paginate
  • Result: Multiple tables scanned, slow for 500M+ posts!

✅ The Cassandra Approach (Denormalized)

Instead, Instagram stores a denormalized feed table per user:

-- Cassandra Table (Denormalized)
user_feed Table:
user_id | timestamp | post_id | author_id | author_name | photo_url | caption | likes
--------|-----------|---------|-----------|-------------|-----------|---------|------
123     | 2025-...  | 1       | 456       | Bob         | url1      | "..."   | 1500
123     | 2025-...  | 2       | 789       | Charlie     | url2      | "..."   | 2300

Now to show Alice's feed:

  • ✅ Single query: SELECT * FROM user_feed WHERE user_id = 123 LIMIT 50
  • ✅ All data pre-computed and ready
  • ✅ No JOINs, no sorting (already sorted by timestamp)
  • ✅ Sub-millisecond response, scales to billions!

🎯 The Tradeoff

Yes, data is duplicated (Bob's post appears in every follower's feed).
But this duplication enables instant reads for 2+ billion users!
Write complexity → Read simplicity

📊 What is Denormalization?

Simple Definition

Denormalization is the intentional duplication of data across multiple tables to optimize for specific query patterns. Instead of storing data once and joining tables, you store complete query results in each table.

📘 Normalization (SQL)

Store data once, no duplication

  • Goal: Eliminate redundancy
  • Method: Split into many tables
  • Reads: JOIN tables together
  • Writes: Simple (single location)
  • Best for: OLTP, complex queries

📗 Denormalization (Cassandra)

Store data many times, embrace duplication

  • Goal: Optimize for fast reads
  • Method: One table per query
  • Reads: No JOINs (single table)
  • Writes: Complex (multiple tables)
  • Best for: High-scale reads, known queries

The Mindset Shift

From SQL thinking: "Store data once, query flexibly"

To Cassandra thinking: "Know your queries first, then duplicate data to serve each query optimally"

Storage is cheap, read latency is expensive! Cassandra trades disk space for speed.

🤔 Why Does Cassandra Require Denormalization?

Understanding the technical reasons behind denormalization.

🚫

No JOINs Allowed

Cassandra doesn't support JOINs between tables.

Why?

  • Data distributed across nodes
  • JOINs require gathering data from multiple nodes
  • Network overhead kills performance
  • Unpredictable latency at scale

Solution: Pre-compute joins via denormalization

🎯

Query-Driven Design

Tables are designed for specific queries

The Rule:

  • Each query gets its own table
  • Table structure matches query pattern
  • Primary key enables efficient lookup
  • All needed data in one row

Result: Every query = single partition read

⚡

Fast Read Performance

Reads must be lightning fast

How denormalization helps:

  • Single partition read = O(1) time
  • No disk seeks across tables
  • Data co-located on same node
  • Predictable sub-millisecond latency

Tradeoff: Slower writes, blazing reads

📈

Horizontal Scalability

Enables linear scaling

Why it matters:

  • Each query hits single partition
  • Partitions distributed across nodes
  • Add nodes = add capacity linearly
  • No cross-node coordination

Benefit: Scale to billions of rows

The Cost of Denormalization

  • More Storage: Same data stored multiple times (but storage is cheap)
  • Write Complexity: Must update multiple tables for one logical change
  • Eventual Consistency: Updates may not be immediately consistent across tables
  • Data Integrity: Application must maintain consistency (no DB constraints)

Worth it? Yes! When you need to serve millions of reads per second.

👀 Visual Comparison: Normalized vs Denormalized

Let's see the difference with a real example: a music streaming app.

Normalized (SQL) vs Denormalized (Cassandra) ❌ SQL - Normalized (3 Tables) users user_id name email created_at 4 columns songs song_id title artist album 4 columns plays user_id (FK) song_id (FK) play_count last_played device 5 columns Query: "Get user's top songs" SELECT s.title, s.artist, p.play_count FROM plays p JOIN songs s ON p.song_id = s.song_id ✅ Cassandra - Denormalized (1 Table) user_top_songs user_id (PK) 123 play_count (CK) 5000 song_id 456 title "Bohemian..." artist "Queen" album "A Night at..." last_played 2025-01-15 device "mobile" 8 columns - ALL data in one row! Query: "Get user's top songs" SELECT * FROM user_top_songs WHERE user_id = 123 LIMIT 10 3 tables, 2 JOINs, multiple disk seeks ❌ 1 table, 0 JOINs, single partition read ✅

Performance Impact

SQL Approach (Normalized):

  • Query time: 50-200ms (with indexes, on small data)
  • Scales poorly: JOINs get slower as data grows
  • Requires powerful single server or complex sharding

Cassandra Approach (Denormalized):

  • Query time: 1-5ms (consistent, even at massive scale)
  • Scales linearly: Add nodes → add capacity
  • Runs on commodity hardware distributed globally

Result: 10-100x faster reads at web scale!

🐦 Real Data Example: Twitter Timeline

Let's see how denormalization works with actual data. Imagine you're building Twitter's timeline feature.

The Scenario

User @alice follows: @bob, @charlie, @david (3 people)

When Alice opens Twitter, she should see: All tweets from people she follows, sorted by time

❌ SQL Approach (Normalized)

Three separate tables, requires JOINs:

📋 users
user_id username
1alice
2bob
3charlie
4david
👥 follows
follower following
1 (alice)2 (bob)
1 (alice)3 (charlie)
1 (alice)4 (david)
🐦 tweets
tweet_id user_id text
1012Hello!
1023Great day
1034Learning
-- ❌ SQL Query (Slow - requires 2 JOINs)
SELECT t.tweet_id, u.username, t.text, t.created_at
FROM tweets t
JOIN follows f ON t.user_id = f.following_id
JOIN users u ON t.user_id = u.user_id
WHERE f.follower_id = 1  -- alice's ID
ORDER BY t.created_at DESC
LIMIT 50;

-- Problem: Scans follows table (3 rows) + tweets table (millions!) + users table

✅ Cassandra Approach (Denormalized)

Single table with all data pre-computed:

📱 timeline_by_user (Alice's Timeline)
user_id (PK) created_at (CK) tweet_id author_id author_name text likes
1 2025-01-15 14:30 103 4 david Learning Cassandra! 42
1 2025-01-15 12:15 102 3 charlie Great day today! 128
1 2025-01-15 10:00 101 2 bob Hello world! 256

✨ Notice: All data Alice needs is in ONE partition! Author names, likes, everything!

-- ✅ Cassandra Query (Fast - single partition read!)
SELECT * FROM timeline_by_user 
WHERE user_id = 1  -- alice's ID
LIMIT 50;

-- Result: Instant! All data in one partition, pre-sorted by timestamp

📊 SQL Performance

  • Query time: 50-500ms
  • Tables scanned: 3
  • Rows examined: 1000s-millions
  • JOINs required: 2
  • Scales: Poorly (gets slower)

⚡ Cassandra Performance

  • Query time: 1-3ms
  • Tables scanned: 1
  • Rows examined: 50 (exactly what's needed)
  • JOINs required: 0
  • Scales: Linearly (stays fast)

"But how does the data get there?"

Great question! When Bob posts a tweet:

  • Step 1: Store tweet in tweets_by_author table (Bob's tweets)
  • Step 2: Look up Bob's followers (Alice, Emma, Frank...)
  • Step 3: Write tweet to EACH follower's timeline table:
    • INSERT INTO timeline_by_user (user_id=alice, ...) VALUES (...)
    • INSERT INTO timeline_by_user (user_id=emma, ...) VALUES (...)
    • INSERT INTO timeline_by_user (user_id=frank, ...) VALUES (...)

Result: Bob has 10,000 followers? Write tweet 10,000 times!

This is called "fan-out on write" - do the hard work ONCE so millions of reads are instant!

💡 Practical Example: E-commerce Order System

Let's design tables for an e-commerce system to see denormalization in action.

Step 1: Identify Query Patterns

Our Application Needs

  • Q1: Get all orders for a specific user (user dashboard)
  • Q2: Get order details by order ID (order confirmation page)
  • Q3: Get all orders by status (admin panel - "show pending orders")

Step 2: Create One Table Per Query

Table 1: orders_by_user

Use Case: User dashboard - "Show me all MY orders"

📦 Sample Data
user_id order_date order_id total status products
john_123 2025-01-15 ORD-789 $299.99 shipped ['Laptop', 'Mouse']
john_123 2025-01-10 ORD-456 $49.99 delivered ['Headphones']
john_123 2025-01-05 ORD-123 $899.00 delivered ['iPhone', 'Case']
CREATE TABLE orders_by_user (
    user_id UUID,
    order_date TIMESTAMP,
    order_id UUID,
    total_amount DECIMAL,
    status TEXT,
    shipping_address TEXT,
    -- Duplicated product info
    product_names LIST<TEXT>,
    product_quantities LIST<INT>,
    PRIMARY KEY (user_id, order_date, order_id)
) WITH CLUSTERING ORDER BY (order_date DESC);

-- Query: Get John's last 10 orders
SELECT * FROM orders_by_user 
WHERE user_id = 'john_123' 
LIMIT 10;

-- Returns: All 3 orders instantly! Already sorted by date!

Table 2: orders_by_id

Use Case: Order details page - "Show me order ORD-789"

📋 Sample Data (Single Order View)
order_id user_id user_name user_email total status products
ORD-789 john_123 John Doe john@email.com $299.99 shipped ['Laptop'=$279, 'Mouse'=$20]

✨ Notice: User info (name, email) is DUPLICATED here! No need to look up user table.

CREATE TABLE orders_by_id (
    order_id UUID,
    user_id UUID,
    order_date TIMESTAMP,
    total_amount DECIMAL,
    status TEXT,
    shipping_address TEXT,
    -- Duplicated user info (no lookup needed!)
    user_name TEXT,
    user_email TEXT,
    -- Duplicated product info
    product_names LIST<TEXT>,
    product_quantities LIST<INT>,
    product_prices LIST<DECIMAL>,
    PRIMARY KEY (order_id)
);

-- Query: Get complete order details
SELECT * FROM orders_by_id 
WHERE order_id = 'ORD-789';

-- Returns: Everything in ONE read! User name, email, products, prices!

Table 3: orders_by_status

Use Case: Admin panel - "Show me all PENDING orders"

🔧 Sample Data (Admin View)
status order_date order_id user_id user_name total
pending 2025-01-15 14:30 ORD-999 sarah_456 Sarah Smith $599.00
pending 2025-01-15 12:00 ORD-888 mike_789 Mike Johnson $149.99
pending 2025-01-15 09:30 ORD-777 emma_321 Emma Wilson $89.99

⚠️ Same order data appears in ALL 3 tables! That's denormalization!

CREATE TABLE orders_by_status (
    status TEXT,
    order_date TIMESTAMP,
    order_id UUID,
    user_id UUID,
    total_amount DECIMAL,
    -- Duplicated data again!
    user_name TEXT,
    shipping_address TEXT,
    PRIMARY KEY (status, order_date, order_id)
) WITH CLUSTERING ORDER BY (order_date DESC);

-- Query: Get all pending orders
SELECT * FROM orders_by_status 
WHERE status = 'pending';

-- Returns: All 3 pending orders sorted by date! Admin can process them.

Notice the Duplication?

The SAME order data appears in 3 different tables!

  • orders_by_user: Stores order for user's dashboard
  • orders_by_id: Stores order for order details page
  • orders_by_status: Stores order for admin filtering

When an order is created: Write to all 3 tables!

When an order is updated: Update all 3 tables!

This is intentional! Each query reads from exactly ONE table.

🔄 How Writes Work Across All Tables

When Sarah places an order for $599, here's what happens:

Single Order → Written to 3 Tables 👤 User Action Sarah clicks "Place Order" Order: Laptop ($599) ⚙️ Application Layer Batches 3 write operations orders_by_user user_id: sarah_456 order_id: ORD-999 total: $599.00 products: ['Laptop'] orders_by_id order_id: ORD-999 user_name: Sarah Smith user_email: sarah@email.com total: $599.00 orders_by_status status: pending order_id: ORD-999 user_name: Sarah Smith total: $599.00 📝 Actual CQL Batch Statement BEGIN BATCH INSERT INTO orders_by_user (user_id, order_id, ...) VALUES ('sarah_456', 'ORD-999', ...); INSERT INTO orders_by_id (order_id, user_name, ...) VALUES ('ORD-999', 'Sarah', ...); INSERT INTO orders_by_status (status, order_id, ...) VALUES ('pending', 'ORD-999', ...); APPLY BATCH;

✅ Read Benefits

  • User dashboard: 1 query to orders_by_user
  • Order details: 1 query to orders_by_id
  • Admin panel: 1 query to orders_by_status
  • Result: All queries ⚡ instant!

⚠️ Write Cost

  • 3 tables to update
  • 3x write load on database
  • Consistency must be maintained
  • Tradeoff: Worth it for fast reads!

🎵 Another Real Example: Spotify Playlists

How does Spotify show "Songs in a Playlist" AND "Playlists containing a Song"?

📱 songs_by_playlist

Query: "Show songs in 'Workout Mix'"

playlist_id song
workout_mixEye of Tiger
workout_mixLose Yourself
workout_mixStronger
🎵 playlists_by_song

Query: "Which playlists have 'Stronger'?"

song_id playlist
strongerWorkout Mix
strongerParty Hits
stronger2000s Classics

The Pattern

Many-to-many relationship? Create 2 tables!

  • Table 1: songs_by_playlist (partition key: playlist_id)
  • Table 2: playlists_by_song (partition key: song_id)
  • Both directions of the relationship get their own table!
  • When you add a song to a playlist → write to BOTH tables
Advertisement

📈 Real-World Performance Metrics

Let's look at actual numbers from production systems to understand the impact.

🏢 Company: Netflix

Users: 260+ million
Read latency: < 5ms (p99)
Tables per query: 1 table
Duplication factor: 3-5x
Storage cost: < 1% of infrastructure

🛒 Company: Uber Eats

Orders per day: 6+ million
Read latency: < 10ms
Write amplification: 4-6x
Tables per entity: 3-4 tables
Availability: 99.99%

Comparison: Before vs After Denormalization

A real e-commerce company's migration story:

Metric Before (SQL + JOINs) After (Cassandra Denormalized) Improvement
User Dashboard Load 250ms average 8ms average 31x faster ⚡
Order Details Page 180ms average 5ms average 36x faster ⚡
Peak Hour Throughput 5,000 req/sec 50,000 req/sec 10x capacity 📈
Database Servers 12 powerful servers 15 commodity nodes 60% cost reduction 💰
Storage Used 2 TB 6 TB (3x duplication) 3x more storage 📦
Write Latency 5ms 12ms (3 tables) 2.4x slower writes ⏱️
Customer Complaints 450/month (slow pages) 12/month 97% reduction 😊
⚡

30-40x

Faster read queries

📈

10x

Higher throughput capacity

📦

3-5x

More storage needed

The Math: Is Denormalization Worth It?

Let's calculate the total cost for a mid-sized app (1M users, 5M requests/day):

Option 1: SQL with JOINs (Normalized)

  • Storage: 500GB * $0.10/GB = $50/month
  • Compute (powerful servers): $2,000/month
  • Slow queries → Need caching layer: $500/month
  • Engineering time fixing performance: $5,000/month
  • Total: ~$7,550/month

Option 2: Cassandra Denormalized

  • Storage: 1.5TB (3x duplication) * $0.10/GB = $150/month
  • Compute (commodity nodes): $1,200/month
  • No caching needed: $0/month
  • Minimal performance tuning: $500/month
  • Total: ~$1,850/month

💰 Result: $5,700/month savings + 30x faster performance!

The "expensive" 3x storage costs $100 extra, but saves $5,800 in other costs!

✅ Denormalization Best Practices

Follow these guidelines to denormalize effectively.

1️⃣

Know Your Queries First

Start with application requirements

Process:

  • List all queries your app needs
  • Define access patterns
  • Understand filtering requirements
  • Then design tables

❌ Don't: Design tables first like SQL

2️⃣

One Table Per Query Pattern

Each unique query gets its own table

Example:

  • "User's orders" → orders_by_user
  • "Order by ID" → orders_by_id
  • "Pending orders" → orders_by_status

✅ Result: Every query = single partition read

3️⃣

Store Complete Data in Each Row

Include all data needed for the query

Don't require:

  • Secondary lookups
  • Application-side joins
  • Additional queries

✅ One query should return everything needed

4️⃣

Use Batches for Consistency

Write to multiple tables atomically

BEGIN BATCH
  INSERT INTO orders_by_user ...
  INSERT INTO orders_by_id ...
  INSERT INTO orders_by_status ...
APPLY BATCH;

✅ All-or-nothing consistency

5️⃣

Accept Eventual Consistency

Different tables may be briefly out of sync

Why:

  • Distributed system realities
  • Network delays between writes
  • Cassandra's AP design (CAP)

✅ Usually consistent within milliseconds

6️⃣

Avoid Unbounded Growth

Don't let partitions grow forever

Solution:

  • Time-bucket data (daily/monthly)
  • Set TTLs for old data
  • Archive to cold storage

⚠️ Partitions > 100MB slow down

❌ Common Denormalization Anti-Patterns

Mistakes to avoid when denormalizing data.

Anti-Pattern #1: Normalizing in Cassandra

Mistake: Trying to normalize data like you would in SQL

-- ❌ BAD: Normalized structure requiring "joins"
CREATE TABLE users (user_id UUID PRIMARY KEY, name TEXT);
CREATE TABLE orders (order_id UUID PRIMARY KEY, user_id UUID);

-- Application must do 2 queries:
1. SELECT * FROM orders WHERE order_id = X;
2. SELECT * FROM users WHERE user_id = Y;  -- lookup from step 1

Problem:

  • Requires multiple queries
  • Application-side "joins"
  • Defeats Cassandra's strengths

✅ Fix: Duplicate user data in orders table

Anti-Pattern #2: One Giant Table

Mistake: Putting all data in one massive table and trying to query it different ways

-- ❌ BAD: Single table for all queries
CREATE TABLE everything (
    user_id UUID,
    order_id UUID,
    product_id UUID,
    ...
    PRIMARY KEY (user_id, order_id)
);

-- Can query by user_id, but NOT by order_id alone!
SELECT * FROM everything WHERE order_id = X;  -- ❌ Requires ALLOW FILTERING

✅ Fix: Create separate tables for different query patterns

Anti-Pattern #3: Not Writing to All Tables

Mistake: Forgetting to update all denormalized copies when data changes

Example:

  • Update order status in orders_by_id
  • Forget to update orders_by_user
  • Now data is inconsistent!

✅ Fix: Use batches, or application logic to update all tables

Anti-Pattern #4: Using ALLOW FILTERING

Mistake: Relying on ALLOW FILTERING for queries

-- ❌ BAD: Filtering on non-primary-key column
SELECT * FROM orders_by_user 
WHERE status = 'pending' 
ALLOW FILTERING;  -- ❌ Scans all partitions!

Problem:

  • Scans entire table (slow!)
  • Kills performance at scale
  • Sign of wrong data model

✅ Fix: Create orders_by_status table with status as partition key

Anti-Pattern #5: Unbounded Partition Growth

Mistake: Letting a single partition grow without limits

-- ❌ BAD: User's ALL orders in one partition forever
CREATE TABLE orders_by_user (
    user_id UUID,
    order_date TIMESTAMP,
    ...
    PRIMARY KEY (user_id, order_date)
);

-- After 10 years, user has 10,000 orders in one partition!
-- Partition size > 100MB → performance degradation

✅ Fix: Time-bucket partitions

-- ✅ GOOD: Partition per month
CREATE TABLE orders_by_user (
    user_id UUID,
    month TEXT,  -- "2025-01"
    order_date TIMESTAMP,
    ...
    PRIMARY KEY ((user_id, month), order_date)
);
Advertisement

💼 Top 15 Interview Questions - Denormalization

Master these questions to demonstrate expert-level understanding of Cassandra data modeling!

1
What is denormalization and why is it required in Cassandra?
+

Answer:

Denormalization is the intentional duplication of data across multiple tables to optimize read performance by avoiding JOINs.

Why Required in Cassandra:

  • No JOINs Support: Cassandra doesn't support JOINs between tables - data would need to be gathered from multiple nodes
  • Query-Driven Design: Each table is designed for a specific query pattern
  • Fast Reads: All data needed for a query is in one partition - single read operation
  • Horizontal Scalability: Each query hits one partition on one node - scales linearly
  • Distributed Architecture: JOINs across distributed nodes would kill performance

Tradeoff: Accept write complexity and data duplication to achieve blazing-fast reads at web scale.

2
How is denormalization different from normalization in SQL?
+

Answer:

Aspect Normalization (SQL) Denormalization (Cassandra)
Data Storage Store data once, no duplication Store data multiple times intentionally
Table Count Many normalized tables One table per query pattern
Reads Use JOINs across tables Single table, no JOINs
Writes Simple, one location Complex, multiple tables
Design Process Model entities and relationships Model queries and access patterns
Goal Eliminate redundancy Optimize read performance

Key Insight: SQL trades read complexity for write simplicity. Cassandra trades write complexity for read simplicity.

3
What is query-driven data modeling?
+

Answer:

Query-driven data modeling means designing your database schema based on how you plan to query the data, not on the structure of the entities themselves.

The Process:

  • Step 1: List all queries your application needs to perform
  • Step 2: For each query, design a table optimized for that specific access pattern
  • Step 3: Structure the primary key to enable efficient lookup
  • Step 4: Include all necessary data in each row (denormalize)

Example:

  • Query: "Get user's orders" → orders_by_user table with user_id as partition key
  • Query: "Get order by ID" → orders_by_id table with order_id as partition key
  • Query: "Get pending orders" → orders_by_status table with status as partition key

Rule: If you can't answer "What queries will use this table?" you're doing it wrong!

4
How do you maintain consistency when data is duplicated across multiple tables?
+

Answer:

Use BATCH statements to write to multiple tables atomically:

BEGIN BATCH
  INSERT INTO orders_by_user (user_id, order_id, ...) VALUES (...);
  INSERT INTO orders_by_id (order_id, user_id, ...) VALUES (...);
  INSERT INTO orders_by_status (status, order_id, ...) VALUES (...);
APPLY BATCH;

How Batches Help:

  • Atomicity: All writes succeed or all fail (within same partition)
  • Isolation: Batch appears as single operation
  • Timestamp Consistency: All writes get same timestamp

Important Caveats:

  • Batches are NOT transactions (no rollback)
  • Only use for writes to same partition or denormalized data
  • Eventual consistency still applies across replicas
  • Application must handle batch failures and retries

Alternative: Use application logic with retry mechanisms to ensure all tables updated.

5
What are the downsides of denormalization?
+

Answer:

Main Downsides:

  • Increased Storage: Same data stored multiple times - requires more disk space
  • Write Complexity: Single logical change requires updating multiple tables
  • Write Amplification: One user action = multiple database writes (performance cost)
  • Consistency Challenges: Keeping duplicate data in sync is application's responsibility
  • Eventual Consistency: Different tables may temporarily show different values
  • No Referential Integrity: Database doesn't enforce consistency - application must
  • Schema Changes: Updating structure requires changing multiple tables
  • Data Anomalies: Risk of insert/update/delete anomalies if not handled carefully

When It's Worth It:

  • Read-heavy workloads (90%+ reads)
  • Need for consistent low latency at massive scale
  • Known query patterns (not ad-hoc)
  • Storage cost acceptable tradeoff for performance
6
How do you decide what data to denormalize?
+

Answer:

Decision Framework:

  • Rule 1: Denormalize data needed together
    • If query needs user name + order details, store both in orders table
    • Avoid requiring secondary lookups
  • Rule 2: Consider query frequency
    • Frequently accessed data → definitely denormalize
    • Rarely accessed → may not be worth duplication
  • Rule 3: Analyze update frequency
    • Rarely changing data (product categories) → safe to denormalize
    • Frequently changing data (real-time prices) → be cautious
  • Rule 4: Consider data size
    • Small data (names, IDs) → duplicate freely
    • Large data (images, files) → store references only

Example Decision:

Order table includes user_name (small, rarely changes, needed for display) but NOT user_profile_picture (large, store URL reference instead).

7
What is "one table per query" principle?
+

Answer:

"One table per query" means creating a separate table optimized for each distinct access pattern in your application.

The Principle:

  • Each query pattern gets its own table
  • Table's primary key matches the query's WHERE clause
  • Table contains all data needed by that query
  • Query can be satisfied with single partition read

Example - User Activity System:

  • Query 1: "Get activities by user" → activities_by_user (PK: user_id)
  • Query 2: "Get activities by type" → activities_by_type (PK: activity_type)
  • Query 3: "Get today's activities" → activities_by_date (PK: date)

Why It Works:

  • Each table optimized for its specific use case
  • No need to compromise primary key for multiple access patterns
  • Guaranteed fast reads for all queries
  • Clear mapping: query → table

Cost: More tables, more writes, more storage. But reads are always fast!

8
Can you give a real-world example of when NOT to denormalize?
+

Answer:

Scenario: Real-time stock trading platform

Problem:

  • Stock prices change multiple times per second
  • Price stored in: user_portfolios, order_history, price_alerts, analytics_table
  • One price update → must update 4+ tables immediately
  • Write amplification = 4x or more
  • Risk of inconsistent prices across tables

Better Approach:

  • Don't denormalize: Store only stock_id in other tables
  • Separate price table: current_stock_prices with real-time updates
  • Application joins: Fetch price separately when displaying
  • Cache: Use Redis/Memcached for frequently accessed prices

Other "Don't Denormalize" Cases:

  • Real-time sensor data (temperature, location)
  • Live sports scores
  • Inventory counts with high turnover
  • Currency exchange rates

Rule: If data changes more frequently than it's read, reconsider denormalization!

9
What is write amplification and how does denormalization cause it?
+

Answer:

Write amplification occurs when a single logical update requires multiple physical writes to the database.

How Denormalization Causes It:

  • Same data duplicated across multiple tables
  • Changing one piece of data requires updating all copies
  • 1 logical write → N physical writes (N = number of tables)

Example:

User changes their name:
1 logical update → must write to:
  - users_by_id
  - users_by_email  
  - orders_by_user (all user's orders)
  - comments_by_user (all user's comments)
  - reviews_by_user (all user's reviews)
  
Result: 1 name change = 100+ physical writes!

Impact:

  • More I/O: Higher disk write load
  • Higher Latency: Writes take longer
  • Network Traffic: More data transferred to replicas
  • Cost: More cloud storage I/O charges

Mitigation Strategies:

  • Only denormalize rarely-changing data
  • Use batches to write to multiple tables atomically
  • Consider eventual consistency for non-critical updates
  • Accept the tradeoff for read-heavy workloads
10
How do you handle deletions in denormalized tables?
+

Answer:

Challenge: When data is duplicated, deletions must be propagated to all copies.

Strategies:

  • Option 1: Batch Deletes
    BEGIN BATCH
      DELETE FROM orders_by_user WHERE user_id = X AND order_id = Y;
      DELETE FROM orders_by_id WHERE order_id = Y;
      DELETE FROM orders_by_status WHERE status = 'pending' AND order_id = Y;
    APPLY BATCH;
  • Option 2: Soft Deletes (Recommended)
    • Add `deleted BOOLEAN` column to all tables
    • SET deleted = true instead of DELETE
    • Filter deleted rows in application
    • Periodically clean up with TTL or batch job
  • Option 3: TTL-Based Cleanup
    • Set TTL when inserting data
    • Data auto-expires after time period
    • Good for time-series or temporary data

Best Practice: Use soft deletes for most cases - easier to handle, recoverable, and avoids deletion anomalies.

11
What is the "table per query" design pattern?
+

Answer:

Already covered in Question 7 - see above for complete answer about "One Table Per Query" principle.

12
How does denormalization affect storage costs?
+

Answer:

Increased Storage Requirements:

  • Data Duplication: Same data stored N times (N = number of tables)
  • Multiplication Factor: 3 tables with duplicated data = 3x storage
  • Replication Factor: With RF=3, actual storage = 3 * N * data_size

Example Calculation:

Normalized SQL: 100GB data * RF=3 = 300GB total storage
Denormalized (3 tables): 100GB * 3 tables * RF=3 = 900GB total storage

Result: 3x more storage required!

Cost-Benefit Analysis:

  • Storage Cost: ~$0.02-0.10/GB/month (cheap!)
  • Compute Cost: Servers to handle slow queries (expensive!)
  • Latency Cost: User drop-off from slow loads (very expensive!)

Reality Check:

  • 900GB cloud storage = $9-90/month
  • Additional servers for slow queries = $500-5000/month
  • Lost revenue from poor performance = $$$$

Conclusion: Storage cost increase is trivial compared to performance gains. Modern storage is cheap!

13
What is the difference between denormalization and materialized views?
+

Answer:

Aspect Manual Denormalization Materialized Views
Definition Application creates and maintains multiple tables Cassandra automatically maintains derived tables
Control Full control over schema and updates Limited control, schema auto-derived
Writes Application handles batch writes Cassandra handles automatically
Consistency Application responsible Eventually consistent (guaranteed)
Flexibility Can denormalize differently per table Must match base table columns
Performance Optimized as needed Some overhead for view maintenance

When to Use Each:

  • Manual Denormalization: Complex business logic, need full control, different data in each table
  • Materialized Views: Simple alternate primary key on same data, reduce boilerplate code

Best Practice: Start with materialized views for simplicity, switch to manual if you need more control.

14
How do you denormalize a many-to-many relationship?
+

Answer:

Challenge: In SQL, many-to-many requires junction table. In Cassandra, we denormalize based on query patterns.

Example: Students ↔ Courses (many students take many courses)

SQL Approach (3 tables):

students: student_id, name
courses: course_id, title
enrollments: student_id, course_id  -- junction table

Cassandra Approach (2+ tables based on queries):

  • Query 1: "Get courses for student X"
  • CREATE TABLE courses_by_student (
        student_id UUID,
        course_id UUID,
        -- Denormalized course data
        course_title TEXT,
        course_instructor TEXT,
        enrollment_date TIMESTAMP,
        PRIMARY KEY (student_id, course_id)
    );
  • Query 2: "Get students in course Y"
  • CREATE TABLE students_by_course (
        course_id UUID,
        student_id UUID,
        -- Denormalized student data
        student_name TEXT,
        student_email TEXT,
        enrollment_date TIMESTAMP,
        PRIMARY KEY (course_id, student_id)
    );

Writing:

BEGIN BATCH
  INSERT INTO courses_by_student (...) VALUES (...);
  INSERT INTO students_by_course (...) VALUES (...);
APPLY BATCH;

Key Point: Each direction of the relationship gets its own table!

15
What strategies help minimize denormalization overhead?
+

Answer:

Strategies to Reduce Overhead:

  • 1. Denormalize Selectively
    • Only duplicate data that's actually needed together
    • Don't denormalize "just in case"
    • Analyze which fields each query uses
  • 2. Prefer Immutable Data
    • Denormalize data that rarely changes (categories, types)
    • Avoid denormalizing frequently updated data (prices, counts)
  • 3. Use Batches Efficiently
    • Batch writes to multiple tables together
    • Reduces network roundtrips
    • Ensures atomic application of changes
  • 4. Consider Materialized Views
    • Let Cassandra maintain simple denormalizations
    • Reduces application code complexity
    • Automatic consistency management
  • 5. Asynchronous Updates
    • For non-critical data, update asynchronously
    • Use message queues for eventual consistency
    • Improves write latency
  • 6. TTL-Based Cleanup
    • Set TTL on denormalized data that expires
    • Automatic deletion reduces maintenance
  • 7. Limit Table Count
    • Don't create table for every possible query
    • Focus on high-frequency queries
    • Accept slower performance for rare queries

Golden Rule: Denormalization is a tradeoff. Optimize for your specific use case, not theoretical perfection!

Advertisement

Responsive Ad