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

Clustering Order, Slice Queries, Prefix Rules, and Efficient Within-Partition Reads

Make clustering tuples and contiguous-slice rules explain which time-range reads are naturally efficient.

Intermediate90–120 minutesClustering-prefix + tracing labApache Cassandra 5.0.9 · cqlsh/nodetool · UCSLast reviewed: September 2026

Learning outcomes

AtlasMart's order-history API knows the customer and month, then asks for one status and a time range. Cassandra can serve that as a contiguous clustering slice—if the clustering columns are ordered to match the predicates. If the API skips an earlier clustering column, the same-looking query can be rejected or require a different table.

01

Relate clustering-column sequence to lexicographic row order inside one partition.

02

Explain the prefix/contiguous-slice rule instead of memorizing isolated WHERE-clause errors.

03

Choose declared ASC/DESC clustering order from dominant read direction.

04

Use tracing carefully to confirm a bounded single-partition read path.

05

Redesign a query that skips an earlier clustering column rather than hiding it behind filtering.

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. Clustering order is lexicographic storage order

For PRIMARY KEY ((customer_id,month_bucket), status, order_time, order_id), rows inside each customer/month partition are ordered first by status, then by order_time within equal status, then by order_id as a tie-breaker. Think of this as an ordered tuple. A range on a later clustering component is efficient when all earlier components have been constrained so the requested rows form one contiguous interval in that tuple order.

Predicate shape Result Reason
partition key + status='SHIPPED' + time range natural contiguous slice earlier clustering prefix is fixed
partition key + time range, status omitted normally rejected without another access path matching rows are interleaved across status groups
partition key + exact status prefix read all rows in one clustering prefix
ORDER BY arbitrary non-key column not supported as relational sort storage order is defined by clustering columns

2. Build a table for the exact slice

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 · schema aligned to status + newest-time slices
CREATE TABLE atlasmart_keys.orders_by_customer_bucket_status (    customer_id uuid,    month_bucket text,    status text,    order_time timestamp,    order_id uuid,    total_cents bigint,    PRIMARY KEY ((customer_id, month_bucket), status, order_time, order_id)) WITH CLUSTERING ORDER BY (status ASC, order_time DESC, order_id ASC)  AND compaction = {'class':'UnifiedCompactionStrategy'};INSERT INTO atlasmart_keys.orders_by_customer_bucket_status VALUES(11111111-1111-1111-1111-111111111111,'2026-09','PAID','2026-09-07T10:00:00Z',aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1,12000);INSERT INTO atlasmart_keys.orders_by_customer_bucket_status VALUES(11111111-1111-1111-1111-111111111111,'2026-09','SHIPPED','2026-09-07T10:20:00Z',aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2,12000);INSERT INTO atlasmart_keys.orders_by_customer_bucket_status VALUES(11111111-1111-1111-1111-111111111111,'2026-09','SHIPPED','2026-09-07T10:10:00Z',aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa3,9000);INSERT INTO atlasmart_keys.orders_by_customer_bucket_status VALUES(11111111-1111-1111-1111-111111111111,'2026-09','SHIPPED','2026-09-07T09:50:00Z',aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa4,4500);SELECT status,order_time,order_id,total_centsFROM atlasmart_keys.orders_by_customer_bucket_statusWHERE customer_id=11111111-1111-1111-1111-111111111111  AND month_bucket='2026-09'  AND status='SHIPPED'  AND order_time >= '2026-09-07T10:00:00Z';

The expected result contains the two SHIPPED rows at 10:20 and 10:10, newest first. The coordinator can identify one partition from the composite partition key and one contiguous clustering interval from the status/time constraints.

3. Trace the good query, then trigger the prefix failure

sql · bounded tracing and deliberately invalid shape
TRACING ON;SELECT status,order_time,total_centsFROM atlasmart_keys.orders_by_customer_bucket_statusWHERE customer_id=11111111-1111-1111-1111-111111111111  AND month_bucket='2026-09'  AND status='SHIPPED'  AND order_time >= '2026-09-07T10:00:00Z';TRACING OFF;-- Deliberately wrong: skips the first clustering column (status).SELECT status,order_time,total_centsFROM atlasmart_keys.orders_by_customer_bucket_statusWHERE customer_id=11111111-1111-1111-1111-111111111111  AND month_bucket='2026-09'  AND order_time >= '2026-09-07T10:00:00Z';

Tracing is diagnostic overhead, not a benchmark mode. The good trace should show a targeted read rather than prove a universal latency number. The second query should be rejected in this schema because a time range without fixing the preceding status clustering component does not describe one contiguous slice. Exact error text can vary by patch.

4. Repair the access path, not the error message

If AtlasMart genuinely needs “all statuses in this customer/month after time T,” then status should not precede time in the table serving that query. Build another query table such as orders_by_customer_bucket_time with order_time as the first clustering column. This is the query-first denormalization pattern from Chapter 06. Adding ALLOW FILTERING to make one schema impersonate both access paths can replace a predictable slice with data-dependent scanning and tail latency.

sql · second read shape for status-agnostic time queries
CREATE TABLE atlasmart_keys.orders_by_customer_bucket_time (    customer_id uuid,    month_bucket text,    order_time timestamp,    order_id uuid,    status text,    total_cents bigint,    PRIMARY KEY ((customer_id, month_bucket), order_time, order_id)) WITH CLUSTERING ORDER BY (order_time DESC, order_id ASC)  AND compaction = {'class':'UnifiedCompactionStrategy'};

5. Production judgment

Clustering order should reflect the stable read contract, not an aesthetic preference. Put highly constrained grouping columns before range columns when that is the dominant query, and remember that changing clustering order usually means building/migrating a table rather than issuing a casual runtime sort. Wide partitions increase the value of efficient slices but also increase compaction, tombstone and repair consequences; bounded partition design still comes first. Monitor read latency and cells/tombstones scanned, test reverse-order reads if they matter, and include realistic status/cardinality skew. Drivers should bind the full partition key and clustering predicates so token-aware routing and paging operate on a known read shape; retries must remain bounded and idempotency-aware.

Verification checklist

  • The valid status/time slice returns the expected contiguous rows.
  • The deliberately skipped clustering-prefix query is rejected rather than silently scanned.
  • You inspected one trace only as diagnostic evidence, not as a benchmark result.
  • You can explain why a second query table is cleaner than forcing incompatible predicates.

Check your understanding

  1. Why can status then time support status-specific time ranges?
  2. Why is a time range without status problematic in this schema?
  3. What does CLUSTERING ORDER BY optimize?
  4. Should tracing be used for throughput benchmarks?
  5. What is the Cassandra-native fix for a second incompatible query shape?
Review the answers

1. Fixing status selects one clustering prefix; the time range is contiguous within that prefix.

2. Rows for different status values occupy separate lexicographic groups, so the time predicate alone is not one contiguous clustering slice.

3. The physical/default within-partition order, allowing the dominant direction to be read naturally.

4. No. Tracing adds overhead and is for request-path diagnosis, not clean capacity measurement.

5. Create another query table with a primary/clustering key designed for that read and maintain the projection deliberately.

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

Clustering columns make efficient reads possible only when predicates follow the stored tuple order. Next we add explicit time buckets and synthetic shards when one otherwise-correct partition would still grow or run too hot.

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.