Chapter 05 · CQL Foundations: DDL, DML, Filtering Rules, and cqlsh Workflows

WHERE Restrictions, ALLOW FILTERING, Query Constraints, and Why CQL Is Not SQL

Learn why CQL queryability follows primary-key and index design, and demonstrate the cost boundary that ALLOW FILTERING deliberately crosses.

Intermediate105–150 minutesMechanism-first CQL labApache Cassandra 5.0.9 · Java 17 · ASF Java Driver 4.19.3Last reviewed: September 2026

Learning outcomes

AtlasMart developers now ask for “all active electronics products under 50 dollars.” In SQL, that request sounds like a WHERE clause. In Cassandra, the first question is which partition(s) the query can route to and whether the table was modeled for that access path. This lesson makes accepted, rejected and explicitly filtered queries concrete.

01

Explain CQL WHERE restrictions from partition/clustering-key structure instead of memorizing error messages.

02

Differentiate a bounded partition slice from a cluster-wide/filtering request.

03

Use ALLOW FILTERING only on a deliberately small fixture and explain why LIMIT does not make the scan predictable.

04

Use cqlsh tracing to connect a query to coordinator/replica work without treating trace timing as a benchmark.

05

Choose table redesign or an appropriate index/search mechanism rather than adding ALLOW FILTERING by reflex.

Pinned Chapter 05 baseline

Examples target Apache Cassandra 5.0.9, Java 17, cqlsh/nodetool from the same 5.0.9 distribution, three local Docker nodes in dc1/rack1..rack3, NetworkTopologyStrategy RF=3, 16 vnodes per node, and QUORUM for the chapter's normal replicated reads/writes. The ASF Java driver baseline used where application behavior matters is 4.19.3. Authentication/TLS are intentionally disabled only inside the isolated disposable Docker network; later security chapters replace that learning shortcut.

Execution note

This generation environment does not provide a running Docker/Cassandra cluster. Commands and expected output shapes were checked against current official Cassandra/CQL/driver documentation, but no runtime result is presented as captured evidence. Record the exact output, versions, schema UUIDs, timestamps, TTLs, traces, and latencies produced on your own machine.

1. CQL query rules are a data-placement contract

Cassandra distributes partitions by hashing the complete partition key. The coordinator can route a point or bounded partition query because the request identifies the partition key. Clustering restrictions then describe slices inside that partition. When predicates do not identify an efficient access path, Cassandra often rejects the query rather than silently scanning an unbounded data set.

This is the opposite of the “write normalized tables now, let an optimizer discover access paths later” reflex. Cassandra data modeling is query-first. Multiple denormalized tables are normal when the application has multiple stable access patterns.

Query shape Typical CQL status Reason
Complete partition key equality Accepted Coordinator can determine token/replicas
Complete partition key + clustering range Accepted when clustering restrictions follow key order Bounded work inside known partition
Only one component of compound partition key Rejected Does not identify one partition token
Non-key predicate without usable index Rejected unless filtering explicitly allowed Could require scanning data unrelated to result size
Same predicate with ALLOW FILTERING May execute Caller explicitly accepts filtering risk; not an optimization

2. Build a table whose primary key makes one query cheap

