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.
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.
Relate clustering-column sequence to lexicographic row order inside one partition.
Explain the prefix/contiguous-slice rule instead of memorizing isolated WHERE-clause errors.
Choose declared ASC/DESC clustering order from dominant read direction.
Use tracing carefully to confirm a bounded single-partition read path.
Redesign a query that skips an earlier clustering column rather than hiding it behind filtering.
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.
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
# 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"
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
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.
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
- Why can status then time support status-specific time ranges?
- Why is a time range without status problematic in this schema?
- What does CLUSTERING ORDER BY optimize?
- Should tracing be used for throughput benchmarks?
- 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.
# 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
- CQL data definition — primary-key grammar, partition keys, clustering columns, and clustering order.
- CQL data manipulation — primary-key restrictions, contiguous clustering slices, token queries, and ordering behavior.
- Cassandra data-modeling introduction — partition-key and clustering-key physical meaning.
-
CREATE TABLE reference
— composite partition keys and
CLUSTERING ORDER BY. - nodetool tablehistograms — table-level percentile evidence including partition-size distribution.
- nodetool tablestats — table statistics and partition-size-related metrics.