Ask HN: Do you use foreign keys in relational databases?
I use foreign keys quite often in my schemas because of data integrity, while my colleague has a no FK policy. His main argument is difficulties during data migrations which he frequently encounters. He rather have a smooth data migration process than having an unexpected error and abort the whole operation that the migration is only a small part of. I suspect the errors might be mainly caused by not considering data integrity at all at the first place, but I can feel his pain. To be fair, as far as I know, he never had major data problems.
He is not the only one I've met who insisted on not having FK. I've even seen large systems prohibit using JOIN statements.
Personally, I see the data integrity out weights the inconveniences, do you use FK for your systems, what are your experiences?
250 comments
[ 3.7 ms ] story [ 284 ms ] threadWhy? Fks don’t require unicity. Or do you mean that you target non-unique columns? That sounds… horrendous.
But regardless sounds like something you’d want to resolve before declaring the migration finished.
[Emphasis added -- CRC.]
But that's a good thing, not a bad one -- it shows you you've made a mistake! Might even be you wouldn't have caught that mistake at all otherwise. How could it not be better to catch a mistake right as you made it and are still in the maintenance window to fix it, than to have it lingering undetected, getting ever more corrupt data, until it rears its head possibly years later, when some vital report shows your DB is full of garbage?
I'm remembering the time I was working at a place that had a huge number of Microsoft Access, Microsoft SQL Server and mysql databases and I was the first person they'd hired who knew how to do joins and they thought it was pretty scary.
In a strict relational model implementation, the only way to reify that is by the FK of the Order in the Line Item, but in some other implementation, say a generic programming language, the Line Items might be an array or similar data structure that is part of the the Order, and the programming language implementation keeps track of the pointers or address offsets or whatever detail it cares about.
Anyway that's all beside the point.
In my experience, if the IDs are not autogenerated by the relational system, then it's relatively easy to migrate, however when data is full of IDs defined via some AUTO_INCREMENT behavior then migrations become an awful mess and any system (and this happens more frequently than you might expect) where a specific ID starts to have semantic meaning (oh, the customer's ID is 387437. whoops) then all bets are off and you might as well just give up and accept that your auto-generated IDs are now fixed for all time and can't be changed.
Oh and just to add, I have notebooks from previous employers where I have written THE Important IDs to Know, which started life as auto-generated numbers but which now are enshrined and encased in acrylic to the extent that years later they are important tribal knowledge.
Fundamentally it's a question of how one should go about handling multi-tenant situations.
It sounds like your colleagues want NoSQL (key-value) given their no JOINs policy.
This example makes it pretty clear: https://en.wikipedia.org/wiki/Finitary_relation#Example
I always thought it was referring to foreign keys too. Pretty bad name in hindsight.
I couldn’t make up a more misleading name if my life depended on it..
EDIT: Reading the page in more detail, it seems to me relational means both. Indeed each field of a row has a relationship (they belong to the same row after all), but there are other kinds of relationships like one-to-many achieved by foreign keys.
Eg.: each row of a class table corresponds to a class, and a class corresponds to multiple students, so the relationship between the class table and the student table is "one to many"
When building systems for scale where the databases may grow large, foreign keys can cause many issues -
- ORM features around foreign keys can easily bring your system down when joining large tables with incorrect/missing indexes during heavy loads
- As the table grows, not having foreign keys makes it simple in taking out large tables into big-data solutions in the future
- The schemas and relations are sometimes hard to understand during the initial phases of application development. Not having those relations makes changing schemas simpler and faster.
- Sharding tables is much simpler when there are no foreign keys
- It helps to add some of the reference logic in the application rather than the database. Databases are the bottlenecks when it comes to IOPS and scaling. The more processing you move to your application server, the better scalability you can achieve.
Then it's up to all your apps to guarantee that they fill those fields in correctly, and do the reference check when inserting or updating rows in the dependent table.
You also need to write checking programs that report foreign key errors on a timetable that suits (every minute, every hour, every day, whattever).
Report the errors to someone who understands the significance of them, and can fix them.
alter table ${table name} disable constraint ${constraint name};
There are a few different schools of thought. I will list them, but the important thing to remember is not to be dogmatic. They are all right or wrong depending on your circumstance.
One school of thought says "I want all data in my DB to be normalized. I want it to be right when it goes in so it never breaks the application layer." That school would say foreign key constraints are critical.
Another school of thought says "I want all the data in my DB to be retrieved and inserted quickly. I want the application layer to do any error-checking that is necessary, or, I want to be in a situation where I can always fail gracefully if there's errors in data validation."
Still another school of thought says "I don't trust those programmers to write good application code, so I will insist on normalized data for that reason," and yet another says, "I don't have control over the DB schema, that's some DBA's job, so I will just do all my validation in-app."
The point of this being, there's tradeoffs either way you go.
Personally, I typically would rather have the application layer do the validation and even the joining of data, a lot of the time, when the application is high-volume. At the volumes my organization works with, it is very difficult to write performant SQL queries that use JOINs and other relationships as a developer - even as a DBA! - and often much easier, for me, to write performant application code. The DB is also a pet with many owners, whereas the infra for my applications is owned by my team. So, it's better for me to do relationship validation in code myself. (We also do not use a heavy ORM, again for performance reasons. Just Dapper.)
At my previous job, the situation was the opposite - we weren't under such load at most times that it mattered if the queries were performant, we had Entity Framework building relationship, and EF will blow up if you ask it to build relationships where none exist. So, we needed more normalized data, and that was what we went for. But even then, not in every situation.
How can this possibly be true? Won't that result in sending unnecessary data over the wire, stressing network and SQL buffer?
What are these queries and what are these volumes? I just can't wrap my head around the performance statement. You know better join algorithms that SQL Server is capable of (Loop/Hash/Merge)? Given that you have appropriate indexes in place, perhaps query hints is what you need to control sql plan guides if you know a thing or two about your data and it's distribution more than the sql statistics.
If they do it in application code, then they probably ought to learn about fancy sorting and joining algorithms.
But they should really just do it in the database (using read only replicas if the load gets high).
The database can only do so well (and will spend a lot of CPU cycles working on your crazy query plan, because getting it wrong is more expensive, so now you effectively limit capacity regardless of how good your storage engine is).
Joins are great, tons of research went into making joins work, and lots of different join algorithms and optimizations based on data sizes, indexes, etc. But you really have to be careful, verses just denormalizing data across multiple tables/collections. Most applications are read-heavy, anyway... I generally plan for things to be successful, in which case joins don't usually work in the hot path.
Of course each scenario is different, YMMV, and as always "it depends".
I think the point of the GP is that all of these application instances are still connected to the DB, doing sub optimal data fetches taxing the database in multiples.
Also some ORMs write dreadful SQL where it comes to joins
A badly written join (or collection of joins) will take a longer time to run that will, when the system is under load, backlog other queries. If these errant queries make up a significant portion of your queries then it will hit performance significantly
It's not the joins themselves just the incorrect use of them
A lot of people have the same concern but I'm just gonna reply to this comment.
The ratio of SQL-focused devs to non-SQL-focused devs at my org is not favorable. And we certainly DO write joins... just not complex ones. Likewise, we do use constraints... just not all the time against multi-billion-record tables.
But that's not all. Our biggest tables are also our oldest and most unwieldy. Here is an (admittedly outdated in the specifics) example of what it's like to add constraints to a big table in SQL server that didn't have them already: https://social.msdn.microsoft.com/Forums/sqlserver/en-US/3eb...
So for a dev to come along and add a whole bunch of FK relationships and/or write some big fun queries against one of these tables is asking for a lot more than one realizes immediately. New devs join and run up against this all the time.
Is it a good or great situation? No. But that's not the question. The question is is it a real one and why.
And SQL can do parallel operations too.
Leaving aside my initial snark reactions¹ as they are not really relevant.
What you say may be true if the data is not arranged in a manner conducive to efficient queries of the type you are trying to make (for instance if the DB was optimised for a different sort of output because the needs were (or were expected to be) different at design time. BUT, read performance is not relevant to foreign keys. A constraint is assessed as INSERT/UPDATE/other time to maintain referential integrity and has no effect on later reads. You can do your own linking in the application if you want, but I'm keeping my foreign keys to stop bad data getting in - they won't affect your process of getting data out either way.
Also note that a foreign key does not imply an index exists in most DBMSs⁴ so if you are expecting the constraint to help performance when referring to a table from its parent then you may be disappointed. An index will exist where the key is referring to as FKs will always refer to a primary key or unique index but the other side is not usually indexed unless you explicitly ask for it to be. I've seen a few people run into this trap, expecting an index to be there because an FK constraint is, and coming to the conclusion that JOINs are just slow because one isn't so their queries that would benefit from it are slow.
----
[1] Sorry, not a good enough person: “sounds like you need a better DBA!”²
[2] Well, a better database developer. Even the best can't get good performance from an inappropriate design. Or maybe a time machine, everyone who has worked with BDs long enough will have been stuck with bad or inappropriate design³ we have no power or time to fix…
[3] Possibly of our own making!
[4] Some DBs create one automatically, and some ORMs & other data manipulation libraries built on top do too. But it is generally not done because it is far from always necessary, so it could waste space, and you may want a compound index instead depending on other properties of the data ans desired outputs.
There is basically no way it is faster for "high-volume" systems to return excess data to the application rather than doing the joins on the dB and returning the record set to the application.
Even if we were talking about multiple billions of records you'd still be better off with a completely denormalized data warehouse style table and doing filtering against indexed columns db-side before sending vast quantities of data to the application.
I think people think this way because of licensing and the specialized nature of DBAs.
But when you say "return excess data to the application" I'm not sure what you mean. Not doing lots of complex JOINs doesn't mean not filtering the queries at the DB at all. Nobody is pulling back a billion records at a time.
Here's an example of what I'm talking about - read the first comment on https://www.brentozar.com/archive/2015/05/do-foreign-keys-ma..., another venue for this same debate.
> But when you say "return excess data to the application" I'm not sure what you mean. Not doing lots of complex JOINs doesn't mean not filtering the queries at the DB at all. Nobody is pulling back a billion records at a time.
The question then becomes "Are you gaining anything by not using foreign keys on the database?" What is the additional speed impact of JOINs actually costing you? How denormalized is your database already, if JOINs are costly?
If you want raw speed at billions-of-records scale, you want as flat a schema as you can get and good indexes are actually going to fit into RAM.
By that point though, you should be able to recognize your use case is not the 90% (or even 95%) case, and your specific requirements are driving doing something different. That's very different than the vague "high-volume" statement you made at first.
My experience has been in assisting clients in migrating to data warehouses and specifically during the heyday of Hive/Hadoop/Spark years ago in seeing clients mistakenly believe they were "big data" and go down the rabbit hole of trying to scale out before it was actually necessary. The problem I have with the vague notion of "high-volume" is that I saw clients with 500m records believe they fit the bill, as well as clients with as few as 20m records who thought the same. The reality is neither of them did and they wound up wasting a lot of money in pursuit of slower systems.
> Here's an example of what I'm talking about - read the first comment on [...], another venue for this same debate.
That's not an example though, that's just some vague statements about needing to profile your query and evaluate the costs for yourself, which should be rather obvious.
Apart from your skepticism about whether my org's DB is as efficient as could be, I don't think we actually disagree, unless your argument is that people in my situation are somehow obligated to make a specific set of choices rather than what works for them.
I have seen over and over firsthand the kinds of data integrity problems that come from leaving the decision to the business software and those who meddle asynchronously with data.
You can always rewrite software. Rewriting bad data is not only difficult but often impossible.
"You can always rewrite software. Rewriting bad data is not only difficult but often impossible."
Isn't that an argument against FKs? It's easier to rewrite the software to handle FKs than to deal with trying to setup FKs with bad data since it's difficult to fix.
> Isn't that an argument against FKs?
No, it's an argument for FK checks.
> It's easier to rewrite the software to handle FKs
Except once you have that bad data, you don't know what to do to fix it in whatever language your app is coded in any more than you do than in SQL. Once you know that, you can just as well do it in SQL as in any other language. Or, if it's just that you know that language better than the SQL language, by all means write a separate one-time fix-the-data app in that language, run it once to fix the data...
And then enable the FK checks so you won't have to do it again.
> than to deal with trying to setup FKs with bad data since it's difficult to fix.
Which is why you want FK checks in your DB from the beginning.
[Edit: Fix bad original editing.]
I think trying to manage consistency in your application is probably a bad idea.
Entire generations of application may rise and fall. New languages, frameworks, developers all lead to rot over time. Heck, some legacy projects the application code is incomplete or lost.
But the database doesn't rot. Show me a database that is 20 years old, and it is as fresh as the day it was created. If it has FK constraints, it's even healthier.
That means that the more value is embedded in the database - FK and of course many more constraints - that value will live for decades.
Commenting just to draw attention to your comment.
Another classic is the “joins are slow” argument, which I believe goes back to a period in the late 1990s when in one, not highly regarded at the time, database, namely MySQL, they were indeed slow. But the reason “everyone” knew about this was precisely the oddness of this situation: in fact RDBMSes are highly optimized pieces of software that are especially good at combining sets of data. Much better than ORMs, anyway, or, god forbid, whatever you cobble together on your own.
There is, in my mind, only one valid reason to not use foreign keys in a database schema. If your database is mostly write only, the additional overhead of generating the indexes for the foreign keys may slow you down a little (for reading, these very same foreign keys in fact speed things up quite considerably). Even in such a case, however, I’d argue you’re doing it wrong and there should be a cache of some sort before things are written out in bulk to a properly setup RDBMS.
It was a pretty central table, and the inability to use FKs there kinda spread outward.
Or something. I can't remember the details, but I was (and still am) very averse to complexity in my application code.
I recently migrated to EntityFramework Core (from the non-core version) and I’m actually impressed. Most SQL is pretty much what I’d write by hand.
Now granted, if there are complex joins, subqueries and stuff, I don’t even try wrangling the ORM to somehow give me that output, but still. I feel more comfortable just using EF than I used to.
Most of the time when I had performce issues it isn't EF. It's a missed index or higher level query issue.
Like simple operation
what is y.Name ? Even though you didn't save anything to the database yet ? And the second Find didn't actually refresh from the database ?Oh and the random bugs where people improperly include related entities but it somehow ends up working because they are automatically added as you're firing off other related queries, until eventually it does not (usually in production only).
It's a really really complex system designed to look simple and pave over important details with "works most of the time" defaults.
No weird caching, no auto saves. Just an object mapper that you can use when you want and ignore when you need to.
If the SQL is performant and it returns the expected data, that is good enough for 99.9% of cases.
In this case, with ORMS, even good ones, this happens often enough in production that to actually master the tool you do need to care.
In this case the important part to know is that the DbContext represents the unit of work and "knows" Entities you previously queried on it. That's very useful, but also can hide bugs like you mentioned with the Includes. I do wish that you'd get more obvious errors if you forget an include, this can be really annoying to debug especially if you're new to EF Core. For read queries I mostly use Select instead of Include, which I find easier and more straightforward in most cases.
ORMs are really useful for making very common operations easy and for making stuff composable. They're also very complex and to make the best use of them you do need to understand both SQL and some basics on how your specific ORM generates this SQL.
This is why I avoid ORMs in favor of writing SQL queries manually: I only need to understand one complex system for non-trivial cases instead of two.
(To be fair, I haven’t done any database programming for a few years. ORMs may have significantly improved since I last looked at them.)
Editing in general is hard. E.g. if in a form you change a field that participates in some filter which generates a dataset to be used in that form, it creates an issue that a naive join now returns incorrect data (because the join condition itself was edited). Complex ORMs which help with that^ are not leaky abstractions, they just try to avoid mistakes an average programmer would do anyway in “trivial” SQL tasks without blinking once.
And yes, gp question about x vs y means that no thought of editing contexts was ever considered. Plain old fetch-store is too low-level and doesn’t represent a model that business logic thinks in.
^ Idk about EF in particular, just assuming
I think the biggest pitfall is how it maps object model to SQL.
The thing people fear about SQL query generation - IMO it's a non issue - when you identify hotspots you write your query manually, tools for that are there, it's easy to do retroactively and >90% of the code won't be the critical path.
DbContext is basically shared mutable state between your entire execution scope, and worst of all it makes it non-obvious.
i’ve also worked places where orm were held as such anathema that any orm proposal was dismissed out of hand without any sort of discussion.
In my experience all using an ORM accomplishes is making sure the people on your team who are amazing with SQL write just as bad queries as those who suck at SQL.
> Much better than ORMs
These two things are not mutually exclusive though right?
It’s entirely possible to have a lightweight and relatively transparent ORM which makes full use of the underlying RDBMS.
I use JPA/Hibernate professionally, as a decision maker, but I don't think I'm in either camp entirely. ORMs aren't a magic wand, but they do help you standardize the boilerplate that you'd end up with one way or the other, in most cases.
But, everything is an abstraction, and I tend to think that if you use any abstraction, you need to have at least a little bit of knowledge about what’s happening in the layer beneath it.
So using an ORM will not be an optimal experience if you don’t know how the underlying RDBMS works.
And effectively using an RDBMS directly still requires a bit of knowledge about the layer below that level of abstraction too (eg how underlying query optimisation works etc).
It’s possible to implement both incorrectly and get bad results and the opposite is true too
Doing intensive string manipulation to put your query together becomes painful, fast, especially when you're dealing with optional parts like ordering, limiting, filtering, pagination, etc. It's also incredibly easy to slip in an injection vulnerability as you do that (especially if you're new to programming).
Just don't use it as a crutch because the declarative nature of SQL is vastly more powerful than an imperative wrapper and you'll be at a loss for only knowing the conventions and opinions of your ORM of choice.
The only person I knew who died on that hill would insist on doing two queries to the database, and then would insist on doing a client side cartesian join.
I think many were burned by mysql back in the day - trying to use sql as a document database - or using php frameworks that happily did a hundred queries pr page view.
As a general rule of thumb, for a REST app - I'd say the db should be normalized, and the cache layer(s) can handle the denormalization.
Ie when you get /page=1 varnish can spit out a response from ram (which if you squint, is a denormalized projection of your data), or it can go talk to your app, that talks to the db. And the latter is most likely fast enough (tm).
While when using denormalized database, your read will have to go to the disk.
But each query will use a different copy of the same data instead of joining with the same copy.
Storing both copy in memory take more space so you can’t cache as much in memory.
I’m not talking redis or memcached but the page cache inside the sql engine.
Unlikely in practice, though.
Along with the "indexes slow down INSERTs and UPDATEs" argument that you touch on. I mean, it is literally true that indexes make writes slightly slower, and an excessive quantity of indexes (which I have seen) can slow down writes enough to cause problems. But - in general - the slowdown is irrelevant compared with the overhead of querying a table that contains 2 billion rows using, oh, I don't know, a table scan because you don't have even a single index (I have also seen this).
Got any arguments to back up this bald assertion?
In particular, I'd love to hear more about how to manage schema migrations on large tables with FK's without incurring lengthy locks or downtime.
Betting the answer is going to involve some variation on "well, don't do that" which is when I'll rest my case.
They made an query and indexing system on top of it to make it fast called TAO.
Without it you need to send a distinct SQL query pet parent object to get list of associated child object which would be awfuly slow.
ORMs are just a wrapper around RDBMSes. If your ORM is producing incredibly stupid SQL to query the DB with, you might want to check that you're not modelling your data in a stupid way.
I am by no means an expert, but in general I have found that if the ORM is doing something particularly crazy, it's because my underlying assumptions about the data model is wrong.
Though the answers would probably vary and there's most likely lots of nuance per individual case (which might matter more than just yes/no), personally I can think of the following as examples:
Someone else mentioned varying schools of thought, which rings true. Personally, my opinions about database design in general are along the lines of: Though my ideal database design probably looks way different and scales slightly differently (which hasn't mattered as much yet) than someone else's.There are people who want to build their entire database around a "classifier" system, about which I wrote previously here: https://news.ycombinator.com/item?id=32416093 (this also makes the DB hard to visualize as ER diagram because of meaningless links, and sometimes makes the DB hard to use without the app, e.g. type_enum_value vs table_name).
There are people who want to do everything in procedural SQL (I've seen application views call stored procedures to fetch all data and validate forms), there are those who don't want to touch it with a 10 foot pole.
It really varies a lot, though in my experience it's invaluable to be able to feed a database into something like DbVisualizer and get an overview about how the different tables are related to one another, basically like documentation: https://www.dbvis.com/
I create all fields as NOT NULL and use empty string in place of NULL.
Last time I tried foreign key constraints can't work in an environment like this.
...but why?
Say, if I want to check how many records I don't have value for "ref", I don't want the count(*) query to show
I want both added together. That's for example one simple reason out of many others.it sounds like you're just using the database wrong.
Extending this one will require you to use "infinite types of NULLs". Decades ago there was an article written explaining it, and that turned into a meme for a while.
For example : Guess you set up a "collector database" that collects data from other databases. It might don't know what the other database's field value is, (call this situation "NULL", the classic case). Or know the other database field value and it's NULL on the other database (now call it NULL-KNOWN, or NULL-TYPE2).
And then do the same thing for a program that now reads from this "collector database ". NULL = Program haven't read the database yet and doesn't know. NULL1 = Program have read the database and it's null. NULL2 = it was null from where this collector database read this data.
See where's that going?
Use a separate field if you want to distinguish.
It's not a valid computing problem. In your example there's no difference whatsoever between NULL1 and NULL2, because it doesn't matter where it came from, and as for the difference between NULL and NULL1 - it's irrelevant, because if we know that the field exists, we have the database.
> "decades ago"
I'm fairly sure nobody needed more than 1 type of NULL decades ago, and certainly nobody needs more than 1 type of NULL now.
Lesson learned.
Thanks.
The small number of people with high enough scale that they can't use them know who they are, the rest of us need to think carefully when performing database migrations and reason out the order of operations required to maintain data integrity (sounds like a good idea anyway?).
Heck, yes. If you are butting against constraints then you may have misunderstood the problem.
I don't mind people turning constrains off for a mass migration under the following conditions:
• This is not production, or if it is production you are in a maintenance window during which you have exclusive access to this DB
• The data you are piling in will be verified against the constraints once the job is complete (no turning things back on with “WITH NOCHECK”), and you have a rollback plan for if that fails (“restore from backup take before the maintenance window” might be acceptable, if that can be done in the timeframe of the maintenance window and you are happy taking questions from your team/clients/management if this means planned updates have to be completely postponed).
• You can explain why not getting the data to be modified in an order that allows the constraints to be kept on through the process would be significantly more complicated (simplifying migration code is a valid reason for temporarily turning off constraints if it makes maintaining the relevant code less error prone) or significantly slower (sometimes doing it most right unavoidably takes more time) or both.
The issue with managing the relationship just in code is if you ship a bug to break the relationship, you now have to manually fix your data, and if you want to find out when or where the bug was introduced, you're looking at commit history instead of a migration history. Same thing when it comes to making manual updates or adds in the db. Even if it's just on a dev stage, if your code makes an assumption about the constraint which isn't true, you can end up with bugs or exceptions on dev, which is also annoying. If you want to remove the assumption of the relationship from the code entirely, that would be more understandable, but not if instead it means replacing what would be an efficient constraint and join with a separate query.
Specifically I hadn't thought through an Order model and the OrderLines ended up being dependent directly on the product meaning through an FK. The orders wouldn't "settle" once they had been completed, since the product could be updated and change the values of the orderline and order. Dumb dumb. It was one of the cases where denormalizing data, very much, makes sense