Chapter 07 · Primary Keys, Partition Keys, Clustering Columns, and Ordering

PRIMARY KEY Syntax: Composite Partition Keys and Ordered Clustering Columns

Parse the punctuation that decides both replica placement and the sorted row layout AtlasMart reads.

Intermediate90–120 minutesComposite-key + clustering-order labApache Cassandra 5.0.9 · cqlsh/nodetool · UCSLast reviewed: September 2026

Learning outcomes

AtlasMart wants “the newest order events for one customer in one month” without scanning the cluster or sorting a large result in application memory. The table's PRIMARY KEY must encode both where that customer's month lives and how rows are ordered inside that partition. This lesson turns the punctuation in CQL primary-key syntax into a physical mental model.

01

Parse simple, compound, and composite PRIMARY KEY forms without confusing the partition key with the whole primary key.

02

Explain how the partition-key values are hashed to a token and therefore select a replica set.

03

Explain how clustering columns add row uniqueness and define sorted within-partition layout.

04

Use CLUSTERING ORDER BY to match a dominant newest-first read while respecting legal reverse-order queries.

05

Diagnose a schema where the developer thought every primary-key column participated in distribution.

Chapter 07 lab baseline

The mandatory labs use the pinned cassandra:5.0.9 image. Java 17, cqlsh, and nodetool are the versions bundled by that image. The course topology is three disposable nodes (atlasmart-cass-1..3) in cluster atlasmart-course, datacenter dc1, racks rack1..rack3, 16 vnodes per node, replication factor (RF) 3, and LOCAL_QUORUM for the chapter's consistency-sensitive examples. Authentication, client TLS, internode TLS, and remote JMX are not enabled in this isolated learning network; do not copy that security posture to production. New Chapter 07 tables explicitly use UnifiedCompactionStrategy (UCS). Default table TTL is zero unless a lesson says otherwise; gc_grace_seconds is not changed. SAI/vector features are not used. No application driver is required for the mandatory lab; if you adapt the examples to an application, re-check your chosen driver's current compatibility and routing behavior.

Execution disclosure and resource path

These commands are documentation- and syntax-reviewed but were not executed in this generation environment. Treat shown output as an expected shape, then capture your own exact tokens, latencies, partition-size percentiles, and errors. A three-node local cluster commonly needs several GiB of RAM; if your machine cannot support it, use one disposable node and RF=1 to learn primary-key/clustering mechanics, but do not interpret that reduced topology as evidence about RF=3 availability or replica behavior.

1. PRIMARY KEY is two decisions, not one label

In Cassandra Query Language (CQL), a primary key uniquely identifies a row, but its first component has a second job: it is the partition key. Cassandra serializes the partition-key value or values and the configured partitioner maps that byte representation to a token. The token determines the token range and therefore the natural replica set. Columns after the partition key are clustering columns; they do not choose another node. They order rows that already belong to the same partition and extend row uniqueness.

Definition Partition key Clustering columns Physical consequence
PRIMARY KEY (a) a none one logical row per partition
PRIMARY KEY (a,b,c) a b,c all rows with the same a colocate; b,c sort within it
PRIMARY KEY ((a,b),c) (a,b) together c the pair (a,b) is hashed as one partition-key value
Parentheses change distribution.

PRIMARY KEY (customer_id, month_bucket, event_time) partitions only by customer_id. PRIMARY KEY ((customer_id, month_bucket), event_time) partitions by the customer/month pair. A monthly bucket does nothing for partition growth unless it is actually inside the partition-key component.

2. AtlasMart schema: customer + month chooses placement; time chooses order

The read contract is: the caller knows customer_id and month_bucket, wants a bounded time slice, and normally reads newest events first. That yields a composite partition key (customer_id, month_bucket). Within the partition, event_time and event_id give deterministic ordering and uniqueness. The event_id tie-breaker matters because two events can share a millisecond timestamp.

sql · create the Chapter 07 event table
CREATE TABLE atlasmart_keys.order_events_by_customer_bucket (    customer_id uuid,    month_bucket text,    event_time timestamp,    event_id timeuuid,    order_id uuid,    event_type text,    payload text,    PRIMARY KEY ((customer_id, month_bucket), event_time, event_id)) WITH CLUSTERING ORDER BY (event_time DESC, event_id DESC)  AND compaction = {'class':'UnifiedCompactionStrategy'};

