So you'll want to build this model as a time series with a time component "bucket" to keep the partitions from growing too large. To do that, we'll need one more column: month_bucket.
The "bucket" column is dependent on two things:
- Your business logic. Do you typically query for data within a certain month? Or week? Or does the job process it fast enough that you really only need to worry about the current day?
- Number of rows written over time. Cassandra has a cells/per-partition limit of 2 billion, size of 2GB, and the partition will become unusable long before those hard limits. I recommend keeping no more than 20k-50k rows in a partition, and less than 10MB.
The point, is to take this example with "a grain of salt." A different time bucket may make more sense for you (week, day, etc.). I've built models using a bucket based on hour before, because of the expected write throughput.
So month_bucket will be the partition key, and we'll cluster by create_date in ASCending order. That way, all the most-recently created rows will be at the "bottom" of the partition, and you'll have an easier time with queries like "get the oldest 100 w/ a certain status" (which will read from the "top" of the partition).
To query by status_code, you'll need a secondary index on it. Yes, I'm sure you've read "secondary indexes are bad." And that's true. But, in certain cases with (what I call) "middle of the road" cardinality, they are ok. They are particularly useful with a finite amount of options (processed, received, etc.) on a column which can change (can't change the values of primary key components). But with the queries you describe, you'll be using a secondary index with the partition keys, which will limit their "badness" significantly.
In short, I'd build the table like this:
CREATE TABLE inbound (
request_id uuid,
payload text,
status_code text,
tryNumber int,
update_date timestamp,
create_date timestamp,
month_bucket int,
PRIMARY KEY (month_bucket,create_date,request_id)
) WITH CLUSTERING ORDER BY (create_date ASC, request_id ASC);
CREATE INDEX ON inbound (status_code);
With this PRIMARY KEY definition, all rows will be stored by month (ex: 202110), ordered by create_date, and with request_id to help ensure uniqueness.
My only concerns here, are around the use of tryNumber:
- Cassandra doesn't overwrite. It stores obsoleted values for up to 10 days. Too many updates, and there will be a loooooong list of obsoleted values that Cassandra has to sort through, not to mention read from multiple files on-disk. But if the idea here is that it typically only gets updated once or twice, it'll be ok.
- Cassandra doesn't provide ACID transactions, which makes multi-threaded operations all doing read-write-read-write a bit of an anti-pattern. There's no way to "lock" a value to prevent it from being updated by another process (race condition). Ex: the job reads a
tryNumber of 0 and attempts to set it to 1, but another process sets it to 1 before that happens. Just something to be aware of.
With this model, queries like this should work just fine:
SELECT * FROM inbound
WHERE month_bucket=202110 AND status_code='RECEIVED'
LIMIT 100;
Or:
SELECT * FROM inbound
WHERE month_bucket=202110 AND status_code IN ('RECEIVED','FAILED')
LIMIT 100;
Again, with the rows in ascending order by create_date, this should give you the oldest 100 with a particular status. Cassandra does't have an OR keyword, because that incurs "random" reads, and Cassandra was built for "sequential" reads. Using IN is a way around that, but use of IN comes with the same precautions that secondary indexes have (it's not too bad if you're using it while filtering on a partition key).