Cassandra Denormalization

Denormalization means storing redundant copies of data across multiple tables so that each table can answer a specific query efficiently. In relational databases, normalization removes redundancy. In Cassandra, denormalization adds it intentionally — because Cassandra cannot perform joins at query time, each table must contain everything a query needs in one place.

The Fast Food Analogy

A relational database works like a sit-down restaurant where a waiter fetches your main course, then goes back for side dishes, then visits another station for drinks, then assembles everything at your table (a JOIN). Cassandra works like a fast food counter where every combo is pre-assembled in one tray and handed to you instantly. Denormalization is the act of pre-assembling those trays in advance.

Normalization vs Denormalization

Normalized (relational style):

Table: users          Table: orders         Table: products
user_id | name        order_id | user_id    product_id | name | price
─────────────────     ─────────────────     ──────────────────────────
u-1     | Alice       o-1      | u-1        p-1        | Laptop | 999
u-2     | Bob         o-2      | u-1        p-2        | Mouse  | 29

To answer "show Alice's orders with product names and prices":
  → JOIN users + orders + products
  → 3 table reads + join operation

Denormalized (Cassandra style):

Table: orders_by_user
user_id | order_id | user_name | product_name | product_price
───────────────────────────────────────────────────────────────────
u-1     | o-1      | Alice     | Laptop       | 999
u-1     | o-2      | Alice     | Mouse        |  29

To answer the same question:
  → 1 table read by partition key u-1
  → Done instantly

When to Denormalize

Denormalize whenever two different query patterns need the same underlying data arranged differently. If your application needs to answer "orders by customer" and "orders by status," you create two separate tables — one per query — and write the order data to both.

Query 1: "Show all orders for customer X"
  → Table: orders_by_customer
    PRIMARY KEY (customer_id, order_date)

Query 2: "Show all pending orders this week"
  → Table: orders_by_status
    PRIMARY KEY (status, order_date, order_id)

Application writes to BOTH tables on every order creation.

Managing Consistency Across Denormalized Tables

When data lives in multiple tables, your application must keep them in sync. The standard pattern is to write to all tables in the same LOGGED BATCH. Cassandra's logged batch guarantees that all statements in the batch eventually apply — even if a node fails mid-batch.

BEGIN BATCH
  INSERT INTO orders_by_customer (customer_id, order_date, order_id, status, total)
  VALUES ([uuid], '2024-06-15', [order-uuid], 'pending', 149.99);

  INSERT INTO orders_by_status (status, order_date, order_id, customer_id, total)
  VALUES ('pending', '2024-06-15', [order-uuid], [uuid], 149.99);
APPLY BATCH;

Trade-offs of Denormalization

Benefit                              Cost
──────────────────────────────────────────────────────────────
Reads are fast (single partition)    More disk storage
No joins needed                      Application writes to multiple
                                     tables
Queries are predictable              Updates must touch every copy
Simple, partition-local queries      Schema changes affect all
                                     denormalized tables
Scales horizontally without joins    Harder to reason about data
                                     consistency across tables

Practical Denormalization Example: Social Media

Application needs:
  1. "Show my timeline (posts from people I follow)"
  2. "Show a specific user's posts"

Normalized approach (unusable in Cassandra):
  JOIN posts + follows → compute timeline per request

Denormalized approach:

Table: posts_by_user
  PRIMARY KEY (user_id, post_time)
  → Answers query 2

Table: timeline_by_user
  PRIMARY KEY (viewer_id, post_time, post_id)
  → Answers query 1

On every new post:
  INSERT into posts_by_user
  INSERT into timeline_by_user for EACH follower

Denormalization is a Feature, Not a Bug

Developers coming from relational backgrounds often view data duplication as a mistake. In Cassandra, it is a deliberate design choice that makes distributed, join-free reads possible at massive scale. The key is to denormalize thoughtfully — only create the tables your queries actually need, and manage write consistency carefully across all copies.

Summary

Denormalization stores redundant copies of data across multiple tables, each optimized for one query pattern. It replaces joins with pre-assembled partition reads. Write to all relevant tables in a LOGGED BATCH to maintain consistency. Accept the storage cost and increased write complexity as the price for fast, scalable, join-free reads. Good Cassandra data modeling starts with listing your queries first and designing one table per query.

Leave a Comment

Your email address will not be published. Required fields are marked *