The declared descending order is a storage choice optimized for the dominant newest-first access pattern. Cassandra can read the complete clustering order in the declared direction or its reverse; it is not a general-purpose ORDER BY any_column engine.

3. Observe token identity and sorted rows

bash · verify or recreate the disposable course cluster
# If you already have the Chapter 01-06 lab, verify it first.docker exec atlasmart-cass-1 nodetool versiondocker exec atlasmart-cass-1 nodetool status# Standalone recreation path (skip existing resources as needed).docker network inspect atlasmart-cassandra >/dev/null 2>&1 || docker network create atlasmart-cassandradocker volume create atlasmart-cass-1-datadocker volume create atlasmart-cass-2-datadocker volume create atlasmart-cass-3-datadocker inspect atlasmart-cass-1 >/dev/null 2>&1 || docker run -d --name atlasmart-cass-1 --hostname atlasmart-cass-1 --network atlasmart-cassandra -e CASSANDRA_CLUSTER_NAME=atlasmart-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack1 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -v atlasmart-cass-1-data:/var/lib/cassandra cassandra:5.0.9# Wait until node 1 answers before starting peers.docker exec atlasmart-cass-1 nodetool statusdocker inspect atlasmart-cass-2 >/dev/null 2>&1 || docker run -d --name atlasmart-cass-2 --hostname atlasmart-cass-2 --network atlasmart-cassandra -e CASSANDRA_CLUSTER_NAME=atlasmart-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack2 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -e CASSANDRA_SEEDS=atlasmart-cass-1 -v atlasmart-cass-2-data:/var/lib/cassandra cassandra:5.0.9docker inspect atlasmart-cass-3 >/dev/null 2>&1 || docker run -d --name atlasmart-cass-3 --hostname atlasmart-cass-3 --network atlasmart-cassandra -e CASSANDRA_CLUSTER_NAME=atlasmart-course -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack3 -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch -e CASSANDRA_NUM_TOKENS=16 -e CASSANDRA_SEEDS=atlasmart-cass-1 -v atlasmart-cass-3-data:/var/lib/cassandra cassandra:5.0.9# Continue only when all three nodes report UN.docker exec atlasmart-cass-1 nodetool statusdocker exec atlasmart-cass-1 cqlsh -e "CREATE KEYSPACE IF NOT EXISTS atlasmart_keys WITH replication = {'class':'NetworkTopologyStrategy','dc1':3};"docker exec atlasmart-cass-1 cqlsh -e "DESCRIBE KEYSPACE atlasmart_keys"
sql · insert deterministic fixture and inspect partition token
CONSISTENCY LOCAL_QUORUM;INSERT INTO atlasmart_keys.order_events_by_customer_bucket(customer_id,month_bucket,event_time,event_id,order_id,event_type,payload)VALUES (11111111-1111-1111-1111-111111111111,'2026-09','2026-09-07T10:00:00Z',11111111-1111-11f1-8000-000000000001,aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa,'CREATED','{}');INSERT INTO atlasmart_keys.order_events_by_customer_bucket(customer_id,month_bucket,event_time,event_id,order_id,event_type,payload)VALUES (11111111-1111-1111-1111-111111111111,'2026-09','2026-09-07T10:05:00Z',22222222-2222-11f1-8000-000000000002,aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa,'PAID','{}');INSERT INTO atlasmart_keys.order_events_by_customer_bucket(customer_id,month_bucket,event_time,event_id,order_id,event_type,payload)VALUES (11111111-1111-1111-1111-111111111111,'2026-09','2026-09-07T10:10:00Z',33333333-3333-11f1-8000-000000000003,aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa,'SHIPPED','{}');SELECT customer_id, month_bucket, token(customer_id,month_bucket) AS partition_token,       event_time, event_typeFROM atlasmart_keys.order_events_by_customer_bucketWHERE customer_id=11111111-1111-1111-1111-111111111111  AND month_bucket='2026-09';