bash · verify the reusable cluster
# Disposable Chapter 05 cluster: three nodes, three racks, one DC, 16 vnodes/node# If these objects already exist from Chapters 01–04, reuse them instead of recreating them.docker network create atlasmart-cassandradocker volume create atlasmart-cass-1-datadocker volume create atlasmart-cass-2-datadocker volume create atlasmart-cass-3-datadocker 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 for node 1 to accept CQL, then start the two peers.docker run -d --name atlasmart-cass-2 --hostname atlasmart-cass-2 --network atlasmart-cassandra   -e CASSANDRA_CLUSTER_NAME=atlasmart-course -e CASSANDRA_SEEDS=atlasmart-cass-1   -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack2   -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch   -e CASSANDRA_NUM_TOKENS=16   -v atlasmart-cass-2-data:/var/lib/cassandra cassandra:5.0.9docker run -d --name atlasmart-cass-3 --hostname atlasmart-cass-3 --network atlasmart-cassandra   -e CASSANDRA_CLUSTER_NAME=atlasmart-course -e CASSANDRA_SEEDS=atlasmart-cass-1   -e CASSANDRA_DC=dc1 -e CASSANDRA_RACK=rack3   -e CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch   -e CASSANDRA_NUM_TOKENS=16   -v atlasmart-cass-3-data:/var/lib/cassandra cassandra:5.0.9docker exec atlasmart-cass-1 nodetool statusdocker exec atlasmart-cass-1 nodetool versiondocker exec atlasmart-cass-1 java -version
sql · create the chapter schema
CREATE KEYSPACE IF NOT EXISTS atlasmart_cqlWITH replication = {'class': 'NetworkTopologyStrategy', 'dc1': 3}AND durable_writes = true;CONSISTENCY QUORUM;CREATE TABLE IF NOT EXISTS atlasmart_cql.products_by_id (    product_id text PRIMARY KEY,    name text,    category text,    price_cents bigint,    status text,    description text,    updated_at timestamp);CREATE TABLE IF NOT EXISTS atlasmart_cql.products_by_category (    category text,    bucket tinyint,    product_id text,    name text,    status text,    price_cents bigint,    PRIMARY KEY ((category, bucket), product_id));CREATE TABLE IF NOT EXISTS atlasmart_cql.inventory_by_product (    product_id text,    location_id text,    quantity int,    status text,    promo_note text,    PRIMARY KEY ((product_id), location_id));
sql · load a bounded fixture
INSERT INTO atlasmart_cql.products_by_category (category,bucket,product_id,name,status,price_cents) VALUES ('electronics',0,'p-100','Mouse','active',2500);INSERT INTO atlasmart_cql.products_by_category (category,bucket,product_id,name,status,price_cents) VALUES ('electronics',0,'p-101','Keyboard','active',4800);INSERT INTO atlasmart_cql.products_by_category (category,bucket,product_id,name,status,price_cents) VALUES ('electronics',0,'p-102','Camera','inactive',29900);INSERT INTO atlasmart_cql.products_by_category (category,bucket,product_id,name,status,price_cents) VALUES ('books',0,'p-200','Cassandra Field Guide','active',3900);-- Designed query: exact compound partition key.SELECT product_id,name,status,price_centsFROM atlasmart_cql.products_by_categoryWHERE category='electronics' AND bucket=0;

This table supports “products in one category bucket.” It does not automatically support every predicate on status and price. Bucketing also creates an application obligation: if a category spans several buckets, the application must know which buckets to query and merge.

3. Observe rejected query shapes before reaching for ALLOW FILTERING

sql · deliberately invalid or filtering-prone queries
-- Missing one component of the compound partition key: expect rejection.SELECT * FROM atlasmart_cql.products_by_categoryWHERE category='electronics';-- Non-key predicate without an index/access path: expect rejection.SELECT * FROM atlasmart_cql.products_by_categoryWHERE status='active';-- Complete partition key plus non-key filtering predicate: may still require ALLOW FILTERING.SELECT * FROM atlasmart_cql.products_by_categoryWHERE category='electronics' AND bucket=0 AND status='active';

Capture the exact Cassandra error text on your version. The error is a design signal, not an inconvenience to suppress. Ask whether the access pattern deserves its own table, whether Storage-Attached Indexing (SAI) is appropriate in the later indexing chapter, or whether this is genuinely a tiny administrative fixture where filtering is bounded and acceptable.

4. ALLOW FILTERING changes permission, not the physical schema

The clause says, in effect, “execute this request even though Cassandra cannot guarantee work proportional to the returned rows.” It does not create an index, repartition data, or teach the coordinator a cheaper route. A LIMIT 10 limits returned rows; it does not guarantee the system examines only ten rows.

