Stage 3 · Build
NoSQL & Distributed Databases
Wide-Column Stores (Cassandra)
Partition keys, clustering keys, tombstones, and compaction strategies.
Wide-Column Data Model
Cassandra organizes data into keyspaces, tables, and rows — but with a fundamentally different storage model than relational databases. Data is distributed across nodes by partition key, and within each partition, rows are sorted by clustering key.
-- Keyspace (like a database in PostgreSQL)
CREATE KEYSPACE IF NOT EXISTS app_data
WITH replication = {
'class': 'NetworkTopologyStrategy',
'us-east': 3,
'eu-west': 3
};
-- Table with partition and clustering keys
CREATE TABLE app_data.user_events (
user_id UUID,
event_date DATE,
event_time TIMESTAMP,
event_type TEXT,
payload MAP<TEXT, TEXT>,
PRIMARY KEY ((user_id), event_date, event_time)
) WITH CLUSTERING ORDER BY (event_date DESC, event_time DESC);The PRIMARY KEY has two parts: ((user_id)) is the partition key (determines which node stores the data), and event_date, event_time are clustering keys (determine sort order within the partition).
All data with the same partition key lives on the same node. The partition key is the most important design decision in Cassandra — it determines data distribution, query patterns, and scalability.
Partition and Clustering Keys
-- Good: partition key distributes data evenly
PRIMARY KEY ((user_id), event_date)
-- Each user's events are on one node, users spread across nodes
-- Good: composite partition key for high-cardinality
PRIMARY KEY ((user_id, event_type), event_date)
-- Events partitioned by user+type, queried by date range
-- Bad: low cardinality partition key
PRIMARY KEY ((region), event_date)
-- Only 3-5 regions means uneven distribution (hot nodes)
-- Bad: too many partitions queried at once
SELECT * FROM user_events WHERE user_id IN (uuid1, uuid2, ...uuid1000);
-- Each IN value is a different partition on potentially different nodes
-- Range queries on clustering key (efficient within partition)
SELECT * FROM user_events
WHERE user_id = ? AND event_date >= '2024-01-01' AND event_date <= '2024-06-30';The partition key determines data distribution. Choose a partition key with high cardinality (many unique values) to distribute data evenly. Clustering keys determine sort order and enable efficient range queries within a partition.
CQL Query Language
-- Insert (upsert semantics — same PK overwrites)
INSERT INTO user_events (user_id, event_date, event_time, event_type)
VALUES (uuid(), '2024-06-15', toTimestamp(now()), 'login');
-- Select (must include partition key)
SELECT * FROM user_events WHERE user_id = ?;
SELECT * FROM user_events WHERE user_id = ? AND event_date = '2024-06-15';
-- Select with LIMIT
SELECT * FROM user_events WHERE user_id = ? LIMIT 10;
-- Update (same as upsert)
UPDATE user_events SET payload = {'key': 'value'}
WHERE user_id = ? AND event_date = '2024-06-15' AND event_time = ?;
-- Delete (creates tombstone)
DELETE FROM user_events WHERE user_id = ? AND event_date = '2024-06-15';
-- Batch (atomic across partitions — use sparingly)
BEGIN BATCH
INSERT INTO user_events (user_id, event_date, event_time, event_type)
VALUES (?, '2024-06-15', now(), 'purchase');
UPDATE user_inventory SET quantity = quantity - 1 WHERE user_id = ?;
APPLY BATCH;CQL looks like SQL but has different semantics. There are no JOINs, no aggregations across partitions, and writes are upserts. The WHERE clause must include the partition key for efficient queries.
Tombstones and Deletion
Cassandra does not immediately delete data. Instead, it creates a tombstone marker. During compaction, tombstoned data is physically removed. Until compaction, tombstones are included in read results, adding overhead.
-- Delete creates a tombstone
DELETE FROM user_events WHERE user_id = ? AND event_date = '2024-01-01';
-- Tombstones affect read performance
-- If a query scans many tombstones, it becomes slow
-- tombstone_warn_threshold (default 1000)
-- tombstone_failure_threshold (default 100000)
-- Check tombstone status per table
nodetool tablestats app_data.user_events
-- TTL automatically creates future tombstones
INSERT INTO user_events (user_id, event_date, event_time, event_type)
VALUES (?, '2024-06-15', now(), 'session')
USING TTL 86400; -- auto-delete after 24 hours
-- Avoid large range deletes
DELETE FROM user_events WHERE user_id = ? AND event_date >= '2024-01-01';
-- This creates millions of tombstones — very slow!Tombstones are the biggest performance陷阱 in Cassandra. Large range deletes create millions of tombstones that slow reads for days until compaction runs. Use TTLs instead of manual deletes for time-series data.
If a query scans more than 100,000 tombstones (default threshold), Cassandra throws a TombstoneOverwhelmingException. This happens when querying ranges that contain mostly deleted data. Monitor tombstone counts with nodetool.
Compaction Strategies
| Strategy | Best For | Tradeoff |
|---|---|---|
| SizeTiered (STCS) | Write-heavy, time-series | Read amplification |
| Leveled (LCS) | Read-heavy, updates | Write amplification |
| TimeWindow (TWCS) | Time-series with TTL | Time-based grouping |
-- Size-tiered (default) — good for write-heavy
ALTER TABLE user_events WITH compaction = {
'class': 'SizeTieredCompactionStrategy',
'min_threshold': 4,
'max_threshold': 32
};
-- Leveled — good for read-heavy
ALTER TABLE user_events WITH compaction = {
'class': 'LeveledCompactionStrategy',
'sstable_size_in_mb': 160
};
-- TimeWindow — best for time-series with TTL
ALTER TABLE user_events WITH compaction = {
'class': 'TimeWindowCompactionStrategy',
'compaction_window_size': 1,
'compaction_window_unit': 'DAYS'
};
-- Check compaction status
nodetool compactionstatsTWCS groups SSTables by time window. Old windows are compacted once and never rewritten. This is ideal for time-series data that is inserted once and deleted by TTL — no write amplification for old data.
Data Modeling for Cassandra
Cassandra data modeling starts with queries, not entities. You design tables to support specific query patterns, not to normalize data. This is the opposite of relational modeling.
- List all queries your application needs
- Design a table for each query pattern
- Duplicate data across tables as needed
- Use materialized views or lightweight transactions for secondary queries
- Test with production data volume and query patterns
In relational databases, you normalize entities and derive queries. In Cassandra, you start with queries and denormalize to support them. This means more tables and more data duplication, but each query is fast and efficient.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.