Cassandra Clustering Columns

Clustering columns are the second component of a composite primary key. After the partition key determines which node stores a group of rows, clustering columns determine the sort order of those rows within the partition. They are what make Cassandra powerful for time-series, event logs, and any data that benefits from reading in a specific sequence.

The Filing Cabinet Analogy

Imagine a filing cabinet with many drawers. The drawer label is the partition key — it tells you which drawer to open. Inside each drawer, folders are sorted alphabetically or by date. The sort rule for those folders is the clustering column. Finding a specific folder is instant once you open the right drawer because the sort order is predictable.

Partition Key  ──▶  Selects the Drawer
Clustering Col ──▶  Determines Sort Order Inside the Drawer

Drawer: user_id = 'alice'
  ├── Folder: 2024-01-01 (oldest)
  ├── Folder: 2024-01-02
  ├── Folder: 2024-01-03
  └── Folder: 2024-06-15 (newest)

Defining Clustering Columns

CREATE TABLE user_activity (
  user_id     UUID,
  event_time  TIMESTAMP,
  event_type  TEXT,
  details     TEXT,
  PRIMARY KEY (user_id, event_time)
);

user_id is the partition key. event_time is the clustering column. All events for a given user live in one partition, sorted chronologically by event_time.

Partition: user_id = 'alice-uuid'

event_time               | event_type | details
─────────────────────────────────────────────────────
2024-05-01 08:00:00      | login      | desktop
2024-05-01 08:05:00      | view_page  | /dashboard
2024-05-01 08:10:00      | purchase   | order-099
2024-05-01 08:30:00      | logout     |

Multiple Clustering Columns

You can have more than one clustering column. The first clustering column sorts the rows at the top level. When the first column's values are equal, the second clustering column provides the sub-sort, and so on.

CREATE TABLE game_scores (
  game_id     TEXT,
  level       INT,
  player_id   UUID,
  score       INT,
  PRIMARY KEY (game_id, level, player_id)
);
Partition: game_id = 'chess-pro'

level | player_id | score
──────────────────────────────────
1     | uuid-A    | 980
1     | uuid-B    | 870
1     | uuid-C    | 760
2     | uuid-A    | 1200
2     | uuid-B    | 1100

Rows sort first by level, then by player_id within each level.

Controlling Sort Order with CLUSTERING ORDER BY

By default, clustering columns sort in ascending (ASC) order. Use the CLUSTERING ORDER BY clause to reverse the order to descending (DESC), which is useful when you always want the newest events first.

CREATE TABLE messages (
  room_id    TEXT,
  sent_at    TIMESTAMP,
  sender     TEXT,
  body       TEXT,
  PRIMARY KEY (room_id, sent_at)
) WITH CLUSTERING ORDER BY (sent_at DESC);
Partition: room_id = 'team-chat'

sent_at                  | sender | body
──────────────────────────────────────────────────
2024-06-15 16:55:00      | carol  | See you tomorrow
2024-06-15 16:50:00      | bob    | Meeting is done
2024-06-15 16:45:00      | alice  | Starting now
2024-06-15 16:40:00      | bob    | Be there soon

The most recent message always appears first, making inbox-style queries extremely fast.

Querying with Clustering Columns

Clustering columns support range queries within a partition. You can filter by equality or by range using >, <, >=, and <=.

-- All events for alice on 2024-05-01:
SELECT * FROM user_activity
WHERE user_id = [alice-uuid]
  AND event_time >= '2024-05-01 00:00:00'
  AND event_time <  '2024-05-02 00:00:00';

-- Last 10 messages in a chat room:
SELECT * FROM messages
WHERE room_id = 'team-chat'
LIMIT 10;

The Column Order Rule

You must provide clustering column filters in the order they appear in the primary key definition. You cannot skip a clustering column and filter on a later one without using ALLOW FILTERING.

-- Table: game_scores PRIMARY KEY (game_id, level, player_id)

-- Valid: filter by first clustering column
SELECT * FROM game_scores WHERE game_id='chess-pro' AND level = 1;

-- Valid: filter by both clustering columns
SELECT * FROM game_scores WHERE game_id='chess-pro'
  AND level = 1 AND player_id = [uuid-A];

-- Invalid without ALLOW FILTERING: skips 'level'
SELECT * FROM game_scores WHERE game_id='chess-pro'
  AND player_id = [uuid-A];

Clustering Columns and Storage

On disk, Cassandra stores each partition as a sorted sequence of cells. Clustering column values are physically embedded in the storage key of each cell. This means range reads on clustering columns are sequential disk reads — extremely fast on modern hardware.

Physical Storage (simplified):

Partition key: 'chess-pro'
  Cell: [chess-pro | level=1 | player_id=uuid-A] = score:980
  Cell: [chess-pro | level=1 | player_id=uuid-B] = score:870
  Cell: [chess-pro | level=2 | player_id=uuid-A] = score:1200
  Cell: [chess-pro | level=2 | player_id=uuid-B] = score:1100

Clustering Columns vs Secondary Indexes

Clustering columns provide sorted, efficient range access within a known partition. Secondary indexes allow filtering by columns that are not part of the primary key, but they scatter lookups across many nodes and perform worse at scale. Prefer clustering columns for any sort or range requirement that you know at table design time.

Summary

Clustering columns sort rows within a partition and allow fast range queries. You define them in the PRIMARY KEY clause after the partition key. Use CLUSTERING ORDER BY to control ascending or descending sort direction. Always provide clustering column filters in the order they are defined, and design your clustering columns around the range queries your application makes most frequently.

Leave a Comment

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