Expected shape: all three rows show the same partition_token because the same two partition-key values identify one partition; rows appear newest first because of the declared clustering order. The exact token is environment/data dependent and should be captured, not copied from a tutorial.

sql · valid slice and legal reverse order
SELECT event_time,event_typeFROM atlasmart_keys.order_events_by_customer_bucketWHERE customer_id=11111111-1111-1111-1111-111111111111  AND month_bucket='2026-09'  AND event_time >= '2026-09-07T10:00:00Z'  AND event_time <  '2026-09-07T10:11:00Z';SELECT event_time,event_typeFROM atlasmart_keys.order_events_by_customer_bucketWHERE customer_id=11111111-1111-1111-1111-111111111111  AND month_bucket='2026-09'ORDER BY event_time ASC, event_id ASC;

4. Broken design: “month is in the primary key, so it must shard by month”

A common mistake is PRIMARY KEY (customer_id, month_bucket, event_time). Here only customer_id is the partition key. Month and time are clustering columns. A heavy customer therefore accumulates every month in the same physical partition, defeating the intended retention bound.

sql · prove the missing-partition-component failure
-- This query is intentionally invalid for the correct composite-key table.SELECT * FROM atlasmart_keys.order_events_by_customer_bucketWHERE customer_id=11111111-1111-1111-1111-111111111111  AND event_time >= '2026-09-01T00:00:00Z';

Cassandra should reject the request because a complete composite partition key is not supplied and the request does not describe a single contiguous partition slice. The exact error wording can vary by patch. The repair is not ALLOW FILTERING; the repair is to make the query contract and primary-key grammar agree.

5. Production judgment

Primary-key design is where workload fit becomes physical. A useful partition key has enough cardinality to distribute traffic, enough locality to serve the target read without scatter/gather, and a growth bound that survives retention, repair, compaction and failure recovery. Clustering columns should mirror the order and range predicates you actually use. RF and consistency level determine how many replicas must participate, but they do not rescue a hot single partition: every replica for that partition still sees its traffic. Larger rows, long TTL/delete histories and large partitions amplify SSTable, compaction, tombstone, repair, cache and JVM costs. Application drivers benefit from knowing the complete partition key for token-aware routing; timeout/retry policy must still respect idempotency. Security, SAI and vector features do not change the underlying primary-key placement contract.

Verification checklist

  • You can identify the partition-key component by reading parentheses, not by guessing from column names.
  • The token is constant for rows sharing (customer_id,month_bucket).
  • The default result order matches the declared clustering order.
  • The bounded time-slice query works only after the complete partition key is supplied.
  • You can explain why adding month_bucket as a clustering column would not bound physical partition growth.

Check your understanding

  1. Which part of PRIMARY KEY ((a,b),c,d) determines token placement?
  2. Why include event_id after event_time?
  3. Can ORDER BY sort by payload?
  4. Does RF=3 spread one hot partition across arbitrary nodes on every request?
  5. Why is ALLOW FILTERING not the fix for a missing month_bucket?
Review the answers

1. The pair (a,b). Columns c,d are clustering columns and do not independently choose nodes.

2. It gives deterministic row uniqueness/order when multiple events share the same timestamp.

3. No. Cassandra ordering follows clustering columns and their declared/reverse clustering order, not arbitrary regular columns.

4. No. That partition has a fixed natural replica set; its replicas still absorb the hot key’s traffic.

5. Because the schema/query contract is wrong. Filtering does not create a bounded partition-addressable access path.

bash · reset only Chapter 07 data
# Destructive only to this disposable chapter keyspace.docker exec atlasmart-cass-1 cqlsh -e "DROP KEYSPACE IF EXISTS atlasmart_keys;"# Keep the shared course containers for the next chapter, or remove them only if you want a full reset:# docker rm -f atlasmart-cass-1 atlasmart-cass-2 atlasmart-cass-3# docker volume rm atlasmart-cass-1-data atlasmart-cass-2-data atlasmart-cass-3-data# docker network rm atlasmart-cassandra

Summary and next bridge

The primary key is simultaneously row identity, partition placement, and within-partition sort order. Next we quantify whether candidate partition keys have enough cardinality and sufficiently bounded growth to remain healthy under real AtlasMart load.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.