- Avoids people being able to just iterate through records, or to discover roughly how many records of a thing you have
- Allows you to generate the key before the row is saved
I think I default to auto-increment ID's due to: - Familiarity bias
- They have a temporal aspect to them (IE, I know row with ID 225 was created before row with ID 392, and approximately when they might be created)
- Easier to read (when you have less than +1,000,000 rows in a table)
I agree and think you're right in that UUID's are probably a better default.Though you can never find a "definitive" guide/rule online or in the docs unfortunately.
UUIDv7 (currently a draft spec[0]) are IDs that can be sorted in the chronological order they were created
In the meantime, ulid[1] and ksuid[2] are popular time-sortable ID schemes, both previously discussed on HN[3]
[0] https://datatracker.ietf.org/doc/html/draft-peabody-dispatch...
[1] https://github.com/ulid/spec
[2] https://github.com/segmentio/ksuid
[3] ulid discussion: https://news.ycombinator.com/item?id=18768909
UUIDv7 discusison: https://news.ycombinator.com/item?id=28088213
We ended up implementing UUIDv7 in our ID generation library https://github.com/MatrixAI/js-id. And we have a number of tests ensuring that it is truly monotonic even across process restarts.
See IdSortable.
(And remember, the values are in different tables, so the only disadvantage is that your millionth user has ID 2000000. To the trained eye they look like an invoice; but there is no way for the system itself to treat that as an invoice, so it's only confusing to humans. If you use auto-increment keys that start at 1, you have the same problem. Account 1 and Invoice 1 are obviously different things.)
I agree in principle, but then you have Windows skipping version 9 because of all of the `version.hasPrefix("9")` out in the world that were trying to cleverly handle both 95 and 98 at once. If a feature of data is exploitable, there's a strong chance it will be exploited.
Re: the upper bound: if you reach a million customers, you have lots of other nice problems, like how to spend all the money they earn you :-)
You could use "is_test_account" or whatnot, but that adds an unnecessary column (for ~99% of records)
I like this -- will keep it in mind!
Positive IDs were home-grown, negative ones were Unique Feature Identifiers from some old GNS system import (from some ancient US Government/Military dataset).
Then when something had to be edited/replaced/created it would possibly change a previously negative ID to positive, fun times.
There are UUID variants that can work well with indices, which shrinks the case for big-integers yet further, to micro-optimizing cases that are situational.
Of your disadvantages... if I wanted to know when rows were created, I'd just add a created timestamp column.
But "easier to read" is for real -- it's easy when debugging something to have notes referencing rows 12455 and 98923823 or in some cases even keep them in your head. But UUIDs are right out.
And you definitely don't want to put UUIDs in a user-facing URL -- which is actually how you get around "avoids people being able to just iterate through records", really whether you have UUIDs or numeric pks I think putting internal PK's in URLs or anything else publicly exposed is a bad idea, just keep your pks purely internal. Once you commit to that, the comparison between UUIDs vs numeric as pks changes again.
Why?
Both as a user and as a dev I love that I can just change the PK to get to a specific post, item, whatever instead of changing the whole link.
It's considered undesirable because it's basically an "implementation detail", it's good to let the internal PK change without having to change the public-facing URLs, or sometimes vice versa, when there are business reasons to do so.
And then there are the at least possible security implications (mainly "enumeration attacks") and/or "business intelligence to your competitors" implications too.
But it's true that plenty of sites seem to expose the internal PK too, it's not a complete consensus. Just kind of a principle to separate internal implementation from public interface.
Here's a short 2008 HN discussion on it, in which people have both opinions: https://news.ycombinator.com/item?id=19876901
Here's a more recent and lengthy treatment:
So if you have concurrent updates which are correlated with row insertion time, random keys can be a win. On the other hand, if your lookups are correlated with row insertion time, then the relevant key index pages are less likely to be hot in memory, and depending on how large the table is, you may have thrashing of index pages (this problem would be worse with MySQL, where rows are stored in the PK index).
(UUID doesn't need to mean random any more though as pointed out below. My commentary is specific to random keys and is actually scar tissue from using MD5 as a lookup into multi-billion row tables.)
Ofc, your app performance requirements might vary, and it is objectively true that UUIDs aren't ideal for btree indexes.
Unless you are using a time sorted UUID, and you only do inserts into the table (never updates) avoid any feature that creates a BTree on those fields IMO. Given MVCC architecture of Postgres time sorted UUID's are often not enough if you do a lot of updates as these are really just inserts which again create randomness in the index. I've been in a project where to avoid a refactor (and given Postgres usage was convenient) they just decided to remove constraints and anything that creates a B-Tree index implicitly or explicitly.
It makes me wish Hash indexes could be used to create constraints. They often use less memory these days in my previous testing under new versions of Postgres, and scale a lot better despite less engineering effort in them. In other databases where updates happen in-place so as not to change order of rows (not Postgres MVCC) a BRIN like index on a time ordered UUID would be often fantastic for memory usage. ZHeap seems to have died.
Sadly this is something people should be aware of in advance. Otherwise it will probably bite you later when you have large write volumes, and therefore are most unable to enact changes to the DB when performance drastically goes down (e.g. large customer traffic). This is amplified because writes don't scale in Postgres/most SQL databases.
1. because PRIMARY KEY is its own constraint, and the underlying index is not under you control
2. because PRIMARY KEY further restricts UNIQUE, and as of postgres 14 "only B-tree indexes can be declared unique"
1. The UUIDs are generated in increasing order, so none of the b-tree issues others have mentioned with fully random UUIDs.
2. They're true UUIDs, so migrating between DBs is easy, ensuring uniqueness across DBs is easy, etc.
3. They also have the benefit of having a significant amount of randomness, so if you have a bug that doesn't do an appropriate access check somewhere they are more resistant to someone trying to guess the ID from a previous one.
I beleive the word you are looking for is "ought". :)
Wikipedia also adds: "the probability to find a duplicate within 103 trillion version-4 UUIDs is one in a billion."
Sort of the distinction between unspecified behaviour and undefined behaviour in C.
You need some UX like an error message in a red rectangle or something.
the solution might as well just be not to care about this case (no sarcasm).
Your application might not care about treating this error, but the DB will report it.