UUIDv7 doesn't have the properties you think it does

42 points by robalex a day ago on lobsters | 12 comments

kevinmehall | a day ago

The main reason to use UUIDv7 is because ordered keys improve cache locality in b-tree indexes, which doesn't require these properties to be held strictly.

doctor_eval | a day ago

right, article doesn’t mention this at all yet it’s the main reason I use them. If Postgres could use hash indexes for pk I wouldn’t bother with v7.

daanvdk | a day ago

I don't think its entirely fair to say the article does not mention this at all. After the section about why the ordering is not strictly monotonically ordered he does mention it is still roughly ordered which is still beneficial. He just doesn't exactly explain that this is one of those benefits. I don't think the article is arguing against uuid v7, I think it is just arguing to be aware of some foot guns if you make assumptions about its properties.

[OP] robalex | 20 hours ago

Author here, this comment sums my position up well, thanks!

There was a longer version of this post that talked about Postgres in more detail, but I trimmed it out. Using UUIDv7 for Postgres B-tree write locality does not require strictly monotonic IDs, so I think it is a good fit.

Postgres is interesting to talk about because the UUID could be generated by Postgres itself or it could be generated by a (trusted) client using any UUIDv7 implementation. That sort of distributed generation makes it easier to see how offset clocks could impact monotonicity.

I also wanted to be careful as there are many Postgres-compatible databases which use distributed storage. Those may see worse write performance if UUIDv7 causes all the writes end up on a single shard, but it's implementation specific and I'd want to benchmark it. Vendors encourage developers to think of those as Postgres, but they are only Postgres-compatible and they have different low-level trade offs.

I want to move some identifiers to UUIDv7 so we can partition tables by ID rather than timestamp. We currently partition by timestamp for keeping a fixed window of data, yet cheaply drop old data without expensive vacuums. One of the drawbacks is that for lookup by a v4 UUID you have to (potentially) search all partitions to find an item, but with UUIDv7 you can benefit from partition pruning to hit only the relevant partition.

kel | a day ago

I don't think I've ever met anyone in the wild who chose UUIDv7 because they need monotonicity but I guess there are also a lot of people I haven't met

matklad | 22 hours ago

As an example, TigerBeetle recommends using something similar to UUIDv7 [1] for client-side generated idempotency IDs. To prevent double spending arising from retries at various network layers, the database must ensure that every financial transfer is executed at most once. So, a website or a mobile app generates a transfer ID locally, and the database needs to check if it has ever seen the same ID before. If you have 10 years of transfer history, that's very slow!

However, if IDs are roughly monotonic [2] (e.g., based on client's local time), then it's enough to keep, say, the last day's worth of ID in memory for the checks, which is one way to make idempotency checks fast.

[1]: UUIDv7 is specced to be big endian, which is useful when you compare a pair of ids as strings/byte slices, but we want to compare them as u128 numbers, and so use little-endian.

[2]: Because ID is client side, it is not actually monotonic, and the clock value can be completely bogus, so the time can't be trusted. For internal IDs, we use server-side strictly monotonic u64 timestamps. If you assign ID server-side (and you DB is strictly serializable), you don't need randomness component at all, and server clocks can be made reliable.

brocooks | 21 hours ago

You could use a hybrid logical clock to ensure monotonicity even when the client clock stalls or goes backwards. Well, with the caveat of knowing your stall tolerance, and adjusting the number of bits in the clock dedicated to the logical counter appropriately.

neil_f | 15 hours ago

Could you explain this a bit more? My understanding is that time protocols like NTP and PTP don’t allow for time to go backwards. Do you mean if you get two requests for the same user from different clients and the second one’s clock is ahead of the other’s so time appears to go backwards?

My understanding is that time protocols like NTP and PTP don’t allow for time to go backwards.

They absolutely do! Most obviously, if the time is wrong for whatever reason, it might need to be corrected by setting the clock back.

But also, NTP (like unix time_t) goes backwards one second when there is a positive leap second. (PTP keeps counting as usual because it uses a TAI/GPS-like timescale.) Fortunately, leap seconds have been abolished so we should no longer have bugs like these or these.

matklad | 20 hours ago

Yes, you need two clocks!

locally-monotonic but not globally monotonic purely client-side clock for end-to-end idempotency checks. It's important that idempotency ID is generated on the client, it's too late to do that when the data gets to the server, as it might have been duplicated by that time already. And you can't guarantee strict monotonicity in this case, because clients don't communicate with each other.

And once the data gets on the server, you want to stamp it with hybrid logical physical time. It's not strictly required, but it's so very useful: 64 bits instead of 128 bits, strict creation order, temporal locality, deterministic time for business logic, observability.

singpolyma | 18 hours ago

A spex of course can guarantee nothing. But a good implementation can have many of these properties