Cassandra Composite Keys
Master compound partition keys and composite clustering! Learn time bucketing, multi-tenant patterns, hotspot prevention, and production-grade composite key design with real-world examples.
📖 The Story: The Apartment Building Mail System
Imagine you're designing a mail delivery system for a massive housing complex. How do you organize addresses?
🏢 System 1: One Giant Building (Simple Key)
The Monolithic Approach: Single building with 10,000 apartments
┌─────────────────────────────────┐
│ MEGABUILDING │
│ 10,000 Apartments │
│ │
│ Mail to: "Apartment 523" │
│ │
│ Single Mail Room (Overwhelmed!)│
└─────────────────────────────────┘
Delivering to Apartment 523:
1. Go to THE building
2. Find THE mail room
3. Sort through 10,000 mailboxes
4. Time: 30 minutes 🐌
Critical Problems:
- Unbounded Growth: Building keeps getting taller (no limit!)
- Single Bottleneck: One mail room for 10,000 apartments
- Slow Delivery: Finding one apartment takes forever
- Maintenance Nightmare: Entire building on one foundation
- Can't Scale: What happens at 50,000 apartments?
This is Cassandra with simple partition keys!
🏘️ System 2: Multiple Buildings (Composite Key)
The Smart Approach: 10 buildings with 1,000 apartments each
Building A: 1,000 apartments | Mail Room A
Building B: 1,000 apartments | Mail Room B
Building C: 1,000 apartments | Mail Room C
Building D: 1,000 apartments | Mail Room D
Building E: 1,000 apartments | Mail Room E
... (10 buildings total)
Mail to: "(Building-E, Apartment-523)"
↑ ↑
First key Second key
Delivering to Building-E, Apt-523:
1. Identify building: Building E
2. Go to Building E's mail room
3. Sort through 1,000 mailboxes (not 10,000!)
4. Time: 3 minutes ⚡
10x FASTER!
Massive Benefits:
- Controlled Size: Each building limited to 1,000 apartments
- Distributed Load: 10 mail rooms working in parallel
- Fast Delivery: Search only 1,000 mailboxes, not 10,000
- Easy Scaling: Just add more buildings!
- Isolation: Problem in Building A doesn't affect Building B
- Maintenance: Can repair one building at a time
This is Cassandra with composite partition keys!
🎯 This is EXACTLY How Composite Keys Work!
🏢 Simple Key
- Building = Partition
- Apartment = Row
- One huge building = One huge partition
- Mail room = Node storage
- Limited scalability
🏘️ Composite Key
- Building + Apartment = Composite partition
- Multiple buildings = Multiple partitions
- Smaller buildings = Smaller partitions
- Distributed mail rooms = Distributed nodes
- Unlimited scalability!
PRIMARY KEY (apartment_number)
Result after 1 year:
• One partition with 10,000 rows
• Partition size: 100MB (TOO BIG!)
• Queries slow down over time
• Eventually hits size limits
✅ COMPOSITE KEY (Multiple Buildings):
PRIMARY KEY ((building_id, floor_number))
↑ Both columns together = partition ↑
Result after 1 year:
• 100 partitions (10 buildings × 10 floors)
• Each partition: 100 rows
• Partition size: 1MB (PERFECT!)
• Queries stay fast forever!
• Can add more buildings anytime
100x Better Distribution!
💡 Key Insight
Apartments: Multiple buildings prevent one from getting too big
Cassandra: Multiple partition key columns prevent one partition from getting too big
Both achieve scalability through controlled subdivision!
🔗 What are Composite Keys?
Understanding the two types of composite keys in Cassandra.
Complete Definition
Composite Keys: Using multiple columns together as part of your primary key. There are TWO types:
Type 1: Compound Partition Key
↑ Compound partition ↑
Both col1 AND col2 are HASHED TOGETHER
Result: ONE token, determines ONE node
Purpose: Prevent partition from growing too large
Type 2: Composite Clustering Key
↑ Composite clustering ↑
Multiple clustering columns for SORTING
Sorted by: cluster1 (first), then cluster2
Purpose: Hierarchical sorting within partition
Type 3: BOTH Combined!
↑ Compound ↑ ↑ Composite clustering ↑
Maximum flexibility and control!
Compound partition: Prevents huge partitions
Composite clustering: Hierarchical sorting
Visual Comparison
Simple Key
- Single column partition
- No clustering columns
- Simple but limited
- Risk of huge partitions
Compound Partition
- Multiple columns partition
- Both columns hashed together
- Prevents unbounded growth
- Time bucketing pattern
Composite Clustering
- Single partition key
- Multiple clustering columns
- Hierarchical sorting
- Year → then month order
Both Combined
- Compound partition key
- Composite clustering key
- Maximum control
- Production-grade design!
🔑 Compound Partition Keys: Deep Dive
How multiple columns work together to determine data distribution.
The Problem: Unbounded Partition Growth
Real Scenario: IoT sensor data collection
❌ BAD DESIGN: Simple Partition Key
sensor_id UUID,
timestamp TIMESTAMP,
temperature DECIMAL,
humidity DECIMAL,
PRIMARY KEY (sensor_id, timestamp)
↑
Only partition key = DISASTER!
);
What Happens After 1 Year:
• 6 readings/minute
• 360 readings/hour
• 8,640 readings/day
• 3,153,600 readings/year!
Each row: ~100 bytes
Partition size: 3,153,600 × 100 = 315 MB!
Problems:
• Far exceeds 100MB recommendation
• Slow queries (scan millions of rows)
• Eventually hits Cassandra partition limit
• Performance degrades over time
• Can't delete old data efficiently
✅ GOOD DESIGN: Compound Partition Key
sensor_id UUID,
year_month TEXT, -- "2024-01", "2024-02", etc.
timestamp TIMESTAMP,
temperature DECIMAL,
humidity DECIMAL,
PRIMARY KEY ((sensor_id, year_month), timestamp)
↑ Compound partition! ↑
);
What Happens After 1 Year:
• 12 partitions (one per month)
• Each partition: 262,800 readings
• Partition size: 262,800 × 100 = 26 MB ✓
Benefits:
• Well within 100MB recommendation
• Fast queries (smaller data sets)
• Never hits partition limit
• Performance stays consistent
• Easy to TTL or archive old months!
12x SMALLER partitions!
How Compound Keys Are Hashed
Understanding the hashing mechanism:
PRIMARY KEY ((sensor_id, year_month), timestamp)
1. Insert arrives:
sensor_id = '550e8400-e29b-41d4-a716-446655440000'
year_month = '2024-01'
2. Concatenate partition key columns:
combined = sensor_id + delimiter + year_month
combined = '550e8400...|2024-01'
3. Apply Murmur3 hash:
token = Murmur3(combined)
token = -3847293847562938476
4. Map to token ring:
Node 2 owns range: [-4000... to -2000...]
Token -3847... falls in Node 2
5. Store on Node 2
CRITICAL INSIGHT:
('sensor-A', '2024-01') → Different partition than
('sensor-A', '2024-02') → Completely different partition!
Each unique combination = New partition
Why This Matters:
- Same sensor_id but different year_month = different partitions
- Each partition is independently sized and managed
- Natural data rotation by time buckets
- Easy to drop old data (just drop old partitions)
📅 Time Bucketing Pattern
The most common and powerful composite key pattern.
Hourly Bucketing
(device_id, year_month_day_hour),
timestamp
);
Best For:
- Very High Write Volume: 1000+ writes/sec
- Real-Time Metrics: Live dashboards, monitoring
- Short Retention: Days to weeks
- Example: API request logs, click streams
100 writes/sec
× 3,600 sec/hour
= 360,000 rows/partition
If row = 200 bytes:
360K × 200 = 72 MB ✓
Daily Bucketing
(user_id, date),
timestamp
);
Best For:
- Medium Write Volume: 10-1000 writes/sec
- Daily Analytics: Activity tracking, events
- Medium Retention: Months to a year
- Example: User activity, transactions, orders
1 user action/minute
× 1,440 min/day
= 1,440 rows/partition
If row = 500 bytes:
1,440 × 500 = 720 KB ✓
Monthly Bucketing
(customer_id, year_month),
order_date
);
Best For:
- Lower Write Volume: < 10 writes/sec
- Historical Data: Archival, reporting
- Long Retention: Years
- Example: Customer orders, invoices, billing
5 orders/day
× 30 days/month
= 150 rows/partition
If row = 1 KB:
150 × 1KB = 150 KB ✓
Choosing the Right Bucket Size
Use this formula to determine your bucket size:
Maximum: 100 MB per partition (hard limit)
Formula:
partition_size = writes_per_period × avg_row_size
Example: IoT Sensor
• Write rate: 10 readings/second
• Row size: 500 bytes
Hourly bucket:
10 × 3,600 × 500 = 18 MB (acceptable)
Daily bucket:
10 × 86,400 × 500 = 432 MB (TOO BIG!)
Decision: Use HOURLY bucketing ✓
Pro Tips:
- Start Smaller: Better to have more, smaller partitions than fewer, larger ones
- Consider Growth: Plan for 2-3x data growth over time
- Monitor Size: Track partition sizes in production
- Easy to Coarsen: Can always merge data later, but hard to split
🏢 Multi-Tenant Pattern
Isolating tenant data with composite partition keys.
Real Example: SaaS Application with 1,000 Tenants
❌ WITHOUT Multi-Tenant Key
user_id UUID,
tenant_id UUID,
name TEXT,
PRIMARY KEY (user_id)
);
Problems:
- Can't query "all users for tenant X" efficiently
- One giant tenant can dominate partitions
- No tenant data isolation
- Cross-tenant queries required
✅ WITH Multi-Tenant Key
tenant_id UUID,
user_id UUID,
name TEXT,
PRIMARY KEY ((tenant_id, user_id))
↑ Compound partition! ↑
);
Benefits:
- Tenant Isolation: Each tenant's data in separate partitions
- Fair Distribution: No single tenant dominates
- Fast Tenant Queries: "Get all users for tenant" = fast
- Easy Per-Tenant Operations: Export, delete, backup by tenant
-- Get specific user:
SELECT * FROM users
WHERE tenant_id = 'acme-corp'
AND user_id = 'john-doe';
-- Get all users for tenant:
SELECT * FROM users
WHERE tenant_id = 'acme-corp';
-- Fast and efficient! ⚡
🔥 Hotspot Prevention with Composite Keys
Solving the celebrity problem and uneven data distribution.
The Celebrity Problem
Scenario:
- Celebrity: 50M followers
- Normal user: 200 followers
- All celebrity followers in ONE partition
- Partition size: 500MB+ (HUGE!)
- Node hosting celebrity = overloaded
- Queries slow for celebrity data
Bucket Solution
(user_id, bucket),
follower_id
);
Solution:
- Split followers across 100 buckets
- Celebrity: 100 partitions × 500K followers
- Each partition: 5MB (PERFECT!)
- Distributed across 100 nodes
- Balanced load
- Fast queries!
bucket = follower_id % 100
Query all followers:
FOR bucket IN 0..99:
SELECT * FROM followers
WHERE user_id=?
AND bucket=?;
🌍 Production Examples from Major Companies
How industry leaders use composite keys.
🎵 Spotify: User Listening History
user_id UUID,
year_month TEXT, -- "2024-01"
played_at TIMESTAMP,
track_id UUID,
duration_ms INT,
PRIMARY KEY ((user_id, year_month), played_at)
) WITH CLUSTERING ORDER BY (played_at DESC);
Why This Design:
- Monthly Buckets: Prevents unbounded growth per user
- DESC Order: Most recent plays first (for "Recently Played")
- Easy TTL: Drop old months after 2 years
- Scale: 500M users × 12 months = 6B partitions (distributed!)
Common Queries:
SELECT * FROM listening_history
WHERE user_id = ? AND year_month = '2024-12'
LIMIT 50;
-- This Month's Top Tracks:
SELECT track_id, COUNT(*)
FROM listening_history
WHERE user_id = ? AND year_month = '2024-12'
GROUP BY track_id;
💬 Discord: Message Storage
channel_id UUID,
bucket INT, -- Time bucket (day)
message_id TIMEUUID,
author_id UUID,
content TEXT,
PRIMARY KEY ((channel_id, bucket), message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);
Why This Design:
- Daily Buckets: Popular channels = lots of messages, need small buckets
- TIMEUUID: Natural chronological ordering + uniqueness
- DESC Order: Latest messages first (scroll down to load older)
- Balance: Even popular channels stay under 100MB/partition
💳 Stripe: Transaction Records
merchant_id UUID,
year_month TEXT, -- "2024-01"
transaction_time TIMESTAMP,
transaction_id UUID,
amount DECIMAL,
currency TEXT,
PRIMARY KEY ((merchant_id, year_month), transaction_time)
);
Why This Design:
- Monthly Buckets: Aligns with billing cycles
- Per-Merchant: Easy to generate monthly statements
- Time Ordered: Chronological transaction history
- Compliance: Easy to archive old months for regulations
📋 Composite Clustering Keys: Hierarchical Sorting
Multiple clustering columns create powerful hierarchical sort orders.
The Filing Cabinet Analogy
Imagine organizing files in a cabinet:
📁 Single Clustering Column
Filing Order:
├── 2024-01-15
├── 2024-01-20
├── 2024-02-01
├── 2024-03-05
└── 2024-12-30
Just chronological - simple but limited!
📁 Composite Clustering Columns
Filing Order (Hierarchical!):
├── 2023/
│ ├── 12/
│ │ ├── Day 30
│ │ └── Day 31
├── 2024/
│ ├── 01/
│ │ ├── Day 05
│ │ ├── Day 15
│ │ └── Day 20
│ ├── 02/
│ │ └── Day 01
│ └── 03/
│ └── Day 05
Hierarchical - powerful queries!
Why This is Better:
- Can query by year only: "All 2024 documents"
- Can query by year + month: "January 2024 documents"
- Can query by year + month + day range: "Jan 15-20"
- Natural grouping for analytics
How Composite Clustering Works Internally
The clustering columns create a composite sort key:
Internal Storage:
Row 1: user_id='alice', year=2023, month=12, day=30
→ Composite Key: "2023:12:30"
Row 2: user_id='alice', year=2024, month=01, day=15
→ Composite Key: "2024:01:15"
Row 3: user_id='alice', year=2024, month=01, day=20
→ Composite Key: "2024:01:20"
Physical Sort (Byte Comparison):
"2023:12:30" < "2024:01:15" < "2024:01:20"
This is why you MUST query left-to-right!
The composite key is ONE sorted value.
The Left-to-Right Rule:
- Must specify clustering columns in ORDER
- Can't skip a column in the middle
- Range queries only on the LAST specified column
- This is due to composite key byte ordering
Valid Queries
-- Query by year only:
WHERE user_id=? AND year=2024
-- Query year + month:
WHERE user_id=? AND year=2024
AND month=1
-- Query year + month + day:
WHERE user_id=? AND year=2024
AND month=1 AND day=15
-- Range on LAST specified:
WHERE user_id=? AND year=2024
AND month=1 AND day>10 AND day<20
Invalid Queries
-- Skipped year:
WHERE user_id=? AND month=1
❌ Missing year!
-- Skipped month:
WHERE user_id=? AND year=2024
AND day=15
❌ Missing month!
-- Range on non-last:
WHERE user_id=? AND year>2023
AND month=1
❌ Range must be on LAST column!
🔍 Query Patterns with Composite Keys
Master efficient querying strategies for composite keys.
Pattern 1: Time-Range Queries
sensor_id UUID,
year_month TEXT,
timestamp TIMESTAMP,
temperature DECIMAL,
PRIMARY KEY ((sensor_id, year_month), timestamp)
);
Efficient Time-Range Queries:
SELECT * FROM sensor_data
WHERE sensor_id = ?
AND year_month = '2024-01'
AND timestamp > '2024-01-15 00:00:00'
AND timestamp < '2024-01-20 23:59:59';
-- ✓ Single partition scan
-- Multi-month range (requires multiple queries):
SELECT * FROM sensor_data
WHERE sensor_id = ?
AND year_month IN ('2024-01', '2024-02', '2024-03')
AND timestamp > '2024-01-15 00:00:00';
-- ✓ 3 partition scans (still efficient)
Performance Characteristics:
- Single Partition: O(log n) + O(k) - very fast
- Multiple Partitions: O(p × (log n + k)) where p = partitions
- Key Insight: Each partition scanned independently and in parallel
Pattern 2: Multi-Tenant Queries
tenant_id UUID,
user_id UUID,
event_time TIMESTAMP,
event_type TEXT,
PRIMARY KEY ((tenant_id, user_id), event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);
Efficient Multi-Tenant Queries:
SELECT * FROM tenant_events
WHERE tenant_id = ?
AND user_id = ?
LIMIT 50;
-- ✓ Single partition, DESC order, super fast!
-- All users' recent events (requires app-level aggregation):
FOR EACH user IN tenant_users:
SELECT * FROM tenant_events
WHERE tenant_id = ?
AND user_id = user.id
LIMIT 10;
-- ✓ Parallel queries, then merge in app
Why This Pattern Works:
- Each (tenant, user) combination = separate partition
- No tenant can dominate a single partition
- Queries stay fast even with millions of users
- Easy to implement per-user rate limiting
Pattern 3: Hierarchical Data Queries
product_id UUID,
region TEXT,
year INT,
month INT,
day INT,
sales DECIMAL,
PRIMARY KEY ((product_id, region), year, month, day)
);
Hierarchical Query Examples:
SELECT SUM(sales) FROM product_metrics
WHERE product_id = ?
AND region = 'US-WEST'
AND year = 2024;
-- Monthly total:
SELECT SUM(sales) FROM product_metrics
WHERE product_id = ?
AND region = 'US-WEST'
AND year = 2024
AND month = 1;
-- Daily range:
SELECT * FROM product_metrics
WHERE product_id = ?
AND region = 'US-WEST'
AND year = 2024
AND month = 1
AND day >= 15 AND day <= 20;
Aggregation Strategy:
- Year-level aggregation: Fast (scans all days in year)
- Month-level: Faster (scans fewer days)
- Day-level: Fastest (pinpoint query)
- Consider pre-aggregated tables for common queries
Query Performance Tips
DO:
- Use LIMIT: Always limit result sets to prevent huge scans
- Specify All Partition Columns: Essential for routing to correct node
- Leverage Clustering Order: Use DESC for "latest first" queries
- Batch Related Queries: Fetch multiple partitions in parallel
DON'T:
- Use ALLOW FILTERING: Scans entire table - extremely slow!
- Skip Partition Columns: Results in full cluster scan
- Query Without Limits: Can return millions of rows
- Use Inequality on Partition Key: Not supported
🖥️ Interactive Composite Key Design Tool
Calculate optimal composite key structure based on your requirements.
⚠️ Composite Key Anti-Patterns
Common mistakes to avoid in production.
❌ Anti-Pattern 1: Using High Cardinality Second Column
PRIMARY KEY ((tenant_id, user_id))
Problem:
• Each (tenant, user) = separate partition
• 1M users = 1M partitions
• Too much fragmentation!
• Coordinator overhead
• Can't query "all users in tenant"
✅ BETTER: Use user_id as clustering
PRIMARY KEY (tenant_id, user_id)
Benefits:
• All tenant data in one partition
• Can query "all users"
• Better for analytics
• Less coordinator overhead
When High Cardinality IS Appropriate:
- Time buckets (year-month) - prevents unbounded growth
- Region/shard IDs - distribute load
- When isolation is more important than aggregation
❌ Anti-Pattern 2: Too Many Partition Columns
PRIMARY KEY ((tenant_id, region, year, month))
Problems:
• Must specify ALL 4 in every query
• Extremely fragmented data
• Hard to aggregate across dimensions
• Complex application logic
✅ BETTER: 2-3 columns max
PRIMARY KEY ((tenant_id, year_month), region)
↑clustering
Benefits:
• Only 2 required in WHERE clause
• Can query all regions for month
• Simpler queries
• Better flexibility
Rule of Thumb: Keep compound partition keys to 2-3 columns maximum!
❌ Anti-Pattern 3: Low Cardinality First Column
PRIMARY KEY ((is_active, user_id))
↑ Only 2 values!
Problems:
• Only 2 partitions (true/false)
• MASSIVE partitions
• Hotspots (all activity on 2 nodes)
• Defeats composite key purpose!
✅ BETTER: High cardinality first
PRIMARY KEY (user_id, is_active)
↑clustering
Benefits:
• Each user = separate partition
• Even distribution
• Can filter by is_active WITHIN user
• Scalable!
Guideline: First partition column should have high cardinality (100K+ unique values)!
❌ Anti-Pattern 4: Buckets Too Large
PRIMARY KEY ((sensor_id, year))
With 10 writes/sec:
• 10 × 31,536,000 sec/year = 315M rows/partition
• Partition size: 31.5 GB!
• WAY TOO BIG!
• Queries become slower and slower
✅ BETTER: Right-sized buckets
PRIMARY KEY ((sensor_id, year_month_day_hour))
With 10 writes/sec:
• 10 × 3,600 sec/hour = 36K rows/partition
• Partition size: 3.6 MB ✓
• Perfect size!
• Consistent performance
Always Calculate: Use the formula writes_per_period × row_size to verify!
❌ Anti-Pattern 5: Mutable Partition Columns
PRIMARY KEY ((user_id, subscription_tier))
↑ Changes!
Problems:
• User upgrades: "free" → "premium"
• Can't UPDATE partition key
• Must DELETE + INSERT (expensive!)
• Data moves to different partition
• Can cause temporary duplicates
✅ BETTER: Immutable partition columns
PRIMARY KEY (user_id, subscription_tier)
↑clustering only
Benefits:
• Can UPDATE subscription_tier
• Row stays in same partition
• Simple update operation
• No data movement
Golden Rule: Partition key columns must be immutable or change very rarely!
✅ Production Best Practices
Battle-tested guidelines for composite key design.
ALWAYS Do This
- Calculate Partition Size: Use formula
writes × period × row_sizebefore deploying - Start with Smaller Buckets: Better 100 small partitions than 10 large ones
- Use Time Buckets for Time-Series: Natural data lifecycle management
- Test with Production Load: Simulate real write rates in staging
- Monitor Partition Sizes: Set up alerts for > 50MB partitions
- Document Your Decisions: Explain why you chose specific bucket sizes
- Plan for Growth: Design for 3-5x data volume increase
- Use Immutable Columns: Partition keys should never change
NEVER Do This
- Skip Size Calculations: Guessing leads to production issues!
- Use Year-Only Buckets: For high-volume data - partitions explode
- Put Low Cardinality First: Boolean/enum first = hotspots
- Exceed 3 Partition Columns: Makes queries complex and brittle
- Use Mutable Data in Keys: Updates require delete + insert
- Forget About TTL: Old data piles up without cleanup
- Deploy Without Load Testing: Production is not the place to discover issues
- Ignore Partition Warnings: Cassandra logs large partition warnings for a reason!
Pre-Deployment Checklist
Before going to production, verify:
🎯 Choosing Between Patterns
Use Compound Partition When:
- Preventing unbounded partition growth
- Time-series data with long retention
- Multi-tenant isolation needed
- High write volume per entity
- Natural bucketing exists (region, category)
Use Composite Clustering When:
- Need hierarchical sorting
- Multiple sort dimensions
- Year → Month → Day queries
- Partition size already controlled
- Need flexible query granularity
Pro Tip: Often you'll use BOTH together!
↑ Compound partition ↑ ↑ Composite clustering ↑
💼 Complete Interview Preparation (15 Questions)
Master composite keys for technical interviews!
Answer:
When querying composite clustering keys, you must specify columns in order from left to right, and range queries can only be on the last specified column.
✅ Valid:
WHERE user_id=? AND year=2024
WHERE user_id=? AND year=2024 AND month=1
WHERE user_id=? AND year=2024 AND month=1 AND day>15
❌ Invalid:
WHERE user_id=? AND month=1 (missing year!)
WHERE user_id=? AND day=15 (missing year and month!)
WHERE user_id=? AND year>2023 AND month=1 (range not on last!)
Why? Clustering columns form a composite sort key. Cassandra can only efficiently filter following the sort order!
Answer:
No! Partition key columns (including all columns in a compound key) are immutable and cannot be updated.
Why Not?
- Partition key determines which node stores the data (via hashing)
- Changing it would require moving data to a different partition/node
- This would be extremely expensive
- Would break the physical storage structure
Workaround: DELETE old row, then INSERT new row
DELETE FROM table WHERE user_id=? AND year=2023;
INSERT INTO table (...) VALUES (?, 2024, ...);
Best Practice: Only use immutable data in partition keys!
Answer:
Use the IN clause to query multiple partitions, or make multiple queries in parallel and merge results in your application.
SELECT * FROM sensor_data
WHERE sensor_id = ?
AND year_month IN ('2024-01', '2024-02', '2024-03');
-- Alternative: Parallel queries in app
async function getMultiMonthData(sensorId, months) {
const promises = months.map(month =>
query("SELECT * WHERE sensor_id=? AND year_month=?",
[sensorId, month])
);
return Promise.all(promises);
}
Performance: IN clause with 3-10 values is efficient. Beyond that, parallel queries may be better.
Answer:
- Ideal: 1-10 MB per partition
- Acceptable: 10-50 MB per partition
- Warning Zone: 50-100 MB per partition
- Maximum: 100 MB (Cassandra warns beyond this)
- Hard Limit: 2 GB (Cassandra may refuse)
Why It Matters:
- Large partitions slow down queries (must scan more data)
- Compaction becomes more expensive
- Repair operations take longer
- Memory pressure on coordinators
- Risk of timeouts
Monitoring: Track max_partition_size and avg_partition_size metrics!
Answer:
Time-bucketed composite keys enable efficient deletion of old data by dropping entire partitions instead of individual rows.
-- Delete all January 2023 data:
DELETE FROM sensor_data
WHERE sensor_id = ? AND year_month = '2023-01';
-- This drops ENTIRE partition efficiently!
-- No row-by-row deletion needed
Benefits:
- Fast Deletion: Drop partition vs delete millions of rows
- No Tombstones: Partition deletion is cleaner
- Predictable: Know exactly which buckets to delete
- Automatable: Scheduled cleanup jobs
Pro Pattern: Combine with Time Window Compaction Strategy (TWCS) for automatic partition expiry!
Answer:
Use buckets when you have high-cardinality relationships that could create unbounded partitions. Classic example: social graph (followers).
PRIMARY KEY ((user_id, bucket), follower_id)
Bucket Assignment:
bucket = follower_id % 100
Result:
• 100 partitions × 500K followers
• Each partition: manageable size
• Distributed across many nodes
Other Use Cases:
- Message Threads: Popular threads split across buckets
- Product Reviews: Popular products split
- Event Attendees: Large events bucketed
- Log Files: High-volume services bucketed
Trade-off: Must query all buckets to get complete data, but each query is fast!
Answer:
Compound keys have minimal write overhead - just additional hashing computation. The benefits far outweigh the small cost.
Performance Impact:
- Hash Computation: Minimal (~microseconds to hash 2-3 columns)
- Write Path: Identical to simple keys after hashing
- Overall Impact: < 1% overhead
Performance GAINS:
- Better Distribution: Writes spread across more nodes
- Smaller Memtables: Per-partition memtables stay small
- Faster Compaction: Smaller partitions compact faster
- Reduced Hotspots: More even node utilization
Bottom Line: Compound keys IMPROVE write performance in practice by preventing large partition issues!
Answer:
Yes, but with caution! Secondary indexes on composite key columns work, but you still need to specify the full partition key in queries.
user_id UUID,
year_month TEXT,
event_type TEXT,
timestamp TIMESTAMP,
PRIMARY KEY ((user_id, year_month), timestamp)
);
CREATE INDEX ON events(event_type);
-- This query STILL requires partition key:
SELECT * FROM events
WHERE user_id = ? AND year_month = ?
AND event_type = 'login'; -- Index helps here
-- This is VERY expensive (full cluster scan):
SELECT * FROM events
WHERE event_type = 'login'; -- ❌ No partition key!
Best Practice: Secondary indexes should filter WITHIN partitions, not across the entire cluster!
Answer:
Migration requires creating a new table and copying data. There's no in-place schema change for partition keys.
CREATE TABLE events_v2 (
user_id UUID,
year_month TEXT, -- New!
event_time TIMESTAMP,
event_data TEXT,
PRIMARY KEY ((user_id, year_month), event_time)
);
-- Step 2: Dual-write to both tables
INSERT INTO events (user_id, event_time, event_data)
VALUES (?, ?, ?);
INSERT INTO events_v2 (user_id, year_month, event_time, event_data)
VALUES (?, calculateYearMonth(?), ?, ?);
-- Step 3: Backfill old data
spark-submit backfill_events.py -- Batch job
-- Step 4: Switch reads to new table
-- Step 5: Stop writing to old table
-- Step 6: Drop old table
Migration Timeline: Typically 1-4 weeks depending on data volume
Answer:
This is about data distribution vs data organization within partitions.
Multiple Partition Columns (Compound):
Effect:
• Each (user_id, year) = DIFFERENT partition
• Data distributed across many partitions
• Prevents unbounded partition growth
• Must specify BOTH in WHERE clause
Multiple Clustering Columns (Composite):
Effect:
• All user data in ONE partition
• Sorted by year, then month
• Enables hierarchical queries
• Can query by year only OR year + month
When to Use Each:
- Compound Partition: Partition getting too large
- Composite Clustering: Need flexible query granularity
- Both Together: Large data + hierarchical queries
🎓 Chapter Summary: Composite Key Mastery
You now master Cassandra's most powerful partition control mechanism!
Key Concepts Mastered:
- Compound Partition Keys: Multiple columns hashed together for distribution
- Time Bucketing: Prevent unbounded partition growth
- Multi-Tenant Patterns: Isolate tenant data effectively
- Hotspot Prevention: Balance load across nodes
- Production Patterns: Real examples from Spotify, Discord, Stripe
The Apartment Analogy Recap:
Remember: Single building (simple key) vs multiple buildings (composite key). Smaller buildings = smaller partitions = better performance!
Production Checklist:
- ✅ Calculate expected partition sizes
- ✅ Choose appropriate time bucket (hourly/daily/monthly)
- ✅ Use compound keys for time-series data
- ✅ Implement multi-tenant isolation where needed
- ✅ Monitor partition sizes in production
🚀 You can now design scalable Cassandra schemas!
Responsive Ad