For your options:
- create the main table with PRIMARY KEY (service, user) and create materialized view with PRIMARY KEY (service, user, updated_at). But
this will hurt performance.
Materialized views do not really hurt performance much, and write path is very fast so I wouldnt worry about that but there are a lot of issues with MVs currently and marked experimental for a reason - I would not recommend them or you will face lots of consistency issues in current versions.
- create the table with PRIMARY KEY (service, user) and read with full
consistency before write to check that older update is not written.
But this gives up availability and an anti-pattern for Cassandra.
Maybe I am missing some requirement you havent explained but you dont need to do a read before write. This seems to me like by far the best solution to me. Just when you have an update push the change to the (service, user) table, then when you read from the table you get the latest update per user. Theres always IF EXISTS or IF clauses on your insert/update using paxos as well.
If you need the history (not just the latest) and you dont want a second table you can use group by:
CREATE TABLE state ( // simplified a little
service int,
user int,
updated_at timeuuid,
data text,
PRIMARY KEY (service, user, updated_at)
) WITH CLUSTERING ORDER BY (user ASC, updated_at DESC);
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 1, now(), '1');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 1, now(), '2');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 1, now(), '3');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 2, now(), '1');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 2, now(), '2');
INSERT INTO state (service, user, updated_at, data) VALUES ( 2, 1, now(), '1');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 3, now(), '2');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 3, now(), '3');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 3, now(), '1');
INSERT INTO state (service, user, updated_at, data) VALUES ( 1, 3, now(), '2');
SELECT * FROM state WHERE service = 1 GROUP BY service, user;
service | user | updated_at | data
---------+------+--------------------------------------+------
1 | 1 | 7c2bd900-981e-11e9-a27a-7b01c564a3f0 | 3
1 | 2 | 7c2d1180-981e-11e9-a27a-7b01c564a3f0 | 2
1 | 3 | 7c88c610-981e-11e9-a27a-7b01c564a3f0 | 2
Its not amazingly efficient or anything but it will work providing you never let a single service partition get too large. I would actually strongly recommend adding a date component/bucket to it like:
CREATE TABLE state (
bucket text
service int,
user int,
updated_at timeuuid,
data text,
PRIMARY KEY ((bucket, service), user, updated_at)
) WITH CLUSTERING ORDER BY (user ASC, updated_at DESC);
where bucket is a YYYY-MM-DD string (or YYYY-WEEKOFYEAR or something). Then just around boundary times you query both current and last bucket. Otherwise the partitions will grow until they cause issues.