Pakkit.net
← Back to blog

Systems Thinking

How a Wide-Column Store Wants You to Model Data

Coming from SQL, a wide-column database like Cassandra feels backwards — you design tables around the questions you'll ask, not the shape of the data — and fighting that instinct instead of embracing it is how people conclude these databases "don't scale" when really they were modeled like a relational one.

  • Systems Thinking
  • Databases
  • Data Modeling
  • Architecture

The first time you model data in a wide-column store — Cassandra and its relatives — everything you learned from relational databases works against you. There are no joins to lean on, “just add an index” isn’t the answer, and normalizing your schema, the thing you were trained to do, is often the wrong move. It feels backwards until the mental model clicks: a relational database is designed around the data and its relationships, and a wide-column store is designed around the queries you’re going to run. Once you model for the questions instead of the entities, the whole thing stops fighting you.

The partition key is how you find the data

The center of everything is the partition key. It decides which node holds a row, and it’s how you look data up. In a wide-column store, an efficient read is one that knows its partition key — “get the record for this key.” Reads that don’t name a partition key (find everything where some other column matches) are the expensive, cluster-scanning queries the database quietly hates, because it may have to ask every node.

So the design question isn’t “what does this data look like?” — it’s “how am I going to look this up?” Whatever you’ll query by is what the partition key has to be. If you look devices up by their hardware address, the hardware address is your partition key. If you also need to look the same data up a second way, you don’t add an index — you often store the data a second time, keyed the other way. Which brings us to the part that horrifies relational instincts.

Denormalize on purpose: one table per query

In relational modeling, storing the same data twice is a sin — you normalize, then join. In a wide-column store, joins don’t exist, so you flip it: you denormalize, and you build a table per query pattern. Need the same information retrievable three different ways? That can be three tables, each keyed for one of the questions, each holding a copy. It looks wasteful. It is, in storage — and storage is cheap while cluster-wide scans are not. You’re trading disk (plentiful) for query efficiency (precious), on purpose.

Relational asks “where should this fact live so it lives once?” Wide-column asks “which queries need this fact, and can each of them reach it by its partition key?” — even if that means the fact lives in several places.

The practical workflow inverts too: you start from your access patterns, list the exact queries the application will make, and design a table for each. The queries come first, and the schema falls out of them. Design the tables before you know the queries and you’ll build something elegant that can’t answer the questions you actually have.

Writes are cheap and reads tolerate mess

Two more properties reshape how you model. First, these stores are built for fast writes — often the model is effectively append-only, where an “update” is just a new write of the newer value, and the freshest one wins on read. That’s why writes fly, and it’s why the cost shows up later as compaction, the bill for all those cheap writes. Second, because data is replicated and updates append, a read can encounter slightly stale or duplicate data, and well-designed access patterns tolerate that — retrying, or treating the newest value as truth — rather than assuming a single perfectly-consistent row. Designing as if every read is instantly consistent everywhere is importing a relational assumption these systems don’t make for free.

The trap the primary key sets

The property that bites hardest: your partition key is baked into where the data physically lives, so you can’t just change it later. In a relational table, changing which column you look up by is an index change. In a wide-column store, the lookup key is the layout — so “we need to query by a different field now” can mean rebuilding the table and rewriting the data, not altering an index. This makes the up-front “what will I query by?” decision unusually consequential, and it’s why a change of access pattern can turn into a genuine migration. Choosing the partition key is a decision you live with, not a knob you turn.

Model for the questions, and it scales; model for the entities, and it won’t

The reason people conclude these databases “don’t scale” is almost always that they modeled them like a relational database — normalized tables, queries that filter on non-key columns, an assumption of joins — and then hit the cluster-scanning, coordinator-hammering behavior that design produces. (It’s often not even the database; it’s the client’s routing sending everything at one node.) Model for the questions instead — partition by how you look things up, denormalize into a table per query, expect fast writes and eventually-consistent reads, and choose that partition key like you mean it — and the same database that “didn’t scale” scales beautifully. The database isn’t backwards. It just wants you to know your questions before you design your answers. If you’ve made the jump from relational to wide-column modeling, I’d like to hear what finally made it click.