Without joins, the schema must be shaped by the application's queries, which is the core skill of Cassandra.
Relational design starts from entities and normalizes, then lets the query planner join. Cassandra has no joins and limited filtering, so you invert the process: list the queries first, then design a table for each so that the query reads a single partition.
The method:
- write down the application's access patterns: "messages in a channel, newest first", "channels a user belongs to", "user by email",
- for each, choose a partition key from what the query knows (
channel_id,user_id,email) and clustering columns for the requested order, - include every column the query needs in that table.
The result is denormalized: the same data appears in several tables, e.g. users_by_id and users_by_email. Writes are cheap, so writing to several tables is fine; the application keeps them in sync, often with a logged batch so all writes eventually apply.
Naming tables <entity>_by_<key> makes the pattern obvious. Ask of every table: does each query hit exactly one partition, are partitions bounded in size, and is data evenly spread?
Many-to-many relations become two tables, one per direction (members_by_channel, channels_by_member). Aggregates you need often are maintained as you write (counters or summary tables) rather than computed at read time.
Materialized views exist but are marked experimental and are disabled by default in recent versions; maintain denormalized tables yourself.
Example
-- Q1: find a user by email (login)
CREATE TABLE shop.users_by_email (
email text PRIMARY KEY, user_id uuid, name text, password_hash text);
-- Q2: list a user's orders, newest first
CREATE TABLE shop.orders_by_user (
user_id uuid, placed_at timestamp, order_id uuid, total decimal, status text,
PRIMARY KEY ((user_id), placed_at, order_id)
) WITH CLUSTERING ORDER BY (placed_at DESC, order_id ASC);
-- Q3: show one order with its items
CREATE TABLE shop.order_items (
order_id uuid, sku text, name text, qty int, price decimal,
PRIMARY KEY ((order_id), sku));
-- placing an order writes to orders_by_user AND order_itemsRemember
- List queries first, then design one table per query
- Each query should read exactly one partition
- Duplicate data across tables; writes are cheap
- Name tables <entity>_by_<key>
Watch out for
- Porting a normalized relational schema and then needing joins or ALLOW FILTERING for every screen.