sql · use filtering only on the deliberately tiny fixture
SELECT product_id,name,statusFROM atlasmart_cql.products_by_categoryWHERE status='active'ALLOW FILTERING;SELECT product_id,name,status,price_centsFROM atlasmart_cql.products_by_categoryWHERE category='electronics' AND bucket=0  AND status='active'ALLOW FILTERING;
Do not copy this clause into an unbounded production query

The lesson fixture has four rows so the cost is intentionally bounded. As partitions, nodes and tombstones grow, filtering work can grow independently of the few rows returned. Production acceptance requires measured data volume, partition distribution, trace/metrics, tail latency and a deliberate reason not to remodel or index.

5. Use tracing to see a request path, not to publish benchmarks

cqlsh TRACING ON asks Cassandra to record trace events for subsequent queries. Trace output can reveal coordinator and replica steps and is excellent for mechanism learning. It adds overhead and its individual timings are not a load-test methodology.

sql · trace one designed query
TRACING ON;CONSISTENCY QUORUM;SELECT product_id,name,status,price_centsFROM atlasmart_cql.products_by_categoryWHERE category='electronics' AND bucket=0;TRACING OFF;

Record the coordinator address, replica-related events and elapsed times shown on your cluster. Then repeat the filtered query on the tiny fixture and compare the qualitative event pattern. Do not interpret one trace as p95/p99 performance; Chapter 24 develops representative benchmarking and observability.

6. CQL is not SQL with missing features

No implicit joins, no arbitrary cross-partition predicates, and no cost-based optimizer choosing among many relational access paths are not accidental omissions. Cassandra trades those freedoms for predictable routing around known partition keys and denormalized query tables. The design question is therefore “what query must this table serve?” before “what columns describe this entity?”

For AtlasMart, a dedicated active_products_by_category_bucket table could make active-category reads direct at the cost of write fan-out and reconciliation responsibility. SAI may be a better fit when indexed predicates and selectivity justify it. Search/vector chapters cover different workloads again. Do not force one table to be every access path.

Verification checklist

  • The complete compound partition-key query succeeds.
  • A query missing bucket is rejected on the unindexed table.
  • The status-only query is rejected unless filtering is explicitly allowed.
  • ALLOW FILTERING is tested only on the bounded fixture and its risk is documented.
  • You captured one trace and can distinguish trace evidence from benchmark evidence.

Check your understanding

  1. Why can Cassandra efficiently route an equality predicate on the full partition key?
  2. What does ALLOW FILTERING optimize?
  3. Does LIMIT guarantee that only LIMIT rows are examined?
  4. Why might a second table be preferable to arbitrary filtering?
  5. Why is cqlsh tracing not a benchmark?
Review the answers

1. The driver/coordinator can compute the partition token and identify the natural replicas.

2. Nothing by itself. It grants permission to execute a query that may require filtering with unpredictable work.

3. No. It limits returned rows; filtering may inspect far more data before producing them.

4. A query-specific table encodes a direct partition/access path, exchanging write duplication for predictable reads.

5. Tracing adds overhead and represents individual requests, not a representative concurrent latency distribution.

Summary and next bridge

You can now treat CQL restrictions as placement/routing feedback rather than syntactic hostility. Lesson 4 separates interactive cqlsh workflows from application execution and proves how prepared/bound statements carry reusable query structure and routing metadata.

bash · reset the disposable Chapter 05 lab
# Chapter-only reset or deliberate preservation# Keep atlasmart_cql if you are continuing directly to the next lesson.# Full reset (deletes only the dedicated course containers/volumes/network)docker rm -f atlasmart-cass-1 atlasmart-cass-2 atlasmart-cass-3docker volume rm atlasmart-cass-1-data atlasmart-cass-2-data atlasmart-cass-3-datadocker network rm atlasmart-cassandra

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.