Lesson 04 · The industrialist

Wide-column: design the table around the query

Cassandra, ScyllaDB, HBase, Bigtable: the wide-column family looks deceptively like Lesson 1: tables, rows, columns, even a SQL-ish language (CQL). The resemblance is a costume. Underneath is a different physics, built for a different job: sustained massive write throughput across many machines, with reads that are fast only along pre-planned paths. Two ideas power everything: the primary key is the physical data layout, and the storage engine is an LSM tree instead of a B-tree. Take them in order.

Idea 1: the key is the layout

A wide-column primary key has two parts. The partition key is hashed to pick which node (and which contiguous chunk on disk) owns the row: the consistent-hashing ring from the distributed systems course, now load-bearing. The clustering key sorts rows within the partition. Consequence: rows sharing a partition key live together, pre-sorted. A query that supplies the partition key is one node, one seek, one sequential read. A query that doesn't is... try it:

Query a sensor table with and without the map

Table: readings (sensor_id PARTITION KEY, ts CLUSTERING KEY, value): billions of rows across 6 nodes. Run each query.

:
nodes touched
:
read pattern
:
verdict
:

This is why wide-column modeling inverts Lesson 1's method. Relational: model the data's true structure, normalize, then write any query. Wide-column: list the queries first, then design one table per query, with the partition key chosen so each query hits one partition. Need readings by sensor and recent readings across all sensors? Two tables, same data written twice (the application, or a materialized view, maintains both). Denormalization isn't a lapse here: it's the methodology. Disk is cheap; cross-node scatter-gather is not.

Idea 2: the LSM tree: never rewrite, always append

B-trees update pages in place: random I/O, read-modify-write, per-key locks. LSM (log-structured merge) trees refuse: writes go to an in-memory sorted buffer (memtable) plus a WAL for crash safety; when full, the memtable flushes to disk as an immutable sorted file (SSTable); background compaction merges SSTables, discarding overwritten values and deletions. Every disk write is sequential and batched: the shape disks (and SSDs) love. Operate it:

Drive the write path

Write keys, flush the memtable, write more, then read a key and see why reads must check multiple places. Finish with a compaction.

memtable (RAM, sorted)
·
SSTable-1 (disk, immutable)
·
SSTable-2 (disk, immutable)
·

What you saw on the read: the newest value of a key can be in the memtable or any SSTable, so reads check newest-to-oldest until they find it: that's read amplification, the tax LSM pays for its write speed (bloom filters per SSTable make most "not here" checks nearly free). Compaction is the cleanup crew: merging files, dropping shadowed values and tombstones: costing background I/O (write amplification). The whole design is one deliberate trade: writes optimal, reads managed, cleanup deferred. Lesson 1's B-tree is the mirror image. Neither is smarter; they're aimed at different workloads: and now you can name the workload each is aimed at.

The lineage and the fine print: Google's Bigtable (2006) begat HBase (Hadoop's copy) and inspired Cassandra (Bigtable's model + Dynamo's ring: both course-mates of yours now). "Wide column" refers to partitions holding many, sparse, dynamically-named columns: e.g., one partition per user, one column per event. Fine print worth knowing: deletes are writes (tombstones) and mass-delete workloads can poison read performance until compaction catches up; counters and secondary indexes exist but fight the grain; and multi-partition transactions mostly don't exist: the partition is the consistency boundary, an even harder version of Lesson 3's document-boundary rule.

Check yourself

1. A Cassandra table for a chat app: messages (channel_id, sent_at, author, text). Which primary key serves "load the latest 50 messages in a channel"?

The query names the design: filter by channel → channel_id is the partition key; order by time within it → sent_at clustering, DESC so the newest 50 are the partition's first rows. Option A is the moving-hotspot disaster from the distributed systems course's partitioning lesson (all writes land on "now"), plus it scatters every channel's history across the cluster. In this family, the query IS the schema.

2. Why are LSM-based stores dramatically faster at sustained writes than B-tree stores?

B-tree write: find the page (read I/O), modify, write back: random I/O per key, page splits under load. LSM write: memtable insert + WAL append, both O(1)-ish and sequential; the sorting work is batched into flushes and compaction where it's amortized across thousands of keys. The bill arrives as read amplification and compaction I/O: deferred, not waived.

3. A team migrates a relational schema to Cassandra "as-is": normalized tables, then application-side joins. What have they actually built?

A classic migration failure. Wide-column's speed comes from query-first, denormalized, partition-local design; normalized data in Cassandra means every page load does scatter-gather that the relational engine would at least have planned and optimized. The correct migration re-derives tables from the query list: and if the query list is "we don't know yet, queries change weekly," that's the signal that the workload wanted Lesson 1 all along.