Database IDs Have No Place in URIs (2008)
johntopley.com
johntopley.com
As far as I know, no database has ever had any of the problems he mentions with IDs not round-tripping properly during backup/restore. (Please correct me if I'm wrong! But why do I think this is true? Because any database that doesn't roundtrip primary keys would break all foreign keys on backup/restore. Databases with broken backup/restore tend to either fix it real fast, or go away.)
On the contrary, it's much easier to keep IDs stable over time, than any other field. The problem with using a different field as an identifier (e.g. a URL component) is that almost any property with an objective meaning, might change over time or become non-unique. This is why it's a best practice not to use, e.g., user email as a primary key.
For instance, Stack Overflow allows users to edit the title of a post. Right now when that happens, the canonical URL changes (to include the new slug), but the old URL is easily 301'd to the new one because it includes the ID. If the numeric ID was not in the URL, you'd be stuck with either a misleading slug, or having to maintain a list of former slugs/redirects for each post. Plus, you'd have to hack your slug-generation algorithm to ensure the generated slugs to be unique, and the easiest way to do that is... by appending an incrementing ID to the end of the slug.
The point about avoiding cruft like .aspx in URLs is well taken, but unfortunately it's in direct tension with the point about keeping them stable given that "meaningful" non-cruft tends to change over time!
(And if you don't want someone to guess your metrics from IDs, you can use a random non-numeric ID instead!)
Don't know of any such either. Typical SQLite, MySQL, or PostgerSQL dump/backup is just bunch of SQL statements that re-create the content, including PKs.
You can imagine a non-native (perhaps DB-agnostic?) DB dump format that would use generated IDs for PK, rather than actual PKs. Why would one do that, I don't know.
Another possible case is using file (or memory) offset as the PK. This gives certain nice properties - one less indirection level, and object identity being pointer identity. Perhaps there's a memory image-based LISP version out there that uses such. Perhaps there's a NoSQL engine like that somewhere.
>Plus, you'd have to hack your slug-generation algorithm to ensure the generated slugs to be unique
...or you put the moderators to the task of ruthlessly stamping out duplicate questions. There was a popular website like that, oh wait, that's the Stack Overflow ;-)
[edit]
I've just remembered that PostgreSQL's ctid and SQLite's ROWID are physical identifiers (offset-like identifiers) of records. They are (to certain extent) exposed to userspace and could be misused as IDs in URLs. As physical identifiers (offset-like), they are naturally subject to change upon dump/restore, vacuum, other maintenance tasks, etc.. Clearly those should not be used as end-user visible IDs in URLs.
You might not use foreign keys, but still have autoincremented primary key.
Database ids on the URL are also a security concern.
They shouldnt be. Users should only be allowed access to their own records, even if they know the id's of records belonging to other users.
What performance cost there is (and in my experience it is not much) is because a UUID is twice as large as a bigint. And there are situations where that matters. But not that many of them.
postgres[7878][1]=# SELECT amname, pg_indexam_has_property(oid, 'can_unique') FROM pg_am WHERE amtype = 'i';
┌────────┬─────────────────────────┐
│ amname │ pg_indexam_has_property │
├────────┼─────────────────────────┤
│ btree │ t │
│ hash │ f │
│ gist │ f │
│ gin │ f │
│ spgist │ f │
│ brin │ f │
└────────┴─────────────────────────┘
(6 rows)
And postgres' btree indexes definitely are affected by the randomness in many UUID generation schemes.Looks like I have a conversation to have. Thanks. :)
Yes, in fact - even then. There are fascinating historical stories about lay-people who scoffed and then failed to account for the dark art of statistics. https://www.statisticshowto.datasciencecentral.com/german-ta...
That sounds like they wanted to migrate to another cluster, and wanted to have both clusters running at the same time (for testing) without risking an ID overlap, so they turned the least significant bit of the ID into a cluster identifier. The older cluster was 0 (even numbers), the new cluster was 1 (odd numbers).
More so than in mark up? If so why?
Should surrogate keys never be exposed to users in any form? Then what's the alternative? Nonces for everything, all the time?
Easy to enumerate, if that's a concern.
1. Mutable data (looks nice but can change)
2. Incrementing data (looks nice but can be guessed and measured)
3. Random looking data (immutable and unguessable but ugly)
Guids aren't the only option for 3 and some of the others may not look as bad, but it still seems the safest option. Any other compelling reasons to not use guids?
IDs not round-tripping properly during backup/restore
Oracle has a "ROWID pseudocolumn" which uniquely identifies each row.(I know this because I once had to fix a database table with no primary key, and some rows where every field was identical. The rowid lets you delete one out of two identical rows. So it's not an entirely useless feature)
If you had an inexperienced developer, and no-one reviewing their work, and they heard the rowid is the fastest way of looking up a row - faster even than the primary key - so they decided to use that in URLs, that would be tremendously inconvenient.
Apparently you can't use AUTOINCREMENT on tables using WITHOUT ROWID either.
Simple example - neo4j. Node id's are reused when deleted and you can't create node with id you want. Documentation mentions this. We've just used unique random strings as keys.
I was relying on this just yesterday. We use graphenedb for our production database and I needed to load a production backup locally for testing. It was really useful that I could query a node by id both locally and on prod and get the same result.
https://stackoverflow.com/questions/18692068/will-mysql-reus...
The OID is generated from a sequence shared by all databases in the "cluster" i.e. the PostgreSQL server instance. So it is not only non-deterministic based on insert orders to your DB, but also is affected by inserts in other databases on the same server.
As I recall, OIDs only ever came up in practice if you wound up wrapping the 32 bit uint.
The original goal for OID was a bit like UUIDs, but just at the cluster level rather than truly global. Someone steeped in that background and focused on "tuning", where you would consider things like storage consumption and index efficiency for int4 vs int8, might well have baked OIDs into a URL while dismissing the uuid type as extravagantly expensive.
There's NO NEED for a primary key to be an INTEGER or any sort of ROWID.
I agree with TFA -if this is what it says- that one should not put INTEGER PRIMARY KEY values in URIs -- it's useless to users. Just make the "slug" a column, make a NOT NULL and UNIQUE, disallow changes, and use that. You don't have to make the slug a PRIMARY KEY -- NOT NULL and UNIQUE has the same semantics as PRIMARY KEY.
Surely TFA wouldn't argue to have no DB key at all in URIs, so I won't bother with that possibility. And yes, I should read TFA...
A small number in a URL isn't going to confuse anyone.
Well, you need what is called a https://en.wikipedia.org/wiki/Surrogate_key.
Surrogate keys need to be made up from whole cloth, apart from the natural data (because if they are derived from any of the natural data, they'll become incorrect when the data changes.) And what's easy to make up from whole cloth? Sequential integers. (Or random numbers, which—if you want to guarantee no collisions across a distributed DB cluster—gives you ≥128-bit random numbers, i.e. UUIDv4s.)
But now the slug in the URL of the published article (about party B winning the election) is still "party-a-wins-election"!
There are thousands of use-cases just like this. For another example, someone might have a real claim on a libel suit if you assert something false about them in a URL slug in a link hosted on your own site, even if it points to a page that says only true things about them! ("People don't always click through to find out what the actual article is about", their lawyer would argue; "sometimes they just read the URL itself." The judge would agree, being just the sort of person who is too busy to read everything that crosses their desk, instead triaging the importance of things [read oneself, delegate to their legal analysts, forget] by just such sloppy criteria as looking at the URL slug.)
This is why, in CMSes, you need to always be able to exert editorial control over every part of a page, including the URL. The slug, just like all other parts of the page, must be mutable. And so you can't use the slug as the primary key.
("But what if you just delete posts with bad slugs and make new posts to replace them?" Not always possible; in a CMS for a publisher with a hard-core editorial flow, often the "post" you see is just a view onto the same work-object that passes through the rest of the CMS process of edits and approvals, and you can't just delete the work-object and make another one, without getting whoever assigned you the job to cancel your assignment and re-assign it to you. Heck, often at time-of-assignment you don't even know what you're going to use as the headline. How should a system assign a slug for that?)
(And, while, yes, you could restructure things so as to decouple content-asset objects from business-domain objects, such that it's the content-asset object that has a slug, and maybe content-assets never change at all, just get new versions of them published with the previous versions hidden, with some router using rules to determine to which content-asset a given slug should route... at that point you're talking about the sort of Event Sourcing architecture which people on HN like to fantasize about but which 99% of businesses refuse to implement for the same reason they don't want to use unpopular programming languages: it makes it far harder to hire Joe Average Java-School Dev to maintain the code.)
1. Numeric based Primary Key Lookups are some of the fastest operations on modern databases. 2. "Slugs", are a set of strings, which occupy more memory, space, now you have to come up with some way to make them unique. If you are building a CMS, sure, SEO is important. If you are building a line of business application, no. 3. Just checked, Google and Facebook expose numeric based identifiers. My Facebook Business ID is numeric, my Google Analytics ID is numeric with a prefix on it. 4. There are plenty of libraries out there that will take a numeric value, convert it to a small alphanumeric value with a check digit. 5. No end user cares about URLs (except for SEO). Your business user is not going to navigate your application by going to myapp/settings, they are going to click on the gear icon.
It's pretty useful to me when scraping/syncing data from a website. I'm also a user.
Many applications have meaningful unique identifiers, creating a unique slug is not always a "hack". Any app that displays events that occurred in the past often have unique identifiers. Examples include software logs, email, government records, financial transactions, i.e., anything that displays past events.
These records are inherently "write-once", and the primary key can be something more meaningful than an ID. These may be just a subset of apps out there, but in these cases there is a strong argument for removing any ID in a URL.
If you don't, then your foreign keys will be broken.
> If you have to restore the site’s data from a backup, can you guarantee that those database IDs will remain the same?
Ditto. If they're not, then that backup seems broken.
> If you have to switch to a different database server (say from Microsoft SQL Server to Oracle), can you guarantee that those database IDs will remain the same?
Ditto.
I've only had one situation where I had to change the database ids, including making sure to change all foreign keys accordingly in all tables, and that was when I was migrating multiple company-hosted instances of a webapp they had in their intranets to a unified internet-hosted server. I was merging multiple databases into one so modifying ids was necessary so they wouldn't clash.
At least in my case, I don't think any of those companies' employees would have had the expectation for their intranet URLs to work. I mean, even the host/domain part of the URI changed. Why would they have the expectation for the path part to still work?
If user a has private document 1234 at /doc/1234 and user b has private document 1235 at /doc/1235
when user a goes to /dec/1235 IT SHOULD NOT RETURN THAT DOCUMENT.
Who in their right minds considers obscurity to be a replacement for authorization?
If a random anonymous individual can access private data by hitting a url, the problem is not the id used in the url.
I have no reason to enforce uniqueness of the name of things, but I MUST do this because some jerk said so in 2008?
As a matter of fact, it should return 404 NOT FOUND for both users A and B
> user b has private document 1235 at /doc/1235
So, why do you say it should return a 404 NOT FOUND for user B?
I mean really?
This way you can change the record's UUID at any time to display different data without having to worry about updating a bunch of internal foreign keys. You don't have to worry about a user making easy edits to the UUID in the URL to find nearby records (although you still need real protections if you need to restrict data by user).
https://docs.microsoft.com/en-us/sql/t-sql/functions/newsequ...
Note that the endianness may catch you by surprise, though. For example, what's good in postgres may not be good for SQL server.
Access to resources is either locked down by an authorisation policy, or sometimes you need open resources (like a public share link), in which case, security through obscurity (with perhaps a secondary string on the record acting as an additional key for non-logged in users).
They're also much harder to pass around in cases where you need to refer to a specific record, for example if you've got someone in tech support who needs help with a particular object its much easier to say "its record 18434" than it is to try and read out a UUID over the phone.
That is true, but changing the UUID is also likely to break a lot of assumptions that your users make. If your user indexes your forum posts, for instance, and you change the UUIDs, they'll re-index posts with new UUIDs as new posts, and send end users to now invalid UUIDs until the old index entries are evicted.
I've gone the route of a random number only (similar to a UUID but encoded in a way similar to Youtube video IDs). I used to do the auto-incremented integer but once I got into database sharding, maintaining that global counter started getting a lot more complicated.
This lets me have inserts going to several databases without worrying about the counter. (Of course, as with any eventual consistency method, there's a mechanism to ensure no duplicates exist, however unlikely they are.)
Actually, URIs are opaque. The parts of an URI are implementation details - even the protocol, domain and port. To the user, changing database IDs, that happen to be part of the URI, is no different to changing the domain, port number or the protocol.
Whenever I get into a discussion about "URI design", I state two rules:
1) Care about the URI design as much as you want. But never expect from someone else - especially the client - to make sense of that design.
2) When necessary, handle URI changes in a way that does not break the client (redirects are your friend).
Btw. For this reason, browsers tend to remove the address bar from the UI (as much as possible).
In none of those situations will new entries necessarily follow the same pattern, i.e. there may be huge gaps or it might start filling in deleted questions so entries are "out of order", but those are separate problems and also remedied by using your schema correctly.
In short: this post is very misinformed and I hope that it's presented for that purpose and not taken as serious advice.
This. The answer to 3 question in article is always "YES".
> The slug itself can easily be automatically generated when a new question is saved. Then you can simply retrieve a question by its slug.
And that was just pure ignorance. What happens to slug when question(or product) is edited?
The biggest real world concern I've ever had with embedding incremental IDs into URLs is that incremental IDs can leak information, like how many records you have and how quickly that's growing. Depending on your business, you may want to obfuscate that.
All of these problems go away if you just assign random numbers instead.
(And I know I'm asking a rhetorical question.)
For example:
for i in range(25):
167535774 ^ i
167535774
167535775
167535772
167535773
167535770
167535771
167535768
167535769
167535766
167535767
167535764
167535765
167535762
167535763
167535760
167535761
167535758
167535759
167535756
167535757
167535754
167535755
167535752
167535753
167535750
for i in range(250000,250025):
167535774 ^ i
167752718
167752719
167752716
167752717
167752714
167752715
167752712
167752713
167752710
167752711
167752708
167752709
167752706
167752707
167752704
167752705
167752766
167752767
167752764
167752765
167752762
167752763
167752760
167752761
167752758 ALTER SEQUENCE users_id_seq RESTART WITH 54321;
"Wow, this service is probably mature, well-tested and reliable!"And if you want to keep using integer IDs internally, put the GUIDs into an alternate key.
I'd file this under: things to not worry about.
If it's a business service then the operation is not the end goal but the means to the end.
If it's a business service then the question you should be asking is "should I care that my competitors are able to observe and monitor my service metrics?".
Quite often, businesses do care a lot.
For example, certain kinds of attacks are helped with sequential IDs. Wasn't there an AT&T "hack" where the attackers iterated URLs over sequential account numbers to grab nearly all data? Would've been nigh impossible to perform with UUIDs as identifiers.
About the only use case I can think of where sequential IDs are helpful is using MAX(PRIMARY KEY) to roughly estimate total number of records. And that's only relevant where there's special cased support for MAX(PRIMARY KEY). Think MySQL, but not PostgreSQL. And that's also subject to the over-estimation due to holes left in the sequence, whether due to deleted records, or backed off transactions.
On the other hand, they can cause latch contention on INSERT-heavy tables (because of the competition for the last B-Tree leaf), though this is not common.
Not the worst thing to happen, but at any serious scale you take that into account.
OTOH, storage for binary UUID representation is just 16 bytes. You get non-sequential big big ints. Possibly use UUID v1, with sequential (time based) trailing part.
I agree that using slugs alone is probably not a good idea (simply because they are not stable). They are there to make the URLs friendlier to search engines, but are strictly redundant (and therefore safe to ignore during URL request routing).
> any identifier other than the Primary Key
Well, an index can be covering, for the price of more storage and more expensive index maintenance.
But even if the index is not covering, the additional ROWID lookup (or clustered index seek) is likely to be so fast as to be drowned-out by other factors.
> OTOH, storage for binary UUID representation is just 16 bytes.
Times number of rows in that table and every other table that references it, which may or may not be significant.
>Times number of rows
Still merely twice as much as typical int64 PK, which isn't terrible. Meanwhile - unless the programmer has been exceedingly careful - much more space is wasted on column (field) alignment in records.
Frankly I'd sooner worry about PK index size here; the larger, the more I/O is necessary on average for look-up, for insertion, and for maintenance tasks. RN I don't remember if typical BTree index size is strictly proportional to field size, or if it's optimized away to any extent, perhaps via prefix coding.
Hmmm... I don't think there is much alignment going on within the data pages. Efficiently packing data is a matter of avoiding excessive I/O and I can't imagine databases were not carefully optimized in that regard, especially at the time most of them were invented (with less than a hundred I/O operations per second on that hardware).
Plus the DBMS is free to rearrange the physical order of fields as it sees fit (e.g. MS SQL Server coalesces boolean fields into a bitmap).
But I'm not an expert on physical row structure, so I might be pulling this out of my backside. :)
> Frankly I'd sooner worry about PK index size here;
Yes. And how many indexes are there.
> if typical BTree index size is strictly proportional to field size
Also the fill factor (i.e. how much free space is left in the B-Tree nodes after splitting). Sequential values can have a big advantage here due to 90/10 optimization I mentioned earlier.
BTW, some DBMSes can encode integers in variable format, so the actual values matter, not just the nominal "column width". The same is of course true for varchar, but also for char (typically). The char(n) is just a logical concept, it doesn't require n characters of storage unless the value is actually n characters wide (unless you are still working in Clipper ;) ).
> or if it's optimized away to any extent, perhaps via prefix coding
Prefix compression is supported in some DBMSes but not others (e.g. Oracle does it via CREATE INDEX ... COMPRESS).
>Hmmm... I don't think there is much alignment going on within the data pages.
Depending on the RDBM engine, the atom fields (INTs, FLOATs, etc.) in storage are aligned to multiplies of their sizes. This also applies to fields that are stored as pointers to external data - TEXT, BLOB, etc. get stored like that; either in all cases, or when content length exceeds a threshold.
Not sure why that's done, but probably to help the CPU handle data read/update at full speed and with atomic operation. Or perhaps just to make the format portable to platforms that expect memory accesses to be aligned.
Example: given a record of (INT8, INT64), common RDBMs will waste 7 bytes - padding the first field (INT8) so that the second field (INT64) is aligned to its natural alignment. Thus the physical order of columns matters. And thus my remark on "exceedingly careful". Example for PostgreSQL: typalign [0]
MySQL and PostgreSQL diverge here: when you re-order columns, MySQL normally adjusts only the map of fields, keeping the physical structure unchanged, while PostgreSQL doesn't[1] support such quick change; you either add the new column(s) at the end of record, or re-write the whole table from scratch. This may seem more cumbersome, but gives explicit control over the record's physical format.
[0] https://www.postgresql.org/docs/current/catalog-pg-type.html
[1] last time I checked was 9.x, may have changed since then.
Of course, that short numeric ID doesn't necessarily need to be part of the DB's primary key.
http://stackoverflow.com/questions/13204/why-doesnt-my-cron-... (still works)
http://stackoverflow.com/questions/21064/massive-reputation-... ("this question was removed from stack overflow for reasons of moderation", still available: https://i.imgur.com/Csy94IL.png)
and with integer keys the worst that can happen is that the sequence is not in the backup so after a restore you need to have the p sequence restart from the previous max id + 1; meanwhile with slug built off user entered data a change in collation can really screw up your day, unless you want to mangle all texts into basic ascii, and SO doesn't https://stackoverflow.com/questions/41102371/sql-doesnt-diff... (see the ü in the link more than the question itself)
http://stackoverflow.com/questions/13204/why-doesnt-my-cron-...
Article gives only cons. What are the pros?
https://stackoverflow.com/q/13204
Shorter, easier for users to work with. Could be even shorter with a domain hack. Does Stack Overflow have one?
If the ID comes from something like a SERIAL column, then yes, you can port those easily. I think every database system has something like SERIAL, as well as the ability to control an initial value, taking care of portability concerns.
SERIAL has other problems, namely predictability, so another possibility is UUIDs. And those should be pretty portable too. I know DBMS UUID implementations are different from OS implementations, but the risk of collision should still be vanishingly small.
And if you don't use IDs, then what's the choice? If the URI contains an article name then what about article names being changed? What about duplicate article names?
You also can not change the slugging algorithm if you want to keep bookmarks valid
The algorithm flexibility might not be a big problem either. The old slugs are stored in the database. Update the algorithm, new slugs adhere to the new algorithm, old ones unchanged, everything still works.
2) There is already an index on ids assuming they are used as primary keys, why adding another index (on text)?
3) To support changing slugs (titles) you will have to keep all the old slugs as well (to redirect them to their new versions). With ids you don't have to do this - ids have no reason to change.
In the end you might not feel the difference in execution time, but hardware requirements for servers...
I agree that they are annoying, but when the only reason against them is to improve URLs aesthetics (assuming they don't pose a security/information disclosure risk) - the trade of performance, hardware requirements and extra code needed is not worth it IMHO.
Which basically means a join in the database. While not terribly expensive, its a lot more involved than a simple column value check.
(Apparently this was used by militaries in WW2, to estimate the total number of tanks from the serial numbers printed on them.)
The tricky case is something like email drafts: it's possible for a user to create and then save a draft with no addressee, subject or content. In that case there is little alternative but to make the system use a surrogate key (a database internal key) because the other possible unique identifying values (e.g. time of creation of the draft) are sort of unsatisfactory, because they don't really have a strong, meaningful connection with the content of the item.
I’ve never heard of IDs being something mutable. When would that ever happen?
What was Atwood's calibre at that point? (pre StackOverflow)
But also the way letterboxd and also quora deal with URLs is admirable but it needs special care (at least in the case of quora)