Practice Data Modeling
Design Cassandra schemas for real-world applications!
- Q1: Get user profile by username
- Q2: Get all tweets by a specific user (most recent first)
- Q3: Get user's timeline (tweets from people they follow)
- Q4: Get list of users someone is following
- Q5: Get follower count for a user
- Tweets should be time-ordered (newest first)
- Timeline queries must be fast (< 50ms)
- Users can have millions of followers
- Consider denormalization for performance
📋 Table 1: User Profiles
Why: Username is the partition key for fast user lookups. Counters track followers/following efficiently.
📋 Table 2: User Tweets
Why: Partition by username, cluster by timeuuid DESC for reverse chronological order. Q2 ✓
📋 Table 3: User Timeline (Denormalized)
Why: Pre-computed timeline! When someone tweets, fan-out to all followers' timelines. Fast reads, more writes. Q3 ✓
📋 Table 4: Following Relationships
Why: Who is user X following? Partition by follower. Q4 ✓
- Denormalization: Timeline table duplicates tweet data for fast reads
- Time-ordering: Use timeuuid for natural time-based sorting
- Write amplification: Fan-out writes to followers' timelines
- Counters: Track followers/following without read-modify-write
- Query-first: Each table designed for specific query pattern
- Q1: Get product details by product_id
- Q2: Get all products in a category
- Q3: Get user's order history (most recent first)
- Q4: Get order details including all items
- Q5: Get user's current shopping cart
- Products can belong to multiple categories
- Orders should include snapshot of product data
- Shopping cart must support concurrent updates
- Fast product browsing is critical
📋 Table 1: Products
Why: Simple lookup by product_id. Use collections for categories and images. Q1 ✓
📋 Table 2: Products by Category
Why: Browse products by category. Denormalized for fast category pages. Q2 ✓
📋 Table 3: Orders by User
Why: Order history per user, newest first. Q3 ✓
📋 Table 4: Order Details
Why: Complete order with items as UDT. Snapshot product data at purchase time. Q4 ✓
📋 Table 5: Shopping Cart
Why: One row per cart item. Easy to add/update/remove. Q5 ✓
- Denormalization: products_by_category duplicates for fast browsing
- Collections: Use set for categories, list for images
- UDTs: Order items as frozen UDT for clean structure
- Snapshot data: Store product info in order at purchase time
- Multiple tables: Different query patterns = different tables
- Q1: Get latest reading from a sensor
- Q2: Get sensor readings for last 24 hours
- Q3: Get all readings for specific date range
- Q4: Get hourly averages for a sensor
- Q5: Get all sensors in a location
- 1 reading per minute per sensor = 1.4M reads/day per sensor
- Keep partitions under 100MB
- Old data should auto-expire (TTL)
- Efficient for time-series queries
📋 Table 1: Sensor Readings (Time Bucketed)
Why: Time bucketing keeps partitions bounded. TWCS for time-series. TTL auto-expires. Q1-3 ✓
📋 Table 2: Sensor Metadata
Why: Static sensor information. Fast lookups by sensor_id.
📋 Table 3: Hourly Aggregates
Why: Pre-computed hourly stats for fast analytics. Longer retention than raw data. Q4 ✓
📋 Table 4: Sensors by Location
Why: Find all sensors in a location. Denormalized for performance. Q5 ✓
- Time bucketing: Partition by (sensor_id, day) to limit partition size
- TWCS compaction: Perfect for time-series workloads
- TTL: Auto-expire raw data after 30 days, aggregates after 1 year
- Aggregation: Pre-compute hourly stats for fast queries
- DESC ordering: Latest readings first (most common query)
- Q1: Get player profile and stats
- Q2: Get top 100 players globally
- Q3: Get player's ranking among friends
- Q4: Get player's match history
- Q5: Update player score after each game
- Leaderboard updates must be near real-time
- Support multiple leaderboard types (daily, weekly, all-time)
- Millions of concurrent players
- Fast writes for score updates
📋 Table 1: Player Profiles
Why: Quick player lookup. Store aggregate stats. Q1 ✓
📋 Table 2: Global Leaderboard
Why: Partition by board type, cluster by score DESC. Top players = LIMIT 100. Q2 ✓
📋 Table 3: Friend Rankings
Why: Each player has their own friend leaderboard. Updated when friends' scores change. Q3 ✓
📋 Table 4: Match History
Why: Recent matches first. Partition per player. Q4 ✓
📋 Score Update Pattern
Why: Atomic updates across tables. Delete old leaderboard entry, insert new. Q5 ✓
- Score clustering: Cluster by score DESC for natural leaderboard ordering
- Multiple leaderboards: Partition by board_type (daily/weekly/alltime)
- Delete-insert pattern: Update leaderboard by deleting old, inserting new
- Denormalization: Store username in leaderboard for display
- Batching: Atomic updates across player and leaderboard tables
- Q1: Get conversation between two users
- Q2: Get all conversations for a user
- Q3: Get unread message count
- Q4: Get messages in a group chat
- Q5: Update message delivery status
- Messages must be in chronological order
- Support message editing and deletion
- Real-time delivery updates
- Efficient pagination for long conversations
📋 Table 1: Messages (1-on-1)
Why: Conversation = partition, messages time-ordered. Both users query same conversation_id. Q1 ✓
📋 Table 2: User Conversations
Why: Conversation list per user, ordered by most recent activity. Shows preview. Q2 ✓
📋 Table 3: Unread Messages
Why: Track unread messages per user. Delete when read. Q3 ✓
📋 Table 4: Group Messages
Why: Group chat = single partition. All members read from same partition. Q4 ✓
📋 Delivery Status Update
Why: Atomic status updates across all relevant tables. Q5 ✓
- Deterministic conversation_id: Both users share same partition
- Denormalization: Conversation list duplicates last message for preview
- Unread tracking: Separate table for fast unread queries
- Time ordering: timeuuid for natural message ordering
- Status updates: Batch updates for consistency
Responsive Ad