PRIMARY KEYS - The Complete Story
🔑 Master Cassandra's most important concept with storylines, animations, and real scenarios!
📖 Fundamentals (Start From Zero!)
Let's understand ALL the basics before diving into primary keys!
What is a Database?
Database = Organized collection of data
Think of it like:
• Filing cabinet with folders
• Library with books
• Warehouse with shelves
Purpose:
Store information so you can:
• Save it permanently
• Find it quickly
• Update it easily
• Delete when needed
Examples:
• Instagram: stores photos, users, likes
• Amazon: stores products, orders, customers
• Your phone: stores contacts, messages
What is a Table?
Table = Organized data in rows and columns
Like a spreadsheet:
|-------|-----|-----------|
| Alice | 25 | New York |
| Bob | 30 | London |
| Carol | 28 | Tokyo |
Parts:
• Column: Type of information (Name, Age)
• Row: One complete entry (Alice's info)
• Cell: Single piece of data (25)
Every row describes one thing!
What is a Key?
Key = Unique identifier for each row
Like:
• Your passport number (unique to you!)
• License plate (unique to car)
• Order number (unique to order)
Why needed?
To find specific row instantly!
Without key:
"Find Bob's info" = Check ALL rows ✗
With key:
"Get user_id = 123" = Direct lookup ✓
Keys make databases FAST! ⚡
Uniqueness
Unique = No duplicates allowed!
Example - Student IDs:
✅ Student 001 = Alice
✅ Student 002 = Bob
✗ Student 001 = Carol (CONFLICT!)
Why important?
• Can't have two people with same ID
• Database would be confused!
• "Get student 001" → Which one?? 😱
Keys MUST be unique!
This is the golden rule ✨
Distributed Database
Distributed = Data on multiple computers
Traditional:
• One computer (server)
• All data in one place
• Limited capacity (~1TB)
Distributed (Cassandra):
• Many computers (100+)
• Data split across them
• Unlimited capacity! ✓
Challenge:
How to find data quickly?
Answer: Smart primary keys! 🔑
Data Location
In distributed systems, WHERE matters!
Problem:
Data scattered across 100 computers
Need to find Alice's data fast!
Bad approach:
Check all 100 computers = SLOW! ✗
Good approach:
Use key to calculate location!
hash(key) → Computer #42
Direct lookup = FAST! ✓
This is what primary keys enable!
Sorting
Sorting = Arranging in order
Examples:
• Numbers: 1, 2, 3, 4, 5...
• Letters: A, B, C, D, E...
• Dates: Oldest → Newest
Why sort data?
• Find things faster
• Show in order (recent first)
• Range queries (last week)
In Cassandra:
Primary key controls sorting!
Data stored pre-sorted on disk ✓
Performance
Performance = Speed of operations
Measured in:
• Milliseconds (ms)
• 1 second = 1000ms
Database speeds:
• Excellent: 1-5ms ⚡
• Good: 10-50ms ✓
• Slow: 100-500ms ⚠️
• Very slow: 1000ms+ (1 sec) ✗
Goal:
Design keys for 1-5ms queries!
Primary keys are the secret! 🔑
Hash Function
Hash = Turn data into number
Like a magic formula:
• Input: "Alice"
• Hash function: 🎩✨
• Output: 42
Same input = Same number! ✓
In Cassandra:
hash(partition_key) → Node number
Example:
hash("user_123") = 42
→ Store on Computer #42
Enables O(1) instant lookups! ⚡
🎓 Quick Reference Summary
You now understand:
• Database: Organized storage of information
• Table: Data in rows and columns
• Key: Unique identifier for each row
• Uniqueness: No duplicates allowed!
• Distributed: Data across many computers
• Data Location: WHERE data lives matters
• Sorting: Arranging data in order
• Performance: Speed measured in milliseconds
• Hash Function: Converts key to location
🎯 Ready to learn about Primary Keys!
🤔 What is a Primary Key?
🏫 The School ID Card Analogy
Imagine a large university with 50,000 students...
Problem: How to identify each student?
❌ Option 1: Use first name
"Find Alice" → 200 students named Alice! 😱
Which one? Confusion! ✗
❌ Option 2: Use full name
"Find John Smith" → 15 people with same name!
Still ambiguous! ✗
✅ Solution: Student ID Number!
Every student gets unique ID:
• Alice Johnson = Student #10234
• Bob Anderson = Student #10235
• Carol Smith = Student #10236
• Another Alice = Student #10237
Now finding is INSTANT:
"Get student #10234" → Alice Johnson ✓
No confusion! Always correct person! ⚡
📊 In Database Terms:
Students Table:
-----------|----------------|-----|-------------
10234 | Alice Johnson | 20 | Computer Sci
10235 | Bob Anderson | 21 | Biology
10236 | Carol Smith | 19 | Mathematics
10237 | Alice Lee | 22 | Physics
student_id is the PRIMARY KEY! 🔑
It provides:
1. Uniqueness: No two students have same ID
2. Fast Lookup: Find any student instantly
3. Guaranteed Identity: ID always points to correct person
This is EXACTLY what a primary key does in a database!
Formal Definition
Primary Key =
A column (or combination of columns) that uniquely identifies each row in a table.
In simple terms:
The "address" for finding specific data!
Rules:
• Must be unique
• Cannot be NULL
• Doesn't change
• Every table needs one
Without primary key = Chaos!
With primary key = Order! ✓
Purpose
Why primary keys exist:
1. Identify rows uniquely
No ambiguity, no confusion
2. Enable fast lookups
Find data in 1-5ms ⚡
3. Prevent duplicates
Can't insert same key twice
4. Control data location
(In distributed systems)
5. Enable sorting
Organized data access
Foundation of database efficiency!
In Cassandra
Cassandra's primary key has TWO parts!
PRIMARY KEY (partition_key, clustering_col)
Part 1: Partition Key
• WHERE data lives (which computer)
• Distributes data across nodes
Part 2: Clustering Columns
• HOW data is sorted within partition
• Organizes rows on disk
Together = Complete primary key!
This is Cassandra's superpower! 🚀
🏙️ The Complete City Story
🌆 Cassandra City - A Complete Analogy
Let me tell you the story of how Cassandra City organizes its 10 million residents...
🏙️ THE SETUP
Cassandra City has 10 million people! That's huge!
The city is divided into 100 districts (like neighborhoods).
Each district has its own building complex.
🗺️ PART 1: THE DISTRICTS (Partition Keys)
Problem: How to find someone among 10 million people?
❌ Bad Approach: Random placement
• People live in random districts
• To find Alice, check ALL 100 districts!
• Takes HOURS! 😱
✅ Smart Approach: Use DISTRICT NUMBER!
Rule: Calculate district from your ID number
district = hash(person_id) % 100
Example:
• Alice (ID: 12345) → hash = 42 → District 42
• Bob (ID: 67890) → hash = 17 → District 17
• Carol (ID: 11111) → hash = 89 → District 89
Now finding is INSTANT! ⚡
"Where's Alice?" → Calculate: hash(12345) = 42 → Go to District 42!
Direct lookup! No searching! 1 second! ✓
🎯 This is the PARTITION KEY!
• person_id is partition key
• Determines which district (node)
• Enables instant location finding!
🏢 PART 2: THE APARTMENTS (Clustering Keys)
You go to District 42 to find Alice. Great!
But District 42 has 100,000 people! 😱
Problem: How to find Alice among 100,000 people in district?
❌ Bad Approach: Random apartments
• People in random apartments
• Must check ALL 100,000 apartments!
• Takes DAYS! 😱
✅ Smart Approach: ORGANIZED APARTMENTS!
Solution: Arrange apartments by REGISTRATION DATE!
Building A (District 42):
Apt #2 → Person registered: 2020-01-02 (Sarah)
Apt #3 → Person registered: 2020-01-03 (Mike)
...
Apt #523 → Person registered: 2021-06-15 (Alice) ← HERE!
...
Apt #99,999 → Person registered: 2024-12-28 (Latest)
Apt #100,000 → Person registered: 2024-12-29 (Newest)
Now finding is SUPER FAST! ⚡
1. Go to District 42 (partition key)
2. Alice registered 2021-06-15
3. Binary search by date → Apartment #523
4. Found in 2 seconds! ✓
🎯 This is the CLUSTERING KEY!
• registration_date is clustering key
• Sorts apartments within district
• Enables fast search within partition!
🔑 PART 3: THE COMPLETE ADDRESS (Primary Key)
To find ANYONE in Cassandra City:
Full Address Format:
District Number + Apartment Number
Example - Alice's Address:
• District: 42 (from hash of person_id)
• Apartment: 523 (sorted by registration_date)
• Complete Address: District 42, Apt 523 ✓
In Cassandra Database:
CREATE TABLE residents (
person_id UUID, -- PARTITION KEY (district)
registration_date DATE, -- CLUSTERING KEY (apartment)
name TEXT,
age INT,
city TEXT,
PRIMARY KEY (person_id, registration_date)
);
Complete Primary Key = (person_id, registration_date)
🚀 WHY THIS IS BRILLIANT
Finding Alice:
Traditional system: Check 10,000,000 people = DAYS! ✗
Cassandra City: 2 steps = 2 SECONDS! ✓
Step 1 (Partition Key):
hash(person_id) → District 42
Reduced from 10M to 100K people! (99% eliminated!)
Step 2 (Clustering Key):
Binary search by date → Apartment 523
Reduced from 100K to 1 person! (100% accurate!)
Total time: 1-5 milliseconds! ⚡
This is the POWER of Cassandra's Primary Keys!
🏢 Partition Key - Deep Dive
Understanding the first part of primary key in detail!
What is Partition Key?
Partition Key = Determines WHERE data lives in the cluster
Think of it as:
• Building address in a city
• Bookshelf number in a library
• Warehouse location in a company
How it works:
1. You specify partition key value (e.g., user_id = 12345)
2. Cassandra applies hash function: hash(12345) = 42
3. Result maps to node number: Node #42
4. All data with that key goes to Node #42 ✓
Example:
CREATE TABLE users (
user_id UUID PRIMARY KEY, -- This is partition key
name TEXT,
email TEXT
);
Result: Each user's data stored on specific node! ⚡
🎬 Real Scenario: Netflix User Profiles
Challenge: Store 250 million user profiles efficiently
Table Design:
CREATE TABLE user_profiles (
user_id UUID, -- PARTITION KEY
email TEXT,
subscription_tier TEXT,
watchlist LIST<UUID>,
PRIMARY KEY (user_id)
);
How data distributes across 100 nodes:
• hash(user_A) = 17 → Node 17
• hash(user_B) = 42 → Node 42
• hash(user_C) = 89 → Node 89
• ...
• 250M users spread evenly across 100 nodes!
Finding user profile:
SELECT * FROM user_profiles WHERE user_id = ?;
1. Hash user_id → Node number
2. Go directly to that node
3. Get profile in 2-5ms ⚡
No need to check other 99 nodes!
This is why Netflix is so fast! 🚀
🔢 Clustering Key - Deep Dive
Understanding the second part of primary key in detail!
What is Clustering Key?
Clustering Key = Determines HOW data is sorted within partition
Think of it as:
• Apartment numbers in a building
• Page numbers in a book
• Alphabetical order in a phonebook
How it works:
1. All rows with same partition key are grouped together
2. Within that group, rows are sorted by clustering key
3. Sorting happens at write time (once!)
4. Reads are instant - data already sorted! ✓
Example:
CREATE TABLE user_activity (
user_id UUID,
activity_time TIMESTAMP, -- Clustering key
activity_type TEXT,
PRIMARY KEY (user_id, activity_time)
) WITH CLUSTERING ORDER BY (activity_time DESC);
Result: Activities automatically sorted newest first! 🎯
💬 Real Scenario: WhatsApp Messages
Challenge: Store billions of messages, show conversation history instantly
Table Design:
CREATE TABLE messages (
conversation_id UUID, -- PARTITION KEY
message_time TIMESTAMP, -- CLUSTERING KEY
sender_id UUID,
message_text TEXT,
PRIMARY KEY (conversation_id, message_time)
) WITH CLUSTERING ORDER BY (message_time DESC);
Data organization:
Conversation ABC-123 (Partition):
• Message at 10:30 AM → Position #1 (newest)
• Message at 10:15 AM → Position #2
• Message at 10:05 AM → Position #3
• Message at 10:00 AM → Position #4 (oldest)
Getting latest messages:
SELECT * FROM messages
WHERE conversation_id = 'ABC-123'
LIMIT 50;
Returns 50 newest messages in 2-3ms! ⚡
Already sorted - no computation needed!
This is why WhatsApp loads messages instantly! 🚀
🤝 How Partition Key + Clustering Key Work Together
🌟 Real-World Scenarios (Production Examples)
How industry giants design primary keys at massive scale!
Scenario 1: Netflix - Viewing History (250M+ Users)
Challenge: Store every show/movie watched by 250M users, enable "Continue Watching" feature
Table Design:
CREATE TABLE viewing_history (
user_id UUID, -- PARTITION KEY
watched_at TIMESTAMP, -- CLUSTERING COLUMN 1
content_id UUID, -- CLUSTERING COLUMN 2
content_title TEXT,
content_type TEXT, -- 'movie' or 'series'
season INT,
episode INT,
progress_seconds INT,
total_duration_seconds INT,
PRIMARY KEY (user_id, watched_at, content_id)
) WITH CLUSTERING ORDER BY (watched_at DESC, content_id ASC);
Why this design works perfectly:
1. Partition Key (user_id):
• All viewing history for one user stays together
• 250M users distributed evenly across nodes
• Average user has ~500 viewing records
• Partition size: ~50KB per user (manageable!)
2. Clustering Columns (watched_at DESC, content_id):
• watched_at DESC = Most recent watches first ✓
• content_id ensures uniqueness (same content watched multiple times)
• Pre-sorted = instant "Continue Watching" queries!
Query Patterns Enabled:
-- Get "Continue Watching" (last 10 items)
SELECT * FROM viewing_history
WHERE user_id = ?
LIMIT 10;
-- Result: 2ms ⚡
-- Get all watches from last 30 days
SELECT * FROM viewing_history
WHERE user_id = ?
AND watched_at > now() - 30d;
-- Result: 3-5ms ⚡
-- Check if user watched specific content
SELECT * FROM viewing_history
WHERE user_id = ?
AND watched_at > '2024-01-01'
AND content_id = ?;
-- Result: 2-3ms ⚡
Production Results:
• Query latency: P99 < 5ms (99% of queries under 5ms)
• Throughput: 500K+ queries/second globally
• Storage: ~12TB total (with compression)
• Availability: 99.99%+ uptime
Key Insight: Partition by user_id means each user's data is always together. No cross-node queries needed! 🚀
Scenario 2: Twitter - User Timeline (300M+ Active Users)
Challenge: Show personalized timeline, handle millions of tweets per day
Table Design:
CREATE TABLE user_timeline (
user_id BIGINT, -- PARTITION KEY (viewer!)
tweet_time TIMESTAMP, -- CLUSTERING COLUMN
tweet_id BIGINT,
author_id BIGINT,
author_username TEXT,
author_avatar TEXT,
tweet_text TEXT,
media_urls LIST<TEXT>,
retweet_count INT,
like_count INT,
PRIMARY KEY (user_id, tweet_time)
) WITH CLUSTERING ORDER BY (tweet_time DESC);
Critical Design Decision - Partition Key is VIEWER, not AUTHOR!
Why user_id (viewer) as partition key?
• Each user sees their own personalized timeline
• All tweets in Bob's feed stored together on same node
• Loading timeline = single partition query ✓
What if we used author_id as partition key? ✗
To show Bob's timeline:
1. Get Bob's 1000 followers
2. Query each follower's tweets (1000 partitions!)
3. Merge and sort 1000 result sets
4. Show top 50
Result: 500ms-2s (SLOW!) ✗
With user_id as partition (current design):
SELECT * FROM user_timeline
WHERE user_id = ?
LIMIT 50;
Result: 2-5ms (INSTANT!) ⚡
The Trade-off (Fan-out on Write):
When Alice posts a tweet:
1. Alice has 50,000 followers
2. Write tweet to ALL 50,000 followers' timelines
3. Result: 50,000 writes!
But this is WORTH IT because:
• Writes: 50,000 × 1ms = 50ms total (acceptable!)
• Reads: Every user loads timeline in 2ms ⚡
• Users read 100x more than they post!
Production Results:
• Timeline load: P99 < 10ms
• Handles: 6,000+ tweets/second
• Scale: 300M+ active users
• Philosophy: "Optimize for reads, users scroll constantly!"
Scenario 3: Uber - Trip History (10M+ Drivers)
Challenge: Store every trip, enable driver earnings reports, rider trip history
Multiple Tables (Query-Driven Design):
Table 1: Trips by Driver
CREATE TABLE trips_by_driver (
driver_id UUID, -- PARTITION KEY
trip_start_time TIMESTAMP, -- CLUSTERING COLUMN
trip_id UUID,
rider_id UUID,
pickup_location TEXT,
dropoff_location TEXT,
distance_km DOUBLE,
duration_minutes INT,
fare_amount DECIMAL,
driver_earnings DECIMAL,
status TEXT,
PRIMARY KEY (driver_id, trip_start_time)
) WITH CLUSTERING ORDER BY (trip_start_time DESC);
Use Case: Driver daily earnings report
SELECT SUM(driver_earnings)
Result: 5-10ms ⚡
FROM trips_by_driver
WHERE driver_id = ?
AND trip_start_time >= '2024-12-29 00:00:00'
AND trip_start_time < '2024-12-30 00:00:00';
Table 2: Trips by Rider
CREATE TABLE trips_by_rider (
rider_id UUID, -- PARTITION KEY
trip_start_time TIMESTAMP, -- CLUSTERING COLUMN
trip_id UUID,
driver_id UUID,
driver_name TEXT,
driver_rating DOUBLE,
pickup_location TEXT,
dropoff_location TEXT,
fare_amount DECIMAL,
payment_method TEXT,
PRIMARY KEY (rider_id, trip_start_time)
) WITH CLUSTERING ORDER BY (trip_start_time DESC);
Use Case: Rider "Your Trips" history
SELECT * FROM trips_by_rider
Result: 2-5ms ⚡
WHERE rider_id = ?
LIMIT 20;
Key Design Principle: Duplicate Data for Different Access Patterns!
• Same trip stored in BOTH tables
• trips_by_driver: Optimized for driver queries
• trips_by_rider: Optimized for rider queries
• Storage cost: 2x
• Query performance: 100x faster! ✓
Production Results:
• Daily trips: 20M+
• Query latency: P95 < 10ms
• Storage: ~5TB (with both tables)
• Trade-off: 2x storage cost vs instant queries
Scenario 4: Spotify - Listen History (500M+ Users)
Challenge: Store every song played, enable "Recently Played", year-end stats
Problem with Simple Design:
PRIMARY KEY (user_id, played_at)
Issue: Power users have 50,000+ plays = HUGE partition! ✗
Better Design with Time Bucketing:
CREATE TABLE listen_history (
user_id UUID,
month_bucket TEXT, -- 'YYYY-MM' format
played_at TIMESTAMP,
track_id UUID,
track_name TEXT,
artist_name TEXT,
album_name TEXT,
duration_ms INT,
played_duration_ms INT,
skip BOOLEAN,
shuffle BOOLEAN,
PRIMARY KEY ((user_id, month_bucket), played_at)
) WITH CLUSTERING ORDER BY (played_at DESC);
Composite Partition Key: (user_id, month_bucket)
• Each month = separate partition!
• Heavy user: ~3,000 plays/month = manageable partition size
• Prevents unbounded growth ✓
Query Patterns:
-- Recently Played (this month)
SELECT * FROM listen_history
WHERE user_id = ?
AND month_bucket = '2024-12'
LIMIT 50;
-- Result: 2-3ms ⚡
-- Year-end stats (12 partitions)
SELECT track_name, COUNT(*)
FROM listen_history
WHERE user_id = ?
AND month_bucket IN ('2024-01', '2024-02', ..., '2024-12')
GROUP BY track_name;
-- Result: 30-50ms (12 partitions) ✓
Production Results:
• Daily plays: 1 billion+
• Partition size: 2-5MB per user per month
• Query latency: P99 < 10ms
• TTL: 2 years (auto-delete old data)
Scenario 5: IoT - Temperature Sensors (1M+ Devices)
Challenge: Sensors report every 5 seconds, store readings, enable real-time monitoring
Disaster Design (DON'T DO THIS!):
PRIMARY KEY (sensor_id, reading_time)
Problem:
• 5-second intervals = 17,280 readings/day
• After 1 year = 6.3M readings in ONE partition! 💥
• Partition size: 500MB+ (unmanageable!) ✗
• Queries become slower over time ✗
Production Design with Hour Bucketing:
CREATE TABLE sensor_readings (
sensor_id UUID,
hour_bucket TEXT, -- 'YYYY-MM-DD-HH'
reading_time TIMESTAMP,
temperature DOUBLE,
humidity DOUBLE,
pressure DOUBLE,
battery_level INT,
PRIMARY KEY ((sensor_id, hour_bucket), reading_time)
) WITH CLUSTERING ORDER BY (reading_time DESC)
AND default_time_to_live = 2592000; -- 30 days
Hour Bucketing Benefits:
• hour_bucket = '2024-12-29-14' (2pm on Dec 29)
• Each hour = separate partition
• Readings per hour: 720 (5-sec intervals)
• Partition size: ~50KB (perfect!) ✓
Query Patterns:
-- Latest reading (current hour)
SELECT * FROM sensor_readings
WHERE sensor_id = ?
AND hour_bucket = '2024-12-29-14'
LIMIT 1;
-- Result: 1-2ms ⚡
-- Last 24 hours (24 partitions)
SELECT * FROM sensor_readings
WHERE sensor_id = ?
AND hour_bucket IN ('2024-12-28-14', '2024-12-28-15', ..., '2024-12-29-14')
ORDER BY reading_time DESC;
-- Result: 20-30ms (24 partitions) ✓
-- Average temperature last hour
SELECT AVG(temperature)
FROM sensor_readings
WHERE sensor_id = ?
AND hour_bucket = '2024-12-29-14';
-- Result: 5-10ms ✓
Production Results:
• Sensors: 1M+ devices
• Daily readings: 17B+ (17 billion!)
• Ingestion rate: 200K writes/second
• Query latency: P99 < 10ms
• Storage: 5TB (with 30-day TTL)
• Key insight: Time bucketing prevents infinite partition growth! 🚀
🎯 Common Patterns Across All Scenarios
Key Takeaways:
1. Partition by Entity: user_id, driver_id, sensor_id - group related data
2. Cluster by Time DESC: Most apps need recent data first
3. Time Bucketing: Prevent unbounded partition growth (use day/hour buckets)
4. Duplicate Data: Create multiple tables for different query patterns
5. Composite Partition Keys: (entity_id, time_bucket) for time-series
6. Trade-offs Matter: 2x storage for 100x faster queries = worth it!
7. TTL for Cleanup: Auto-delete old data (saves storage)
8. Fan-out Writes: Acceptable if reads are 100x more frequent
Result: All these companies achieve 1-10ms query latencies at billions of records! ⚡
🎯 Composite Keys - Complete Guide
Understanding complex primary keys with multiple columns!
📚 What Are Composite Keys?
Composite Key = Primary key with multiple columns
Two types:
1. Composite Partition Key: Multiple columns determine WHERE data lives
Syntax: PRIMARY KEY ((col1, col2), clustering_col)
Example: ((user_id, month), activity_time)
2. Multiple Clustering Columns: Multiple columns determine HOW data is sorted
Syntax: PRIMARY KEY (partition_key, col1, col2, col3)
Example: (blog_id, category, publish_date, title)
Can combine both!
PRIMARY KEY ((col1, col2), clustering1, clustering2)
Example 1: News Website - Articles by Category & Date
Requirements:
• Browse articles by category (Tech, Sports, Politics)
• Within category, show newest first
• Same date? Sort by title alphabetically
Table Design:
CREATE TABLE articles_by_category (
site_id TEXT, -- PARTITION KEY (single column)
category TEXT, -- CLUSTERING COLUMN 1
publish_date DATE, -- CLUSTERING COLUMN 2
title TEXT, -- CLUSTERING COLUMN 3
article_id UUID,
author TEXT,
content TEXT,
PRIMARY KEY (site_id, category, publish_date, title)
) WITH CLUSTERING ORDER BY
(category ASC, publish_date DESC, title ASC);
Data Organization:
Category: Politics (ASC - alphabetically first)
2024-12-29 | "Biden Announces New Policy"
2024-12-29 | "Congress Passes Bill"
2024-12-28 | "Senate Hearing Today"
Category: Sports
2024-12-29 | "Lakers Win Championship"
2024-12-29 | "Tennis Finals Recap"
2024-12-28 | "Football Scores"
Category: Tech
2024-12-29 | "AI Breakthrough Announced"
2024-12-29 | "New iPhone Released"
2024-12-28 | "Startup Raises $50M"
Efficient Queries:
-- Get latest Tech articles
SELECT * FROM articles_by_category
WHERE site_id = 'news-site-1'
AND category = 'Tech'
LIMIT 20;
-- Get Sports articles from last week
SELECT * FROM articles_by_category
WHERE site_id = 'news-site-1'
AND category = 'Sports'
AND publish_date > '2024-12-22';
-- Get specific article
SELECT * FROM articles_by_category
WHERE site_id = 'news-site-1'
AND category = 'Tech'
AND publish_date = '2024-12-29'
AND title = 'AI Breakthrough Announced';
Important Rule: Must query clustering columns in ORDER!
✅ WHERE category = 'Tech' (first clustering)
✅ WHERE category = 'Tech' AND publish_date > '2024-12-01' (first + second)
✗ WHERE publish_date = '2024-12-29' (skips category!)
✗ WHERE title = 'Some Article' (skips category and date!)
Example 2: Hotel Booking - Composite Partition Key
Challenge: Store bookings, prevent hot partitions (popular hotels get millions of bookings!)
Bad Design:
PRIMARY KEY (hotel_id, check_in_date)
Problem: Popular hotel in NYC = 10M bookings = HOT PARTITION! 💥
Good Design with Composite Partition Key:
CREATE TABLE hotel_bookings (
hotel_id UUID,
month_bucket TEXT, -- 'YYYY-MM'
check_in_date DATE,
booking_id UUID,
guest_name TEXT,
guest_email TEXT,
room_number INT,
room_type TEXT,
total_amount DECIMAL,
status TEXT,
PRIMARY KEY ((hotel_id, month_bucket), check_in_date, booking_id)
) WITH CLUSTERING ORDER BY (check_in_date ASC, booking_id ASC);
Composite Partition Key: (hotel_id, month_bucket)
• Each hotel-month combination = separate partition
• Hotel "Marriott-NYC" + "2024-12" = one partition
• Hotel "Marriott-NYC" + "2024-11" = different partition
• Result: Even distribution! ✓
Benefits:
• Popular hotel: 10M total bookings split into 120+ partitions (1 per month)
• Each partition: ~83K bookings (manageable!)
• Prevents hot partition ✓
• Natural for queries (usually search within month) ✓
Queries:
-- Get December bookings
SELECT * FROM hotel_bookings
WHERE hotel_id = ?
AND month_bucket = '2024-12';
-- Get bookings for specific date
SELECT * FROM hotel_bookings
WHERE hotel_id = ?
AND month_bucket = '2024-12'
AND check_in_date = '2024-12-25';
Example 3: Gaming - Player Achievements (Complex Composite)
Requirements: Multi-game platform, track achievements per game, show recent unlocks
CREATE TABLE player_achievements (
player_id UUID,
game_id UUID,
unlock_time TIMESTAMP,
achievement_id UUID,
achievement_name TEXT,
achievement_tier TEXT, -- 'Bronze', 'Silver', 'Gold'
points INT,
PRIMARY KEY ((player_id, game_id), unlock_time, achievement_id)
) WITH CLUSTERING ORDER BY (unlock_time DESC, achievement_id ASC);
Design Breakdown:
• Composite Partition: (player_id, game_id)
• Each player-game combo = separate partition
• Player Alice in "Fortnite" = partition 1
• Player Alice in "Minecraft" = partition 2
• Clustering: unlock_time DESC (recent first)
Benefits:
• Natural data grouping by game
• Prevents cross-game pollution
• Efficient "recent achievements in this game" queries
Query Examples:
-- Get Alice's Fortnite achievements
SELECT * FROM player_achievements
WHERE player_id = 'alice-uuid'
AND game_id = 'fortnite-uuid'
LIMIT 20;
-- Get recent unlocks (last 7 days)
SELECT * FROM player_achievements
WHERE player_id = 'alice-uuid'
AND game_id = 'fortnite-uuid'
AND unlock_time > now() - 7d;
📖 Composite Key Rules Summary
Key Rules:
1. Composite Partition Key: Use double parentheses ((col1, col2))
Distributes data based on BOTH columns combined
2. Query Order Matters: Must query clustering columns in sequence
Can't skip first clustering to query second!
3. Range on Last Only: Range queries (>, <) only on final clustering column in WHERE
4. Time Bucketing: Common pattern for preventing hot partitions
(entity_id, time_bucket) as composite partition key
5. Storage Trade-off: More clustering columns = more sorting overhead
Keep to 2-3 clustering columns when possible
When to use composite keys: Multiple access patterns, time-series data, preventing hot partitions!
✅ Best Practices (Production-Tested!)
8 essential guidelines for designing perfect primary keys!
1. Choose High-Cardinality Partition Keys
High-cardinality = many unique values
Good choices:
✅ user_id (millions of users)
✅ order_id (billions of orders)
✅ device_id (millions of devices)
✅ session_id (unique per session)
Bad choices:
✗ country (only ~200 values)
✗ status ('active'/'inactive')
✗ gender ('M'/'F'/'Other')
✗ boolean flags
Why: Even distribution = better performance!
Low cardinality = hot partitions = slow queries ✗
2. Always Include Partition Key in Queries
MANDATORY for performance!
✅ Good:
WHERE user_id = ?
Direct node lookup = 2ms ⚡
✗ Bad:
WHERE name = 'Alice'
Full cluster scan = 10-60s ✗
Rule: If you can't include partition key,
create new table with different partition key!
3. Use Time Bucketing for Time-Series
Prevent unbounded partition growth!
✗ Bad:
PRIMARY KEY (sensor_id, time)
Grows forever! ✗
✅ Good:
PRIMARY KEY
((sensor_id, day), time)
Bounded per day! ✓
Bucket sizes:
• Day: Normal frequency
• Hour: High frequency
• Month: Low frequency
4. Monitor Partition Sizes
Set alerts and monitor!
Recommended limits:
• Warning: 50MB
• Critical: 100MB
• Maximum: 2GB (hard limit)
Check with:
nodetool cfstats table_name
If too large:
Add time bucketing or
redesign partition key!
5. Design for Query Patterns
Know HOW you'll query!
Questions to ask:
• What queries are most common?
• Need recent data or historical?
• Range queries needed?
• Multiple access patterns?
Example:
"Show user's orders" →
PRIMARY KEY (user_id, order_date)
Different query = different table!
Query-driven design is key! 🔑
6. Use DESC for Time-Series
Most apps need recent data first!
✅ Recommended:
WITH CLUSTERING ORDER BY
(timestamp DESC)
Newest first! ⚡
Use cases:
• Activity feeds
• Chat messages
• Recent orders
• Latest logs
~90% of use cases need DESC!
7. Document Your Design
Explain WHY you chose that key!
In schema comments:
-- Partition by user_id:
-- groups user's data together
-- Cluster by timestamp DESC:
-- shows recent activity first
Document:
• Query patterns served
• Expected data volumes
• Why DESC vs ASC
• Time bucket rationale
Future you will thank you! 🙏
8. Test at Scale Early
Don't wait for production!
Load test with:
• Realistic data volumes
• Actual query patterns
• Expected growth (3-5 years)
Tools:
• cassandra-stress
• nosqlbench
• Custom scripts
Check:
• Partition sizes
• Query latencies (P99)
• Distribution (hot partitions?)
Better to redesign early! ✓
⚠️ Anti-Patterns (Critical Mistakes to Avoid!)
Learn from common catastrophic mistakes that break Cassandra!
1. Using Low-Cardinality Partition Keys
DISASTER:
PRIMARY KEY (country, user_id)
Problem:
• Only ~200 countries worldwide
• USA partition = 100M users! 💥
• Hot partition, slow queries
• Uneven load distribution
FIX:
PRIMARY KEY (user_id, country)
Millions of users = even distribution! ✓
Rule: Partition key should have
thousands to millions of unique values!
2. Unbounded Time-Series Partitions
DISASTER:
PRIMARY KEY (sensor_id, timestamp)
Problem:
• Partition grows forever!
• After 1 year: 6M+ rows
• Partition size: 500MB+
• Queries slow over time 📉
FIX - Time Bucketing:
PRIMARY KEY
((sensor_id, day_bucket),
timestamp)
Each day = new partition! ✓
Bounded size: ~17K rows/day ✓
3. Querying Without Partition Key
DISASTER:
SELECT * FROM users
WHERE email = '[email protected]';
Problem:
• Scans ALL nodes! (100+)
• Checks ALL partitions
• 10-60 seconds or timeout ✗
• May crash cluster under load
FIX - Create Appropriate Table:
CREATE TABLE users_by_email (
email TEXT PRIMARY KEY,
...
);
SELECT * FROM users_by_email
WHERE email = ?;
Direct lookup = 2ms! ⚡
4. Using ALLOW FILTERING
MAJOR RED FLAG:
SELECT * FROM users
WHERE age > 25
ALLOW FILTERING;
What it means:
"I know this is slow and dangerous,
do it anyway!" 🚨
Problem:
• Scans entire table
• Filters in memory
• Extremely slow (minutes!)
• Can OOM (out of memory)
FIX - Redesign Schema:
Create table with age as partition key
or part of composite key!
5. Wrong Clustering Order
MISTAKE:
Need recent data first, but:
PRIMARY KEY (user_id, timestamp)
-- No ORDER BY specified!
-- Default = ASC (oldest first)
Problem:
• Returns oldest data first
• Must reverse in application
• Extra processing overhead
• Wrong UX for users
FIX - Be Explicit:
WITH CLUSTERING ORDER BY
(timestamp DESC);
Always specify the order you need! ✓
6. Secondary Indexes (Misuse)
TEMPTING BUT DANGEROUS:
CREATE INDEX ON users(email);
Problem:
• Distributed index = queries all nodes!
• Slow with many matching rows
• Can timeout under load
• Not suitable for high-cardinality
When indexes fail:
• High-cardinality columns (email, user_id)
• Queries returning many rows
• Production load
FIX:
Create dedicated table with column as partition key!
CREATE TABLE users_by_email (
email TEXT PRIMARY KEY,
...
);
⚠️ Warning Signs Your Design is Wrong
Red flags that indicate problems:
🚨 Queries taking >100ms - Should be 1-10ms with good design
🚨 Using ALLOW FILTERING - Almost never the right solution
🚨 Partition warnings in logs - "Partition X is larger than 100MB"
🚨 Uneven node load - Some nodes at 90% CPU, others at 10%
🚨 Queries without partition key - Full cluster scans are disasters
🚨 Slow compactions - Large partitions cause slow maintenance
🚨 Increasing latency over time - Unbounded partitions growing
If you see these, STOP and redesign your primary keys! 🛑
Interview Questions & Answers
Complete Answer:
A primary key in Cassandra is a unique identifier for each row in a table that consists of two components: the partition key (which determines data distribution across nodes) and optional clustering columns (which determine sort order within partitions).
Structure:
PRIMARY KEY = (partition_key, clustering_column1, clustering_column2, ...)
Example: PRIMARY KEY (user_id, timestamp)
• user_id = partition key (WHERE data lives)
• timestamp = clustering column (HOW data is sorted)
Why it's critically important:
1. Data Distribution: The partition key uses a hash function to determine which node stores the data. This enables horizontal scalability - adding more nodes increases capacity linearly.
2. Query Performance: Knowing the partition key allows direct node lookup (O(1) operation) instead of scanning all nodes. This is why Cassandra can maintain 1-5ms response times even at massive scale.
3. Data Organization: Clustering columns pre-sort data at write time, making range queries and ordered retrieval extremely efficient without runtime sorting.
4. Uniqueness Guarantee: The complete primary key (partition + clustering) must be unique, preventing duplicate entries and ensuring data integrity.
Real-world analogy: Think of Cassandra as a city with 100 districts. The partition key tells you which district to go to (eliminates 99% of search space immediately). The clustering columns tell you the apartment number within that district (organized sequentially for fast lookup).
Without proper primary key design: Queries would require full cluster scans (checking all 100 nodes), taking 10-60 seconds instead of milliseconds. This is why primary key design is the foundation of Cassandra data modeling - it directly determines whether your application performs well or fails under load.
Complete Answer:
Partition Key vs Clustering Columns - Complete Comparison:
PARTITION KEY:
Purpose: Determines physical data location - which node in the cluster stores the data.
How it works: Cassandra applies a hash function to the partition key value, and the result maps to a specific node. For example, hash(user_id_123) might equal 42, meaning all data for that user goes to Node 42.
Query requirement: MUST be specified in WHERE clause for queries. You cannot query without partition key (it would require scanning all nodes).
Data grouping: All rows with the same partition key are stored together on the same node, forming a "partition."
CLUSTERING COLUMNS:
Purpose: Determines sort order of rows within a partition. Defines how data is physically organized on disk within that partition.
How it works: Data is automatically sorted by clustering columns at write time. For example, with timestamp as clustering column, messages are stored oldest-to-newest (or newest-to-oldest with DESC).
Query requirement: Optional in WHERE clause, but must be queried in order (can't skip first clustering column to query second).
Data organization: Enables efficient range queries (>, <, BETWEEN) because data is pre-sorted. Also makes LIMIT queries extremely fast - just read first N rows.
Key Differences Summary:
• Location vs Order: Partition key determines WHERE (which node), clustering determines HOW (what order)
• Distribution vs Sorting: Partition key distributes data across cluster, clustering sorts within partition
• Required vs Optional: Partition key mandatory in queries, clustering columns optional
• Hash vs Sort: Partition key uses hash function, clustering uses comparison/sorting
Real example - Chat messages:
PRIMARY KEY (conversation_id, message_time)
• Partition key (conversation_id): All messages in conversation ABC-123 stored on same node. Enables "show all messages in this conversation" query.
• Clustering column (message_time): Messages sorted chronologically. Enables "show last 50 messages" to instantly return newest 50 without scanning all messages.
Analogy: Partition key is like choosing which building in a city, clustering columns are like organizing apartments by floor and number within that building.
Complete Answer:
Querying without a partition key forces Cassandra to perform a full cluster scan, which is extremely inefficient and often results in timeout or severe performance degradation.
What happens technically:
1. Query propagation: The coordinator node must send the query to ALL nodes in the cluster (potentially 100+ nodes).
2. Full partition scans: Each node must scan ALL its partitions and rows to find matches.
3. Network overhead: All matching results must be sent back to coordinator over network, potentially transferring GBs of data.
4. Coordinator bottleneck: Coordinator must merge results from all nodes, sort them if needed, and return to client.
Performance impact:
• Small cluster (3 nodes, 1M rows): Query might take 500ms-2s (compared to 2ms with partition key)
• Medium cluster (20 nodes, 100M rows): Query likely takes 5-30 seconds
• Large cluster (100 nodes, billions of rows): Query will timeout (10-60 seconds, often hits timeout limit)
Cassandra's safeguards:
By default, Cassandra REJECTS queries without partition key to protect the cluster. You'll see error: "Cannot execute this query as it might involve data filtering and thus may have unpredictable performance."
You CAN force it with ALLOW FILTERING, but this is a red flag:
SELECT * FROM users WHERE age > 25 ALLOW FILTERING;
This tells Cassandra "I know this is slow and dangerous, do it anyway." It's almost never the right solution - indicates schema should be redesigned.
Why partition key is mandatory - analogy:
Imagine a library with 100 branches across a city. Each branch has 100,000 books.
• With partition key (book's branch number): "Go to Branch 42, find book" = 2 minutes
• Without partition key (just title): "Check all 100 branches for this title" = 3+ hours
The solution: Design tables for specific query patterns. If you need to query by age, create a table with age as partition key (or part of it). This is the essence of Cassandra's query-driven design philosophy.
Bottom line: Not specifying partition key defeats Cassandra's architecture. It's like trying to use a Ferrari as a tractor - fundamentally wrong tool for the job. Design schema with partition keys matching your query patterns.
Complete Answer:
Scenario: Storing temperature readings from millions of IoT sensors, each reporting every 5 seconds.
Initial naive design (DON'T DO THIS):
PRIMARY KEY (sensor_id, reading_time)
Problems:
• One sensor = 17,280 readings per day (every 5 sec × 86,400 sec/day)
• After 1 year = 6.3 million readings in one partition
• Partition grows unbounded = hot partition, slow queries, eventual failure
Better design - Time bucketing:
PRIMARY KEY ((sensor_id, day_bucket), reading_time)
Explanation:
• Composite partition key: (sensor_id, day_bucket)
• day_bucket format: '2024-12-29'
• Clustering: reading_time DESC (newest first)
Benefits:
1. Bounded partitions: Each day is separate partition, max 17,280 readings
2. Natural querying: "Last 24 hours" queries single partition
3. TTL friendly: Can set TTL on older partitions to auto-delete
4. Even distribution: Millions of sensors × 365 days = excellent distribution
Complete table design:
CREATE TABLE sensor_readings (
sensor_id UUID,
day_bucket TEXT,
reading_time TIMESTAMP,
temperature DOUBLE,
humidity DOUBLE,
battery_level INT,
PRIMARY KEY ((sensor_id, day_bucket), reading_time)
) WITH CLUSTERING ORDER BY (reading_time DESC)
AND default_time_to_live = 2592000; -- 30 days
Query patterns enabled:
1. Latest reading: SELECT * FROM sensor_readings WHERE sensor_id = ? AND day_bucket = '2024-12-29' LIMIT 1;
2. Last hour: SELECT * FROM sensor_readings WHERE sensor_id = ? AND day_bucket = '2024-12-29' AND reading_time > now() - 1h;
3. Today's data: SELECT * FROM sensor_readings WHERE sensor_id = ? AND day_bucket = '2024-12-29';
Advanced consideration - Hour bucketing for high-frequency sensors:
If sensors report every second (86,400 readings/day):
PRIMARY KEY ((sensor_id, hour_bucket), reading_time)
hour_bucket format: '2024-12-29-14' (year-month-day-hour)
Result: ~3,600 readings per partition (manageable size)
Key principles applied:
• Partition key ensures even distribution
• Time bucketing prevents unbounded partition growth
• Clustering by time enables efficient range queries
• DESC ordering shows latest data first (most common need)
• TTL automatically removes old data
This design scales to millions of sensors and billions of readings while maintaining consistent 2-5ms query performance.
Complete Answer:
A hot partition occurs when a single partition receives disproportionately more traffic (reads/writes) than other partitions, creating a performance bottleneck that can cascade into cluster-wide issues.
What causes hot partitions:
1. Low-cardinality partition keys: Using columns with few unique values (country, status, gender) means many rows share the same partition key. For example, PRIMARY KEY (country, user_id) with USA having 100M users creates one massive partition.
2. Celebrity/popular entity problem: Even with high-cardinality keys, some entities naturally attract more traffic. Examples: A celebrity's Twitter account getting millions of interactions, Popular products on e-commerce sites, Viral posts on social media.
3. Temporal hotspots: Time-based access patterns where recent data gets accessed far more than old data, but poor partitioning doesn't distribute this load.
4. Unbounded partition growth: Time-series data without bucketing grows indefinitely. After months/years, the partition becomes so large that even well-distributed load becomes problematic.
Symptoms of hot partitions:
• One node consistently at 90-100% CPU while others at 10-20%
• Increased latency for specific queries (P99 latency spikes)
• "Partition larger than 100MB" warnings in logs
• Compaction taking hours instead of minutes
• Gradual performance degradation over time
• Timeouts for queries that used to be fast
Prevention strategies:
1. Choose high-cardinality partition keys:
✅ Good: user_id, session_id, device_id, order_id (millions of values)
✗ Bad: country, status, department, category (dozens/hundreds of values)
2. Time bucketing (most important!):
Instead of: PRIMARY KEY (sensor_id, timestamp)
Use: PRIMARY KEY ((sensor_id, day_bucket), timestamp)
This creates new partition every day, preventing unbounded growth. Choose bucket size based on data frequency:
• Hour buckets: Very high frequency (readings every second)
• Day buckets: Normal frequency (readings every few seconds/minutes)
• Month buckets: Low frequency (daily or weekly updates)
3. Composite partition keys:
Add another high-cardinality column to spread load:
PRIMARY KEY ((hotel_id, month_bucket), check_in_date)
Popular hotel's bookings split across 12+ monthly partitions instead of one massive partition.
4. Application-level sharding:
For celebrity/popular entity problem, add artificial sharding:
PRIMARY KEY ((tweet_id, shard_id), interaction_time)
Where shard_id = hash(user_id) % 10, spreading one tweet's interactions across 10 partitions.
5. Monitor partition sizes:
Use nodetool cfstats to track partition sizes:
• Warning threshold: 50MB
• Critical threshold: 100MB
• Hard limit: 2GB (failure point)
6. Use TTL for time-series data:
Auto-delete old data to prevent infinite growth:
CREATE TABLE ... WITH default_time_to_live = 2592000; -- 30 days
Real-world example - Twitter's solution:
Problem: Lady Gaga tweets → 80M followers → 80M writes (hot partition on write)
Solution: Fan-out writes to all followers' timelines at tweet time
Result: Read queries (user loading timeline) = single partition query = 2ms
Trade-off: Slower writes (50ms) but faster reads (2ms). Users read 100x more than post!
Detection and remediation:
If hot partition detected in production:
1. Identify hot partition: nodetool tablestats, nodetool tablehistograms
2. Immediate mitigation: Add read replicas, increase resources temporarily
3. Long-term fix: Redesign schema with time bucketing/composite keys
4. Migration: Create new table, write to both, backfill, switch reads, drop old
Bottom line: Hot partitions are Cassandra's #1 performance killer. Prevention through proper primary key design (especially time bucketing) is critical. Once you have hot partitions in production, fixing them requires schema redesign and data migration - expensive and risky. Design correctly from day one!
Complete Answer:
Let me walk through a complete social media application design, demonstrating Cassandra's query-driven approach.
Requirements: Social media platform like Instagram with posts, comments, likes, follows, and user feeds.
Step 1: Identify all query patterns
1. Show user's profile and bio
2. Show user's posts (newest first)
3. Show user's feed (posts from people they follow, newest first)
4. Show comments on a post (chronological order)
5. Show who liked a post
6. Show user's followers
7. Show who user is following
8. Get specific post by ID
Step 2: Design tables for each query pattern
Table 1: User Profiles
CREATE TABLE users (
user_id UUID PRIMARY KEY,
username TEXT,
email TEXT,
bio TEXT,
profile_pic_url TEXT,
created_at TIMESTAMP,
follower_count COUNTER,
following_count COUNTER
);
Query pattern: Get user profile by user_id
Why: Simple lookup, user_id known from authentication
Table 2: Posts by User
CREATE TABLE posts_by_user (
user_id UUID,
post_time TIMESTAMP,
post_id UUID,
image_url TEXT,
caption TEXT,
location TEXT,
like_count COUNTER,
comment_count COUNTER,
PRIMARY KEY (user_id, post_time)
) WITH CLUSTERING ORDER BY (post_time DESC);
Query pattern: Show all posts by specific user, newest first
Why: User profile page showing their posts
Query: SELECT * FROM posts_by_user WHERE user_id = ? LIMIT 20;
Table 3: User Timeline/Feed
CREATE TABLE user_timeline (
user_id UUID, -- viewer's ID!
post_time TIMESTAMP,
post_id UUID,
author_id UUID,
author_username TEXT,
image_url TEXT,
caption TEXT,
PRIMARY KEY (user_id, post_time)
) WITH CLUSTERING ORDER BY (post_time DESC);
Query pattern: Show personalized feed (posts from people you follow)
Why: Home page infinite scroll
Critical design: Partition by VIEWER not AUTHOR (fan-out on write)
Query: SELECT * FROM user_timeline WHERE user_id = ? LIMIT 50;
Write process (Fan-out on write):
1. Alice posts photo at 10:00 AM
2. Alice has 10,000 followers
3. Write to posts_by_user (Alice's posts)
4. Write to user_timeline for ALL 10,000 followers
5. Result: 10,001 writes (expensive!)
6. But: Each follower's timeline query = 2ms (instant!)
Table 4: Posts by ID
CREATE TABLE posts_by_id (
post_id UUID PRIMARY KEY,
user_id UUID,
post_time TIMESTAMP,
image_url TEXT,
caption TEXT,
location TEXT
);
Query pattern: Direct link to specific post
Query: SELECT * FROM posts_by_id WHERE post_id = ?;
Table 5: Comments on Post
CREATE TABLE comments_by_post (
post_id UUID,
comment_time TIMESTAMP,
comment_id UUID,
user_id UUID,
username TEXT,
comment_text TEXT,
PRIMARY KEY (post_id, comment_time)
) WITH CLUSTERING ORDER BY (comment_time ASC);
Query pattern: Show all comments on post, oldest first (chronological)
Why: ASC because people want to read comments in order posted
Query: SELECT * FROM comments_by_post WHERE post_id = ? LIMIT 100;
Table 6: Likes on Post
CREATE TABLE likes_by_post (
post_id UUID,
like_time TIMESTAMP,
user_id UUID,
username TEXT,
PRIMARY KEY (post_id, like_time)
) WITH CLUSTERING ORDER BY (like_time DESC);
Query pattern: Show who liked a post
Query: SELECT * FROM likes_by_post WHERE post_id = ? LIMIT 50;
Table 7: User Followers
CREATE TABLE followers (
user_id UUID,
follower_since TIMESTAMP,
follower_id UUID,
follower_username TEXT,
PRIMARY KEY (user_id, follower_since)
) WITH CLUSTERING ORDER BY (follower_since DESC);
Query pattern: Show Alice's followers
Query: SELECT * FROM followers WHERE user_id = ?;
Table 8: User Following
CREATE TABLE following (
user_id UUID,
following_since TIMESTAMP,
following_id UUID,
following_username TEXT,
PRIMARY KEY (user_id, following_since)
) WITH CLUSTERING ORDER BY (following_since DESC);
Query pattern: Show who Alice follows
Query: SELECT * FROM following WHERE user_id = ?;
Step 3: Data consistency strategy
When Alice posts a photo, write to multiple tables:
1. posts_by_user (Alice's posts)
2. posts_by_id (direct access)
3. user_timeline (all followers' feeds)
4. Update Alice's post count in users table
Use batch operations for atomicity where possible, but accept eventual consistency for fan-out writes.
Step 4: Optimization considerations
For celebrities (millions of followers):
Don't fan-out on write! Would create millions of writes.
Instead: Pull model - when user loads timeline, query followed users' recent posts.
Hybrid: Pre-compute for regular users, pull for celebrities.
Partition size monitoring:
Monitor posts_by_user - power users might need time bucketing:
PRIMARY KEY ((user_id, month_bucket), post_time)
Step 5: Performance results
• Load user feed: 2-5ms (single partition)
• Load user profile with posts: 3-8ms (2 queries)
• Show post with comments: 4-10ms (2 queries)
• New post with 10K followers: 100-200ms (fan-out writes)
• All queries scalable to billions of users!
Key takeaways:
1. One query pattern = One table (query-driven design)
2. Data duplication is normal and necessary
3. Partition by the entity being queried (viewer for feeds, post_id for comments)
4. Choose DESC for time-series (recent first) unless chronological order needed
5. Trade write cost for read speed (fan-out on write)
6. Plan for scale from day one (time bucketing, celebrity handling)
This demonstrates Cassandra's strength: predictable millisecond latency at massive scale by designing schema around exact query patterns!
Complete Answer:
A composite partition key uses multiple columns together to determine data distribution, written as PRIMARY KEY ((col1, col2), clustering_cols). This is fundamentally different from having multiple clustering columns - composite partition keys affect WHERE data is stored, not HOW it's sorted.
When to use composite partition keys:
1. Preventing Hot Partitions (Most Common Use Case):
Scenario: Popular hotel in NYC receives millions of bookings over time. Simple design PRIMARY KEY (hotel_id, check_in_date) creates one massive partition for that hotel, causing severe performance degradation.
Solution with composite partition key:
PRIMARY KEY ((hotel_id, month_bucket), check_in_date)
Now instead of one partition with 10M bookings, you have 120+ partitions (one per month) with ~83K bookings each. This prevents the hot partition problem while maintaining efficient queries since most booking searches are month-specific anyway.
2. Natural Data Grouping:
Scenario: Multi-tenant SaaS application where each tenant has multiple users. You want data for each tenant-user combination stored together.
PRIMARY KEY ((tenant_id, user_id), activity_time)
Benefits: All activities for specific tenant-user combo are co-located. Queries naturally filter by both tenant and user. Provides data isolation between tenants.
3. Time-Series Data Bucketing:
Scenario: IoT sensors reporting every second. Without bucketing, partitions grow unbounded.
PRIMARY KEY ((sensor_id, hour_bucket), reading_time)
This creates new partition every hour per sensor. Each partition contains ~3,600 readings (manageable size). Old partitions can be TTL'd easily. Queries for "last 24 hours" touch only 24 partitions.
How hashing works with composite keys:
Cassandra concatenates all partition key columns and hashes the combined value:
hash((sensor_id + hour_bucket)) → node number
Different combinations distribute to different nodes:
• (sensor_A, 2024-12-29-14) → Node 42
• (sensor_A, 2024-12-29-15) → Node 17
• (sensor_B, 2024-12-29-14) → Node 89
This achieves excellent distribution across cluster.
Query requirements with composite partition keys:
CRITICAL: Must specify ALL partition key columns in WHERE clause.
✅ Valid: WHERE sensor_id = ? AND hour_bucket = ?
✗ Invalid: WHERE sensor_id = ? (missing hour_bucket)
✗ Invalid: WHERE hour_bucket = ? (missing sensor_id)
This is because Cassandra needs complete partition key to calculate hash and find correct node.
Trade-offs to consider:
Advantages:
• Prevents hot partitions
• Controls partition size growth
• Enables better distribution
• Natural for time-series data
• Facilitates TTL strategies
Disadvantages:
• More complex queries (must include all key columns)
• Range queries more limited (can't range on partition key components)
• Application must calculate bucket values
• Historical queries may need to query multiple partitions
Real-world example - Spotify's listening history:
PRIMARY KEY ((user_id, month_bucket), played_at)
Heavy listener plays 3,000 songs/month. Without bucketing, after 5 years that's 180,000 rows in one partition (too large!). With monthly buckets, each partition has only ~3,000 rows (perfect size). Year-end stats require querying 12 partitions (acceptable). Old months can be TTL'd after 2 years.
Bottom line: Use composite partition keys primarily for time-series data or when dealing with entities that generate too much data for a single partition. The bucketing strategy should align with your query patterns - if you typically query "last day," use day buckets; if "last month," use month buckets.
Complete Answer:
While both serve the fundamental purpose of uniquely identifying rows, Cassandra and SQL primary keys are fundamentally different due to their underlying architectures - distributed vs single-node systems.
SQL (Relational) Primary Keys:
Purpose: Purely for uniqueness and integrity. Ensures no duplicate rows exist.
Structure: Simple - one or more columns that uniquely identify a row:
PRIMARY KEY (user_id)
or
PRIMARY KEY (order_id, line_item_id)
Physical storage: Creates clustered index (in most RDBMS). Rows stored in order by primary key on disk, but this is implementation detail - not part of schema design.
Query flexibility: Can query by ANY column, not just primary key. The database will use indexes or scan as needed. Primary key just ensures uniqueness and enables fast lookups.
Data location: Irrelevant - all data on single server (or managed by RDBMS in distributed setups).
Cassandra (Distributed) Primary Keys:
Purpose: Triple role - uniqueness, data distribution, and sort order.
Structure: Complex two-part design:
PRIMARY KEY (partition_key, clustering_columns)
or
PRIMARY KEY ((composite_partition_key), clustering_cols)
Each part has specific role:
• Partition key: WHERE data lives (which node)
• Clustering columns: HOW data is sorted within partition
Physical storage: Dictates exactly how data is distributed AND sorted:
1. Partition key → hash function → determines node
2. Within partition, rows sorted by clustering columns
3. Sort order (ASC/DESC) explicitly specified in schema
Query requirements: MUST include partition key in WHERE clause. Cannot efficiently query without it (would require scanning all nodes). This is architectural limitation, not implementation detail.
Data location: Critical! Partition key determines which of 100+ nodes stores the data. This enables horizontal scalability.
Key Differences Summary:
1. Flexibility vs Performance:
• SQL: Query any column flexibly, database optimizes
• Cassandra: Must design keys for specific queries, but gets predictable millisecond performance
2. Scaling Model:
• SQL: Vertical scaling (bigger server) or complex sharding
• Cassandra: Horizontal scaling (add nodes), partition key enables automatic distribution
3. Design Philosophy:
• SQL: Design for data integrity, query any way later
• Cassandra: Design for queries, optimize for specific access patterns
4. Sort Order:
• SQL: Can ORDER BY any column at query time (potentially slow)
• Cassandra: Sort order baked into schema, data pre-sorted on disk (always fast)
5. Composite Keys:
• SQL: All columns treated equally for uniqueness
• Cassandra: Partition vs clustering columns have completely different meanings
Practical Example - User Orders:
SQL Approach:
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT,
order_date DATE,
...
);
CREATE INDEX ON orders(user_id);
Queries:
• SELECT * FROM orders WHERE order_id = 123 (uses PK)
• SELECT * FROM orders WHERE user_id = 456 (uses index)
• SELECT * FROM orders WHERE order_date > '2024-01-01' (full scan, slow but works)
Cassandra Approach:
CREATE TABLE orders_by_user (
user_id UUID,
order_date DATE,
order_id UUID,
...
PRIMARY KEY (user_id, order_date)
) WITH CLUSTERING ORDER BY (order_date DESC);
Queries:
• SELECT * FROM orders_by_user WHERE user_id = ? (fast - direct partition)
• SELECT * FROM orders_by_user WHERE user_id = ? AND order_date > '2024-01-01' (fast - range on clustering)
• SELECT * FROM orders_by_user WHERE order_id = ? (IMPOSSIBLE without partition key!)
Need to query by order_id? Create separate table:
CREATE TABLE orders_by_id (
order_id UUID PRIMARY KEY,
user_id UUID,
...
);
Migration Challenges:
Teams moving from SQL to Cassandra often struggle because:
1. SQL's "design once, query many ways" doesn't work in Cassandra
2. Must identify all query patterns upfront
3. Data duplication (multiple tables for same logical entity) feels wrong but is necessary
4. Can't add ad-hoc queries later without schema redesign
When Each Approach Wins:
Use SQL Primary Keys when:
• Flexible, ad-hoc queries needed
• Complex JOINs and aggregations required
• Data size fits on single server (< 1TB)
• Access patterns unpredictable
Use Cassandra Primary Keys when:
• Massive scale needed (TBs to PBs)
• Predictable access patterns
• High availability critical (99.99%+)
• Consistent low latency required (1-10ms)
• Linear horizontal scalability needed
Bottom line: SQL primary keys are about uniqueness and convenience. Cassandra primary keys are about distribution, performance, and scale. Understan
Advanced Primary Key Topics
Token-Aware Routing
What is token-aware routing?
When your application knows which node owns which token ranges, it can send queries directly to the correct node, avoiding the coordinator hop.
How it works:
1. Cassandra divides the token space (-2^63 to 2^63) into ranges
2. Each node owns specific token ranges
3. partition_key → hash → token → node owner
4. Driver maintains token map and routes directly
Performance benefit:
• Without token-aware: Client → Coordinator → Data node → Coordinator → Client (2 hops)
• With token-aware: Client → Data node → Client (1 hop)
• Latency reduction: 30-50%
• Lower coordinator load
Enable in drivers:
// Java driver
.withLoadBalancingPolicy(new TokenAwarePolicy(new DCAwareRoundRobinPolicy()))
// Python driver
cluster = Cluster(load_balancing_policy=TokenAwarePolicy(DCAwareRoundRobinPolicy()))
Why this matters for primary keys:
Your partition key choice directly impacts routing efficiency. High-cardinality keys with even distribution maximize token-aware routing benefits!
Partition Key Cardinality Analysis
Mathematical approach to choosing partition keys:
Formula for partition count:
Expected partitions = Total rows / Cardinality
Example 1: E-commerce orders
• Total rows: 100M orders
• Option A - country as partition key: ~200 countries
→ 100M / 200 = 500K rows per partition! ✗ Too large!
• Option B - customer_id as partition key: 5M customers
→ 100M / 5M = 20 rows per partition ✓ Perfect!
Example 2: IoT sensors (unbounded growth)
• Sensor reports: Every 5 seconds = 17,280/day
• Option A - sensor_id only: 10M sensors
→ After 1 year: 6.3M rows per sensor! ✗
• Option B - (sensor_id, day_bucket): 10M × 365
→ 17,280 rows per partition ✓ Manageable!
Target ranges:
• Ideal partition size: 10KB - 50MB
• Ideal rows per partition: 1 - 100K
• Minimum cardinality: 1,000+ unique values
• Optimal cardinality: 1M+ unique values
Check your cardinality:
SELECT COUNT(DISTINCT partition_key) FROM table;
-- Should return thousands to millions
Write Path & Partition Keys
Understanding how writes use partition keys:
Write process step-by-step:
1. Client sends write with partition key
2. Driver calculates token: hash(partition_key) → token value
3. Token-aware routing: Driver knows which node owns token
4. Coordinator receives: Node that owns the token range
5. Replication: Write to N replicas (based on RF)
6. Commit log: Durable write to disk
7. Memtable: In-memory sorted structure
8. Acknowledge: Return success to client
Why partition key matters for writes:
• Determines which node is coordinator
• Affects which replicas receive data
• Clustering columns determine sort order in memtable
• Pre-sorting at write = fast reads later!
Write performance considerations:
• Single partition write: 1-3ms typical
• Batch to same partition: 2-5ms (efficient!)
• Batch to different partitions: 10-30ms (slower)
• Hot partition writes: Can cause write timeouts
Batch write optimization:
-- GOOD: Batch to same partition
BEGIN BATCH
INSERT INTO users (user_id, ...) VALUES (?, ...);
INSERT INTO user_activity (user_id, ...) VALUES (?, ...);
APPLY BATCH;
-- Single coordinator, efficient!
-- BAD: Batch to many partitions
BEGIN BATCH
INSERT INTO users (user_id, ...) VALUES (user1, ...);
INSERT INTO users (user_id, ...) VALUES (user2, ...);
INSERT INTO users (user_id, ...) VALUES (user3, ...);
APPLY BATCH;
-- Multiple coordinators, slow!
Denormalization Patterns
Cassandra requires different thinking from SQL:
SQL Mindset (Normalized):
One truth, multiple views through JOINs
-- SQL approach
CREATE TABLE users (user_id INT, name TEXT);
CREATE TABLE orders (order_id INT, user_id INT);
-- Get user's orders
SELECT * FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE u.user_id = ?;
Cassandra Mindset (Denormalized):
Multiple truths, no JOINs, query-driven design
-- Cassandra approach
CREATE TABLE orders_by_user (
user_id UUID, -- PARTITION KEY
order_date TIMESTAMP, -- CLUSTERING
order_id UUID,
user_name TEXT, -- Denormalized!
user_email TEXT, -- Denormalized!
order_total DECIMAL,
PRIMARY KEY (user_id, order_date)
);
-- Get user's orders (instant!)
SELECT * FROM orders_by_user
WHERE user_id = ?;
Denormalization strategies:
1. Duplicate data across tables:
Same logical entity in multiple tables with different partition keys
• orders_by_user (partition: user_id)
• orders_by_date (partition: order_date)
• orders_by_id (partition: order_id)
2. Embed related data:
Store user info in order table to avoid second query
• user_name, user_email in orders table
• product_name in order_items table
3. Use collections for one-to-many:
CREATE TABLE users (
user_id UUID PRIMARY KEY,
email_addresses SET<TEXT>,
phone_numbers LIST<TEXT>,
address MAP<TEXT, TEXT>
);
Trade-offs:
• Storage cost: 2-5x more (but storage is cheap!)
• Write complexity: Update multiple tables
• Read performance: 100x faster (no JOINs!)
• Consistency: Eventual consistency between tables
Golden rule: Optimize for reads, not writes. Users read 100x more than they write!
Partition Size Calculation
How to calculate expected partition size:
Formula:
Partition Size = (Average Row Size × Rows per Partition) + Overhead
Example: User activity logs
CREATE TABLE user_activity (
user_id UUID, -- 16 bytes
activity_time TIMESTAMP, -- 8 bytes
activity_type TEXT, -- ~20 bytes
activity_data TEXT, -- ~100 bytes
ip_address TEXT, -- ~15 bytes
PRIMARY KEY (user_id, activity_time)
);
Calculation:
1. Row size: 16 + 8 + 20 + 100 + 15 = 159 bytes per row
2. Overhead: ~20% for metadata = 159 × 1.2 = 191 bytes
3. Average user: 1,000 activities
4. Partition size: 191 × 1,000 = 191,000 bytes = ~186 KB ✓ Good!
Example: Sensor readings (unbounded)
• Row size: ~80 bytes
• Frequency: Every 5 seconds = 17,280 readings/day
• Without bucketing: 80 × 17,280 × 365 = 505 MB/year! ✗
• With day bucketing: 80 × 17,280 = 1.32 MB/partition ✓
Warning thresholds:
• < 10 MB: Excellent ✓
• 10-50 MB: Good ✓
• 50-100 MB: Warning ⚠️
• > 100 MB: Critical 🚨
• > 2 GB: Will fail 💥
Check actual partition sizes:
nodetool cfstats keyspace.table | grep "Partition Size"
Troubleshooting Primary Key Issues
Problem 1: Queries Taking 10+ Seconds
Symptoms:
• Queries that used to be fast are now slow
• Timeout errors appearing
• P99 latency > 1 second
Likely causes:
1. Querying without partition key (full cluster scan)
2. Large partition (> 100MB) causing slow reads
3. Too many tombstones in partition
Diagnosis steps:
-- Check if partition key included
TRACING ON;
SELECT * FROM table WHERE ...;
-- Look for "Sending request to X nodes" (bad if X > 1)
-- Check partition sizes
nodetool cfstats keyspace.table
nodetool tablehistograms keyspace.table
-- Check for tombstones
SELECT * FROM table WHERE partition_key = ?;
-- Enable metrics to see tombstone count
Solutions:
• Missing partition key: Redesign query to include it
• Large partition: Add time bucketing to schema
• Tombstones: Run repair, adjust gc_grace_seconds
Problem 2: Uneven Cluster Load
Symptoms:
• One node at 90% CPU, others at 10%
• Uneven disk usage across nodes
• Some nodes getting all the traffic
Likely causes:
1. Low-cardinality partition key (hot partitions)
2. Celebrity/popular entity problem
3. Poor token distribution
Diagnosis:
-- Check node load
nodetool status
nodetool ring
-- Check top partitions by size
nodetool toppartitions keyspace.table 10
-- Check cardinality
SELECT COUNT(DISTINCT partition_key) FROM table;
Solutions:
• Low cardinality: Choose different partition key with more unique values
• Celebrity problem: Add sharding column or use fan-out pattern
• Token distribution: Run nodetool rebuild, check vnodes configuration
Problem 3: "Partition Larger Than 100MB" Warnings
Symptoms:
• Warning in logs: "Writing large partition"
• Compactions taking hours
• Memory pressure on nodes
Root cause:
Unbounded partition growth, usually time-series without bucketing
Diagnosis:
nodetool cfstats keyspace.table | grep -A 10 "Partition Size"
nodetool toppartitions keyspace.table 10
Immediate mitigation:
1. Increase heap size temporarily
2. Adjust compaction strategy
3. Limit query result size (LIMIT clause)
Long-term fix:
Must redesign schema with time bucketing:
-- OLD (bad)
PRIMARY KEY (entity_id, timestamp)
-- NEW (fixed)
PRIMARY KEY ((entity_id, day_bucket), timestamp)
-- Migration required!
Problem 4: Write Timeouts
Symptoms:
• WriteTimeout exceptions
• Writes taking > 1 second
• Failed writes in application logs
Partition key related causes:
1. Writing to hot partition (overwhelmed node)
2. Large partition causing slow memtable flushes
3. Batch writes spanning many partitions
Solutions:
• Hot partition: Add composite partition key or sharding
• Large partition: Implement time bucketing
• Multi-partition batch: Break into single-partition batches
• Increase write timeout (temporary only!)
Problem 5: Cannot Query By Column X
Error message:
"Cannot execute this query as it might involve data filtering"
Cause:
Trying to query by column that's not partition key or clustering column
Example:
CREATE TABLE users (
user_id UUID PRIMARY KEY,
email TEXT,
name TEXT
);
-- This will fail!
SELECT * FROM users WHERE email = 'alice@email.com';
-- Error: Must specify partition key
Solution options:
Option 1: Create new table (BEST)
CREATE TABLE users_by_email (
email TEXT PRIMARY KEY,
user_id UUID,
name TEXT
);
SELECT * FROM users_by_email WHERE email = ?;
Option 2: Secondary index (RARELY GOOD)
Only for low-cardinality columns with limited result sets
Option 3: ALLOW FILTERING (NEVER DO THIS)
Extremely slow, defeats Cassandra's purpose
Schema Migration Guide
When You Need to Change Primary Keys
Bad news: You cannot modify primary keys on existing tables. Cassandra physically organizes data by primary key on disk.
Good news: There's a proven migration process that works at scale!
5-Step Migration Process:
Step 1: Create new table with corrected primary key
-- Old table (bad design)
CREATE TABLE sensor_data_old (
sensor_id UUID,
timestamp TIMESTAMP,
reading DOUBLE,
PRIMARY KEY (sensor_id, timestamp) -- Unbounded!
);
-- New table (fixed design)
CREATE TABLE sensor_data_new (
sensor_id UUID,
day_bucket TEXT,
timestamp TIMESTAMP,
reading DOUBLE,
PRIMARY KEY ((sensor_id, day_bucket), timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC);
Step 2: Dual-write phase
Update application to write to BOTH tables:
// Application code
void saveSensorReading(Reading reading) {
// Write to old table
session.execute(
"INSERT INTO sensor_data_old ...",
reading.sensorId, reading.timestamp, reading.value
);
// Write to new table
String dayBucket = formatDate(reading.timestamp, "yyyy-MM-dd");
session.execute(
"INSERT INTO sensor_data_new ...",
reading.sensorId, dayBucket, reading.timestamp, reading.value
);
}
Deploy this code. All new data now goes to both tables.
Step 3: Backfill historical data
-- Option A: Using Spark (for large datasets)
spark.read
.cassandraFormat("sensor_data_old", "keyspace")
.load()
.withColumn("day_bucket", date_format(col("timestamp"), "yyyy-MM-dd"))
.write
.cassandraFormat("sensor_data_new", "keyspace")
.save();
-- Option B: Using application (for smaller datasets)
// Read from old, write to new
// Run as background job
// Process in batches of 1000
Step 4: Switch reads to new table
Update application to read from new table:
// Change from:
SELECT * FROM sensor_data_old WHERE sensor_id = ?;
// To:
SELECT * FROM sensor_data_new
WHERE sensor_id = ? AND day_bucket = ?;
Deploy and monitor. All reads now use optimized table!
Step 5: Remove old table
After confirming everything works (wait 1-2 weeks):
1. Stop dual writes (remove old table writes from code)
2. Monitor for issues
3. Drop old table: DROP TABLE sensor_data_old;
Migration complete! ✓
Important notes:
• Dual-write period creates 2x storage temporarily
• Backfill can take hours/days for large datasets
• Test thoroughly in staging first
• Have rollback plan ready
• Monitor query latencies closely during migration
• Consider doing during low-traffic period
Zero-Downtime Migration Tips
- ✅ Use feature flags to control dual-write behavior
- ✅ Monitor write latency (should not increase significantly)
- ✅ Backfill in small batches (1000 rows at a time)
- ✅ Add checkpointing to resume failed backfills
- ✅ Verify data consistency before switching reads
- ✅ Keep dual writes for at least 1 week after read switch
- ✅ Have automated rollback scripts ready
Common Migration Mistakes
- ❌ Switching reads before backfill completes
- ❌ Dropping old table too quickly
- ❌ No rollback plan
- ❌ Backfilling during peak traffic
- ❌ Not testing on production-scale data
- ❌ Forgetting to update all application instances
- ❌ No monitoring during migration
Performance Tuning with Primary Keys
Benchmark: Query Performance by Design
| Query Pattern | Latency | Nodes Queried | Performance |
|---|---|---|---|
Partition key onlyWHERE user_id = ? |
1-3ms | 1 | ⚡⚡⚡ Excellent |
Partition + clusteringWHERE user_id = ? AND time > ? |
2-5ms | 1 | ⚡⚡⚡ Excellent |
Partition + LIMITWHERE user_id = ? LIMIT 50 |
2-4ms | 1 | ⚡⚡⚡ Excellent |
Multiple partitions (IN)WHERE user_id IN (?,?,?) |
5-20ms | 3 | ⚠️ OK if <10 |
Secondary indexWHERE email = ? |
50-500ms | All | ❌ Poor |
Without partition keyWHERE name = 'Alice' |
10-60s | All | 💥 Disaster |
ALLOW FILTERINGWHERE age > 25 ALLOW FILTERING |
30-180s | All | 💥 Catastrophic |
Read vs Write Optimization Patterns
Read-Optimized (Fan-out on Write)
Pattern: Twitter timeline
Write:
• Alice posts → 50K writes (all followers)
• Cost: 50-200ms
• Frequency: Rare (few posts/day)
Read:
• Load timeline → 1 partition query
• Cost: 2-5ms ⚡
• Frequency: Constant (users scroll all day)
Trade-off: Slow writes, instant reads
Best for: Read-heavy workloads (100:1 ratio)
Write-Optimized (Pull on Read)
Pattern: Celebrity tweets
Write:
• Lady Gaga posts → 1 write (her timeline)
• Cost: 1-3ms ⚡
• Frequency: Any
Read:
• Load timeline → Query all followed users
• Cost: 50-200ms
• Frequency: Rare (celebrities don't scroll feeds)
Trade-off: Fast writes, slower reads
Best for: Write-heavy or celebrity scenarios
Partition Size Impact on Performance
| Partition Size | Rows | Read Latency | Compaction Time | Status |
|---|---|---|---|---|
| < 10 MB | < 10K | 1-2ms | Seconds | ✅ Excellent |
| 10-50 MB | 10K-50K | 2-5ms | Minutes | ✅ Good |
| 50-100 MB | 50K-100K | 5-15ms | 10-30 min | ⚠️ Warning |
| 100-500 MB | 100K-500K | 15-50ms | 1-3 hours | 🚨 Critical |
| > 500 MB | > 500K | 50-500ms | Hours | 💥 Failure Risk |
Optimization Checklist
🎯 Query Optimization
- Always include partition key
- Use LIMIT for large partitions
- Query clustering columns in order
- Avoid IN with >10 partition keys
- Use token-aware driver
📊 Schema Optimization
- Time bucket time-series data
- Use DESC for recent-first
- Keep partitions under 100MB
- High-cardinality partition keys
- Denormalize for read speed
⚙️ Operational
- Monitor partition sizes
- Alert on >50MB partitions
- Regular compaction health checks
- Load balance token ranges
- Use TTL for old data
📊 Visual Design Comparison
Complete Primary Key Cheat Sheet
🎯 The Essential Rules (Print This!)
✅ ALWAYS DO
- Include partition key in WHERE - Every query, no exceptions
- Use high-cardinality keys - Millions of unique values
- Time bucket time-series - (entity_id, day_bucket)
- Monitor partition sizes - Keep under 100MB
- Use DESC for recent data - 90% need newest first
- Design for queries - Not for data structure
- Denormalize freely - Storage is cheap
- Test at production scale - Before going live
- Document your decisions - Explain the WHY
- Use token-aware driver - 30-50% faster
❌ NEVER DO
- Query without partition key - Full cluster scan
- Use ALLOW FILTERING - Redesign schema instead
- Low-cardinality partition keys - country, status, etc.
- Unbounded partitions - Always use time bucketing
- Skip clustering columns - Must query in order
- Secondary indexes casually - Only for specific cases
- Range on partition key - Doesn't work
- Ignore partition warnings - Fix immediately
- Design like SQL - No JOINs in Cassandra
- Wait to fix hot partitions - Gets worse over time
⚡ Quick Decision Matrix
| If Your Data Is... | Use This Pattern | Example |
|---|---|---|
| Simple lookup by ID | PRIMARY KEY (id) |
User profiles, products |
| Entity with timeline | PRIMARY KEY (entity_id, time) DESC |
User activity, orders |
| High-frequency time-series | PRIMARY KEY ((id, hour), time) DESC |
IoT sensors, logs |
| Low-frequency time-series | PRIMARY KEY ((id, month), time) DESC |
Listen history, bookings |
| Hierarchical data | PRIMARY KEY (id, cat, subcat) |
Articles by category |
| Related entities | PRIMARY KEY (parent_id, time, child_id) |
Post comments, replies |
| Multi-tenant | PRIMARY KEY ((tenant, user), time) DESC |
SaaS applications |
| Personalized feed | PRIMARY KEY (viewer_id, time) DESC |
Social media timelines |
🔍 Monitoring Commands
nodetool cfstats keyspace.table | grep "Partition Size"
# Find largest partitions
nodetool toppartitions keyspace.table 10
# Check table statistics
nodetool tablestats keyspace.table
# View latency histograms
nodetool tablehistograms keyspace.table
# Check compaction status
nodetool compactionstats
# View node status and load
nodetool status
nodetool ring
# Enable query tracing
cqlsh> TRACING ON;
cqlsh> SELECT * FROM table WHERE ...;
cqlsh> TRACING OFF;
# Check data cardinality
SELECT COUNT(DISTINCT partition_key) FROM table;
📈 Target Metrics
- Query latency: P99 < 10ms
- Partition size: < 100MB
- Rows/partition: < 100K
- Write latency: < 5ms
- Compaction time: Minutes not hours
- Node CPU: Evenly distributed
- Cardinality: 1M+ unique values
🚨 Alert Thresholds
- Warning: Partition > 50MB
- Critical: Partition > 100MB
- Critical: P99 latency > 100ms
- Warning: Uneven node load (>20% diff)
- Critical: Compaction > 1 hour
- Warning: Tombstone ratio > 20%
- Critical: Write timeouts
📚 Further Learning Resources
Official Documentation
- Apache Cassandra Docs
- DataStax Academy (Free courses)
- CQL Reference Guide
- Data Modeling Guidelines
Recommended Books
- "Cassandra: The Definitive Guide"
- "Mastering Apache Cassandra"
- "Learning Apache Cassandra"
- "Cassandra Data Modeling"
Video Tutorials
- DataStax YouTube Channel
- ApacheCon Presentations
- Cassandra Summit Talks
- Tech conference recordings
Practice Tools
- Docker Cassandra images
- CCM (Cassandra Cluster Manager)
- DataStax Studio
- NoSQLBench (load testing)
Community
- Apache Cassandra Slack
- Stack Overflow #cassandra tag
- Reddit r/cassandra
- Local Cassandra meetups
Production Resources
- Netflix blog (Cassandra at scale)
- Uber engineering blog
- Discord engineering blog
- DataStax case studies
Quick Reference Guide
🔑 Primary Key Syntax Cheat Sheet
-- Simple partition key only
PRIMARY KEY (user_id)
-- Partition key + single clustering column
PRIMARY KEY (user_id, timestamp)
-- Partition key + multiple clustering columns
PRIMARY KEY (user_id, timestamp, event_id)
-- Composite partition key
PRIMARY KEY ((user_id, month), timestamp)
-- Composite partition + multiple clustering
PRIMARY KEY ((hotel_id, month), check_in_date, booking_id)
-- Specify clustering order
PRIMARY KEY (user_id, timestamp)
WITH CLUSTERING ORDER BY (timestamp DESC);
✅ Do's
- ✅ Use high-cardinality partition keys (millions of values)
- ✅ Always include partition key in WHERE clause
- ✅ Use time bucketing for time-series data
- ✅ Use DESC for recent-first queries (~90% of cases)
- ✅ Monitor partition sizes (<100MB ideal)
- ✅ Design for specific query patterns
- ✅ Duplicate data across tables for different queries
- ✅ Document your design decisions
- ✅ Test at scale before production
- ✅ Use TTL for time-series data cleanup
❌ Don'ts
- ❌ Don't use low-cardinality partition keys (country, status)
- ❌ Don't query without partition key
- ❌ Don't use ALLOW FILTERING (redesign instead)
- ❌ Don't let partitions grow unbounded
- ❌ Don't rely on default ASC (be explicit)
- ❌ Don't use secondary indexes for high-cardinality
- ❌ Don't skip clustering columns in WHERE clause
- ❌ Don't range query on partition key components
- ❌ Don't ignore partition size warnings
- ❌ Don't design like SQL (no JOINs!)
⚡ Performance Quick Wins
Target Metrics
- Query latency: P99 < 10ms
- Partition size: < 100MB
- Rows per partition: < 100K
- Write latency: < 5ms
Monitoring Commands
nodetool cfstats table_name
nodetool tablehistograms
nodetool compactionstats
🎯 Decision Tree: Choosing Your Primary Key
├─ What are the most common queries?
├─ Which entities are being queried?
└─ What's the access pattern? (point lookup vs range vs scan)
Step 2: Choose partition key
├─ Is this time-series data?
│ ├─ YES → Use composite: (entity_id, time_bucket)
│ └─ NO → Continue to next question
├─ Does entity generate many rows?
│ ├─ YES → Add time bucket or sharding
│ └─ NO → Simple partition key (entity_id)
└─ Is it high-cardinality? (millions of values)
├─ YES → Good! Use it
└─ NO → Find different partition key or add more columns
Step 3: Choose clustering columns
├─ Need to query by time?
│ ├─ Recent first → timestamp DESC
│ └─ Chronological → timestamp ASC
├─ Need multiple sort levels?
│ └─ Add more clustering columns (2-3 max)
└─ Need uniqueness?
└─ Add unique ID as final clustering column
Step 4: Validate
├─ Can all queries include partition key? ✓
├─ Will partitions stay under 100MB? ✓
├─ Is data distributed evenly? ✓
└─ Do queries return data in desired order? ✓
✅ If all checks pass → Good design!
❌ If any fails → Redesign needed!
📋 Common Use Case Patterns
User Activity Feed
PRIMARY KEY (user_id, activity_time)
WITH CLUSTERING ORDER BY
(activity_time DESC)
E-commerce Orders
PRIMARY KEY (customer_id, order_date)
WITH CLUSTERING ORDER BY
(order_date DESC)
IoT Sensor Data
PRIMARY KEY
((sensor_id, hour_bucket),
reading_time)
WITH CLUSTERING ORDER BY
(reading_time DESC)
Chat Messages
PRIMARY KEY
(conversation_id, message_time)
WITH CLUSTERING ORDER BY
(message_time DESC)
User Sessions
PRIMARY KEY
(user_id, session_start)
WITH CLUSTERING ORDER BY
(session_start DESC)
Application Logs
PRIMARY KEY
((service_name, hour_bucket),
log_time)
WITH CLUSTERING ORDER BY
(log_time DESC)
You've Mastered Primary Keys!
You now understand Cassandra's most critical concept from zero to production level! You've learned:
✅ Complete fundamentals (9 core concepts)
✅ Partition keys vs clustering columns
✅ The city/district analogy
✅ 5 real production examples (Netflix, Twitter, Uber, Spotify, IoT)
✅ Composite keys and time bucketing
✅ 8 best practices + 6 anti-patterns
✅ 8 comprehensive interview questions
✅ Quick reference guide & decision tree
Total content: 30,000+ words | 8 company examples | 3 SVG diagrams | 2,915+ lines
🚀 Ready to design Cassandra schemas like a pro!
🎯 Remember The Golden Rules:
1. Query-driven design - Design schema for your queries, not your data
2. High-cardinality partition keys - Millions of unique values for even distribution
3. Time bucketing - Prevent unbounded partition growth in time-series
4. Always include partition key - In every WHERE clause, always!
5. Duplicate data freely - Storage is cheap, query performance is priceless
6. Test at scale early - Design mistakes are expensive to fix in production
7. Monitor partition sizes - Keep under 100MB, watch for hot partitions
8. DESC for recent data - 90% of use cases need newest-first ordering
Responsive Ad