DataLane
(updated )4 min readCassandra

Cassandra: A New Query Is a New Table, Not a New Index Hope

ALLOW FILTERING timed out the coordinator and a "quick report" full-scanned the serving cluster. Model the query first, extract without a table scan, and keep Cassandra off the warehouse path.

By Dinesh Chandra

Illustrated overview of Cassandra: A New Query Is a New Table, Not a New Index Hope
Table of contents

The on-call page was coordinator CPU, not a missing dashboard. An analyst had been given a CQL user “for a one-off.” The query was SELECT * FROM events WHERE day = '2026-08-24' ALLOW FILTERING. day was not the partition key. The cluster did a scatter-gather. Timeouts started on the read path the app actually uses.

We revoked the role and rebuilt the report from the warehouse. The serving table was fine. The model was honest: this table answered events_by_user and nothing else. The lie was that Cassandra would answer a new question because it looks like SQL.

I design Cassandra backwards from the query. If product needs a second query, that is a second table, written twice from the same application event, not an index I hope the coordinator can paper over.

Query first, then keys

Write the CQL you will run in prod. Then pick keys so that CQL is a single-partition (or token-aware few-partition) read.

Partition key — which node owns the data. Every query you care about must specify it. Equality, not a range, unless you really meant a token range job.

Clustering key — sort order inside the partition. Ranges, LIMIT, and “latest N” live here. They do not replace the partition key.

-- Serves: get a user's events for a day, newest first.
CREATE TABLE serving.events_by_user_day (
    user_id    uuid,
    day        date,
    event_ts   timestamp,
    event_id   uuid,
    event_type text,
    payload    text,
    PRIMARY KEY ((user_id, day), event_ts, event_id)
) WITH CLUSTERING ORDER BY (event_ts DESC, event_id ASC);

-- New query: events by type for a day. New table.
CREATE TABLE serving.events_by_type_day (
    event_type text,
    day        date,
    event_ts   timestamp,
    event_id   uuid,
    user_id    uuid,
    payload    text,
    PRIMARY KEY ((event_type, day), event_ts, event_id)
) WITH CLUSTERING ORDER BY (event_ts DESC, event_id ASC);

Same writes, two tables. That feels wasteful until you price a coordinator-wide filter against a 40-node ring. Disk is cheaper than a serving outage.

flowchart TD
  q["Write the query"] --> pk["Partition key from equality"]
  pk --> ck["Clustering for order and range"]
  ck --> t["One table per query"]
  q2["New query"] --> t2["New table, same write path"]
  hope["ALLOW FILTERING"] --> scan["Scatter-gather"]
  scan --> timeout["Coordinator timeout"]

The second table is the feature. ALLOW FILTERING is the incident.

Not a warehouse

I do not run finance rollups on Cassandra. No joins, no ad-hoc group-by, no “add a materialized view later” as a substitute for a warehouse model. Views have their own operational cost and still do not make this a columnar analyzer.

Facts and slowly changing dimensions live where the star schema post says they live. Cassandra holds the keyed read the app or the feature service needs in single-digit milliseconds.

Extract without a full scan

Warehouse loads should not SELECT * across every partition. Options that have not paged me:

  • The application writes the same event to Kafka (or the warehouse landing zone) as it writes to Cassandra.
  • A change-data path if you operate one and accept the lag and the operational weight.
  • A bounded extract that walks known partition keys you already have in silver (user ids for the day), not a token-range surprise on the serving cluster at noon.
from cassandra.cluster import Cluster
from cassandra.query import SimpleStatement

SELECT = SimpleStatement(
    """
    SELECT event_id, event_ts, event_type, payload
    FROM serving.events_by_user_day
    WHERE user_id = %s AND day = %s
    """
)

def extract_user_day(session, user_id, day):
    return session.execute(SELECT, (user_id, day))

That is an extract. A token-range Spark job against the serving DC at 14:00 is a load test you did not schedule.

Pitfalls

Secondary index on a high-cardinality column “so BI can filter.” Coordinator scatter. You wanted a table.

Partitions that grow without a day (or bucket) in the key. Hot partitions, long repairs, p99 death.

Materialized views as a free second query. They are another write path with failure modes. Budget them like a table.

Sparking the whole ring for a backfill during business hours. Use the write-time log or a quiet window and throttle.

Teaching analysts CQL and ALLOW FILTERING. Teach them the warehouse.

What this means for your pipelines

Cassandra is fast when the query was designed with the table. It is a liability when someone treats CQL like Snowflake. I add a table when I add a query, I keep partitions bounded, and I extract along keys I already know.

The coordinator timeout was not a capacity miss. It was a report that never should have run there. Put the new question in a new table or in the warehouse. Do not hope an index will invent a partition key you did not write.

Share this post:X / TwitterLinkedIn

Enjoyed this post?

Get the next one in your inbox — one email a week, no spam.

Newsletter signup is not live yet. Use the contact form if you want to be notified.

↑↓ navigate openesc close