Ask HN: What are some examples of good database schema designs?
I've seen many examples of database schemas but don't always recognise a good design over a mediocre or poor design.
This is what I mean by schema (is this a good design?)
https://www.drupal.org/node/1785994
When I read about database schemas with thousands of tables I wonder what kind of problem could possibly require so many tables.
What are good schema examples? And what are some poor examples?
180 comments
[ 4.5 ms ] story [ 218 ms ] threadThe one you linked is a pretty typical relational model and isn't bad, but it has trade offs that I'd personally not make, however, that doesn't make it bad.
In the end context, scale and usage all determine a good schema design. Sometimes what would be a good relational design on paper would be tragically horrid in practice once you get beyond a small dataset.
That said, some of the best things to look at are open source projects and research their pain points and highlights. That will many times point you to the tradeoffs they made in modeling and in code. For e-commerce things like magento and other open source solutions can be really informative, even woocommerce and how it shoehorns itself into wordpress can be informative as to the tradeoffs on highly normalized vs lightly normalized vs key/values etc.
Edit: Not SAP, but certain other ERP products have an alarming habit of not using foreign keys - which makes working out the structure of the database quite interesting...
They're probably mostly small (in terms of columns), many of them done just to have foreign key constraints, separate indexes, and arguably easier querying (joining) on just the data you need.
But I think it's a particular style, rather than a 'problem' that 'requires' so many.
(IANA database expert though, just my tuppence.)
I think the primary problem of giving examples here is similar to teaching software engineering, which needs complex projects solving complex problems - too big for a semester project.
A good schema depends on the problem it's solving.
A secondary problem is similar to code that has sacrificed clarity for performance. The tweaks made for performance are not intrinsic to the logical problem, but are an additional constraint.
For performance on common queries, schema can be flattened or "denomalized". The ability to do so was one of the original motivations for Codd's relational algebra.
https://meta.stackexchange.com/questions/2677/database-schem...
https://i.stack.imgur.com/AyIkW.png
19 million questions, 29m answers, 12m users, 10m visits per day, and most of the db action is in less than two dozen tables.
I'm curious what they do if someone changes their name (though that should be really rare on SO)
On the other hand, if your bookkeeping application uses a database try to keep things as tidy as possible.
I agree that it's very important to not let the physical schema leak into the rest of the system, and to have a strong conceptual model (aka entities and relations). This has been well understood for almost half a century: https://en.wikipedia.org/wiki/Three-schema_approach
But I don't think ORMs are in any special position to help with this. They typically introduce so much other confusion that they tend to divert attention from designing both a good physical schema and a good conceptual model, and maintain a sensible mapping between the two. This can be done with RDBMS views for example, with a fraction of the overhead of an ORM. Most ORM-based code bases I've seen leak tons of db-level details.
> switching to a completely different storage system in the future
Designing for this eventuality is not healthy IMO. If you get there, it will be a so-called "good problem to have" and you will have to deal with whatever unique challenges you face at that level. We might as well be writing code with the possibility of "switching to a completely different programming language in the future" in mind. Yes, clean, modular code will help, but beyond that, not committing to the capabilities of the tools you have chosen will harm you system.
I think there are plenty of bad ORMs and there are plenty of ways to use the good ones in a bad way, but that doesn’t mean that they aren’t providing the value I mentioned. For instance Entity Framework Core with code-first migrations has you designing the data models themselves, then wiring up relationships and other metadata (indexes, keys, etc.) in the DB context itself - your actual entities are completely portable and have nothing to do with the db itself outside of being used by it.
And sure, needing to switch to another storage system may be a good problem to have... that doesn’t mean you should explicitly tie all of your code to one particular RDBMS. If a user is a user is a user, it shouldn’t matter to anything else in your codebase how or where it is stored, it should still be the same entity. Moving those users from your SQL Server to Mongo or to a third party like Auth0 or an Azure/AWS/etc. federated directory service doesn’t change the fact that every user has an ID, an email, a name, etc.
Code for today, but design for tomorrow.
I don't hear people talk about "coding for the web, but design so that you can easily switch to deploy as a Windows desktop app." Or "write it in Python, but in such a way that we can easily swap to OCaml." It seems to me databases are uniquely treated this way, as some kind of disposable, simple piece of side equipment. Again, modular code will always be easier to migrate, but I prefer to take full advantage of db capabilities, as it results in much less code and frees up time and mental space to focus on a good conceptual model and physical schema, among other things.
I've never used EF, so I might not see what you are seeing.
This is exactly right - lots of people are still cargo-culting rules of thumb that no longer make any sense.
This was an artifact of the last generation's commercial DB market. Open source DBs weren't "there" yet; a combination of real limitations and risk-conservatism kept companies shoveling huge amounts of money at vendors for features and stability now provided by `apt-get install postgresql-server`.
If you just lit seven figures on fire for a database license, you're not hungry to do it again, so you wanted all your software to be compatible with whichever vendor you just locked yourself in to. And certain DB vendors are very well known for brass-knuckle negotiation; if you could credibly threaten to migrate to $competition instead of upgrading, it was one of the few actually useful negotiating levers available.
Today, open source DBs are better than the commercial ones in many situations, certainly not worse in general use, and the costs of running a bunch of different ones are far lower. Not to mention, the best way to win a software audit is to run zero instances of something.
When you have a small team working on a given tool that only really needs to manage its own data, it really doesn't matter. But some point, you do need expert gatekeepers to tell engineers when they're Doing It Wrong when there are many heterogenous clients accessing large datastores for different purposes, complex audit requirements, etc.
YAGNI
The number of teams who eventually move to a different data store? Almost zero.
Getting "locked in" to a database is a non-issue. In fact you should get locked into a database system, provided you picked a good one to start with. Most teams never even scratch the surface of what a powerful DB like Postgres can do for them and it breaks my heart every time.
ORMs aren’t bad, but learn their escape hatches or else you’ll have a hard time doing more complicated things.
So in my understanding, the question posed only applies to at least moderately complex systems, which is where engineering skills matter. And in that context, learning what distinguishes a good database design is obviously very valuable, not to say crucial.
> Nobody understands three lines of code of queries with left and right joins.
Not sure if you're being flippant, but a) this is not true, and more importantly b) why is it that we don't expect programmers to be at least as fluent in SQL as in other, less important, languages?
There is no Customer table.
"The Data Model Resource Book, Vol. 1: A Library of Universal Data Models for All Enterprises"
[1] https://www.wiley.com/en-us/The+Data+Model+Resource+Book%2C+...
https://ofbiz.apache.org/
I think 'learning from how not to do something' is a really powerful pedagogical technique that should be more widespread.
(Full disclosure, I agree with your objection and believe piracy is wrong in any case, but seems like we should be consistent with the application of the rules)
Even if an individual did believe that piracy is perfectly acceptable, direct linking still puts the site under risk of moving into the cross-hairs of law enforcement.
Besides, as I mentioned in my comment, direct links to hubs that contain pirated material and direct links containing suites of tools specifically used for pirating such as DRM removal on ebooks and video players, and bypasses for news organizations and scientific publications are also frequently directly linked.
Mentions of key hubs like pirate bay don't count as linking to them IMO though might effectively be so as the mention might clue some to their existence. Discussing the matter completely without mentioning those places/services would not be practical.
Tools for removing DRM have valid uses. Minority uses, sure, but still valid. Whether removing it for your own transcoding & platform shifting purposes, rather than distribution, is morally OK is a complex discussion (I say yes, lawyers will almost certainly say no!), but if you allow that then the valid use list grows.
Bypassing news filters is usually a case of exploiting them giving different responses in different cases to improve, for example, their Google rank. This is against the engines policies. You can of course argue that "two wrongs don't make a right" but then equally one wrong doesn't either! I'm not sure if the scientific publications matter is the same or not, I'll have to submit to your expertise/experience there.
I agree it’s a FANTASTIC way to learn. I was very disheartened I didn’t effectively communicate that to the students.
Someone who's been in the trenches for several years, they'd probably get A LOT out of this technique as they would have many experiences to pull from.
Once you've got some experience (which usually means getting things wrong a few times, seeing wrongness promoted to production because there isn't time to refactor, and having to fix the wrongness later once more layers or wrong are piled on top) you appreciate this sort of analysis much more as it helps you get things right first time more often, and/or helps you spot the actual problem (rather than chasing symptoms) in more complex situations.
I don't think that is it. _Beginners_ being the critical word.
Most learning is part of a negative feedback loop, if we only ever succeeded we wouldn't know why we succeeded, failure has such bad connotations in our society that it blinds students from deeply understanding a subject. Maybe replace it with experience?
Back to the subject of _Beginners_, we really should be teaching students from a very young age, philosophy, cognitive science, and epistemology. They should embrace experience, it shouldn't be up to the database schema instructor to teach both data modeling and learning by failure. Students should be fully versed in how to care and feed their brains by the time they arrive in the GPs class.
But in any specific subject you need a certain level a basic knowledge taught that way before you can be expected to use the tools available (and potentially discover more) to problem solve.
Destigmatize not #winning and embracing experiential learning.
I'd definitely second that recommendation, both for relative beginners and those of us who have been at it long enough to have learned and forgotten these things a few times over...
https://pragprog.com/book/bksqla/sql-antipatterns or your favourite [e]book seller, for those wanting a copy.
> why this is bad, it then shows you the recommended way of doing it and why this way works better
He also takes the time to mention when the anti-pattern might be OK to use.
Saw this thread and just acquired and read this. The book's premise is a great one, I just don't like the execution. Years of my life were dedicated to SQL CRUD and schema evolution before dabbled briefly in NoSQL (meh), random caching systems, then had my aha moment and upgraded to files on unix (awesome caching! great compatibility!) and occasional use of SQLite (easy backup and parallelism! no RDBMS master/slave complexity!).
While there are a few core issues raised, and all struck a chord, I was not a huge fan of the book overall because it was needlessly verbose. I found its authoritative tone grating. Ultimately with so many dialects and projects, SQL style is personal, organizational or project level preference. One issue I felt was undertreated was clarity of syntax. For example, I personally absolutely loathe any use of JOIN as needlessly obtuse cognitive baggage. An untreated issue was (over/mis)use of stored procedures.
In the real world, however, most systems do not have this type of requirement.
https://gist.github.com/nomsolence/69bc0b5fe1ba943d82cd37fdb...
Pictured is the compiler internals; the attached .syrup is the DSL. (It started as a tangent on a project I'm doing for my girlfriend, the schema described in the .syrup has been improved a bit since)
Note: things after # are comments.
I find even just defaulting to NOT NULL and not having to worry about commas is a boon for when I create schemas.
The DSL will of course support things like compound primary keys and SQLite's WITHOUT ROWID.
I'll post the code here, likely before the weekend: https://github.com/nomsolence/syrup
Spec: Product has a supplier [tables:product, supplier]
Reality: Product can be bought from multiple suppliers [table:product, supplier, product_supplier]
The importance of having db schemas and other software entities reflect real-world things is not appreciated enough. It looks unimportant at first, but soon all intuition becomes useless and the system tends to not have "joints" in the right places, ie it doesn't have flexibility in the same places that reality does.
My 2 cents after doing this long enough to recognise it
- Aim for 3NF but not religiously. Still, if you need a flat table try a view.
- Any ternary relationship can be modeled as a pair of binary relations (you'll never regret keeping it simpler)
- You don't need EAV (Magento is a good example of why you shouldn't)
- On the other hand don't serialize data (looking at you WordPress)
- XML and JSON data types though are perfectly fine when you need to store an object
- Every table should have a primary key (preferably an integer)
- If you really want a string for your primary key make it a candidate key (why, because someone will insist on changing it)
- E/R diagrams are your friend
- So are Venn diagrams for visualizing a complex select
CRMs often have hundreds of tables and ERPs have thousands or tens of thousands or more.
My project is a license server for my electron app. The tech stack is a joy: mongo/mongoose, express, graphql, JWT, all with a React web front end. Payments by Stripe. The actual licenses are the signed JWT vended by the graphql API (not to be confused with the totally separate JWT for auth).
The main goal is to sell software so I license by machine fingerprint (node module on electron).
It’s been running for over 6 months without issue. I’m just beginning to start architecting an update where I allow a subscription model similar to Jetbrains Perpetual Fallback license, but slightly different in favor of the user. I’ve taken a lot of notes from comments at https://news.ycombinator.com/item?id=17007036
Here’s what I’m thinking so far:
A) Non-Expiring License at $97.99. Unlimited bug fixes for that one version. or B) Expiring License at $9.79/month, and you always have access to the latest version. After 12 consecutive payments you are granted a Non-Expiring License for the most recent version at the time of each payment.
Now, to model this...
https://i.stack.imgur.com/wnwrJ.jpg
1. Dividing up tables so that one user can have more than one email address
2. Using PostgreSQL’s “row-level security” system to restrict database results based on the logged in user
3. Dividing tables across multiple schemas for additional security
4. SQL comments for documentation
5. SQL functions for common functionality, such as logging in or verifying an email
It’s a fascinating project, and well worth a look!
[1](https://www.graphile.org/postgraphile/)
[2](https://github.com/graphile/starter)
[3](https://github.com/graphile/starter/blob/master/%40app/db/mi...)
This mapped very awkwardly to digital subscribers who we had individual data on. We were able to join databases in a way that sort of works through more or less (mostly less) comfortable assumptions. The queries are not pretty.
Hilariously, it has dozens of names, because it just comes up in so many places for so many people. It appears that "record linkage" is the term that has won the top spot at Wikipedia: https://en.wikipedia.org/wiki/Record_linkage
Record linkage seems to be concerned with joins that aren't guaranteed to be correct because there are no unique keys.
One of the key things you need to do in good database design is to map business verbs to API endpoints in something like a 1-1 way. Having an api endpoint that is essentially "insert this row to the database" is just cargo culting. There is already an API for that, it's called SQL.
Postgrahile allows you to rename/disable api endpoints if needed.
Postgraphile rocks. And it's written in modualar Javascript, so I can hack it if I need to. Unlike Hasura.
It's a great idea, and my clients and bank account agree with me.
There are lots of ways to do software. Some are widely considered by experts to be good, some are not so good.
Then there is a thing called business. You can certainly sell software that is built using bad practices, and honestly nobody will probably complain so long as it works. It might not be quite as maintainable, it might require more effort to add features, and it might be necessary to completely rewrite that software in 5 years when other parts of the system change.
Terrible software is bought all the time, and that's not even really a problem.
Even though you sold it and your customers are happy, there still are things you can probably learn, right?
There is a lot of context lost in generalities so I admit you have to look at every specific situation, but in general CRUD means pushing business logic down to the client (which is generally some kind of code running in a browser or mobile app), which is the opposite of everything good in the world.
Postgrahile is the best of both worlds, providing a nice GraphQL interface on top of your database.
If you decide you need to write crud endpoints, you have your database still. It's a zero-cost abstraction, which is wild.
I'm interested in PostGraphile, but i have a question: How do apply permissions when your user table is different from postgres user systems? i only have handful of users that have permissions spaning a lot of tables.
Do i need to create one postgres user for each of my application users?
then, when you are parsing the events(hydrating or replaying) you just check the schema version in the envelope and handle the changes/payload accordingly.
it's actually very trivial once you put it all together.
This articles explores when it is not the case to apply it (for example when eventual consistency would be a big problem): https://medium.com/@hugo.oliveira.rocha/what-they-dont-tell-...
I guess at some level of load (for scalability reasons) one might need to decouple writing into the append only event log from updating the tables for fast reads, so the one transaction approach won't work, hence the eventual consistency between written data and read data.
Schema maintenance is also a non-trivial task as described in the article: keeping backward compatibility for various event versions, upcasting, lazy upcasting. Hitting a good granularity level for events is important, too big or too small, both are a problem.
For the operational team without intimate app knowledge it is also harder to do ad-hoc work.
Just for record keeping, analysis, learning etc.
E.g. The description of an item in a sales invoice is the description at the time the contract of sale is made, and must be immortalised as such as a copy of that description.
+1. I maintain a couple of systems that younger-me wrote 15+ years ago. If younger-me had understood this, I could have avoided much re-engineering :)
If your InvoiceItem has no description, and only has a FK to Product to get the description you're going to have a bad time. Once you build a system around a wrong relationship like that, it's very hard to go back and fix the inevitable issue that updating a product changes old sales invoice records.
in practice, you have to support retroactive changes, so you have to have "middle" values for versions (i.e. 1.1 between 1 and 2).
People are usually interested in the timestamps associated with those versions (for which period of time is this version valid, when was this version created), so in practice it's easier to just keep track of the timestamps, and leave the versions implicit.
We rely strongly on this at my place of work, it really works! Essentially, you just need two tstzrange columns, representing: (1) when the row was a “valid” representation of the key, (2) when the row could have been used to conduct other “transactions”.
With a valid period and a transaction period, you have a history of the values of an object, and the ability to make non-destructive updates to that object.
It’s an essential component of any audit-worthy system, because it empowers you to trivially answer the question “what did our database think John’s 2018/01/01 address was on the day that we mailed him a check on 2017/12/15”?
1. create a "hole" in the current transaction space's valid space (through expirations of records in transaction time, valid time).
2. insert the new version at (transaction_period=(now, inf), valid_period=(effective_date, inf)).
It sounds complicated, and it sometimes is, but in practice most of the hard work (specifically around transaction periods) is performed for you by triggers. Usually you only have to think about valid periods, as a developer, which is easier to wrap your mind around.
The unfortunate thing is that there aren't a lot of very straightforward open source implementations of bitemporal triggers. The ones that exist are mostly designed to be theoretically sound and feature-complete, rather than usable.
At my company, we have our own implementation that has slowly grown over the years. It's about 500 lines of SQL triggers, and maybe 2k lines of library code in Python/Go to make the ergonomics a little bit easier on the developer.
Let's say that the present concept of John's address is "123 Apple St"; this was inserted into the database at 1/1/2020.
We don't have credible evidence for what John's address was prior to 1/1/2020.
Therefore, the row looks like this:
> (address:"123 Apple St", valid_period:[1/1/2020,+inf), transaction_period:[1/1/2020,+inf))
This is a case where transaction period and valid period are equal. If John sent us this information by mail, and he signed it 12/25/2019, we have credible evidence that this was John's address, at least effective 12/25/2019.
Therefore, his row would look like this: (address:"123 Apple St", valid_period:[12/25/2019,+inf), transaction_period:[1/1/2020,+inf)).
Now, the transaction period is still 1/1/2020, because this information found it's way into the database on 1/1/2020.
Now, let's continue with the second scenario. Let's say we get a second letter from John, processed on 6/1/2020, saying that he moved to "456 Orange St" on 5/1/2020. There are now two rows in the database, as below:
> (address:"123 Apple St", vp:[12/25/2019,5/1/2020), tp:[1/1/2020,+inf))
> (address:"456 Orange St", vp:[5/1/2020,+inf), tp:[6/1/2020,+inf))
Then, we receive a FINAL letter from John, processed on 7/1/2020, revealing that his previous letter contained a typo! it was "789 Orange St", not "456"! Darn. Our table now contains three rows:
> (address:"123 Apple St", vp:[12/25/2019,5/1/2020), tp:[1/1/2020,+inf))
> (address:"456 Orange St", vp:[5/1/2020,+inf), tp:[6/1/2020,7/1/2020))
> (address:"456 Orange St", vp:[5/1/2020,+inf), tp:[7/1/2020,+inf))
Let me know if you have any questions about this example!
- Does the validity period of the erroneous entry ever get closed?
- Or the transaction period of the one that got superseded?
- Do transaction periods close for reasons other than finding out something was wrong, or can I think of them as the period that the rest of the row (including validity) is/was believed?
Thanks for the detailed explanation. This is really interesting.
I had a typo in the last row, it should have been
> (“789 Orange St”, vp:(5/1/2020, +inf), tp:(7/1/2020, +inf))
my apologies!
- the valid period of the middle record is never closed, because it would be a misrepresentation of how the database’s perception of the address at that point in ”transaction time”.
- indeed, the superseded transaction period is closed.
- formally, transaction periods close (and new rows are created) whenever a column’s value changes. Think of it like an “updated at” time stamp, except the meaning is more like “canonical during”. The second half of your question is totally correct, the TP is the period during which the values representing the specified slice of valid time are/were believed.
As the third bullet point implies, one characteristic of this schema is that you have “non-destructive” updates: state is never lost, it’s just put into transaction history. This makes it possible to “roll back” to an earlier, known-good state: simply specify a point in transaction time.
There’s a whole additional rabbit hole to dive down: how to make this stuff fast and intuitive. People have mostly solved the fast part, but we’re still working on the intuition.
The intuition I'm building is that the validity period is metadata and the transaction period is metametadata. I think the similarity between them is a little deceptive, since the validity period is in some sense just another piece of data sort of controlled by (or interpreted in light of) the transaction period, which is itself a way to simulate versioning the whole database; I think of it like the internal representation for a database wrapper that lets you time travel in a database with a history-oblivious schema. In that vision, validity periods are actually part of the history-oblivious schema. You could totally have "transaction periods" for stuff with no concept of validity period, e.g. something timeless where our understanding evolves over time. If you're trying to teach people, I'd suggest introducing the two concepts independently, then showing the interplay with an example like the one you gave here.
I don't know if weird nested contexts can ever be really "intuitive". :D
I will refrain to comment on the quality of Drupal’s schema, but that diagram just shows a bunch of tables and how those tables are connected by foreign keys. What do those connections mean? What other constraints are there in the data? Are they represented in the diagram?
A good database design conveys a lot more semantics. There is currently one ISO standard (ISO 31320-2, key-based style modeling) for database design. Adop ting that standard does not automagically guarantee good designs, but, if used correctly, it helps a lot (it doesn’t help if you don’t have a good grasp of the Relational model, so I would recommend that you would get familiar with that first and foremost).
Most database schemas you’ll find around are rippled with useless ID attributes that are essentially record identifiers (they are not even “surrogates” as many people call them: for the definition of a “surrogate” read Codd’s 1979 paper) and, as a consequence, they are navigational in nature (they resemble more CODASYL structures than Relational models): to connect records in table A with records in table D, you must join with (navigate through) B and C, while in a well-designed Relational schema you might have joined A and D directly. Do you want a rule of thumb to recognized poorly designed databases? Check for ID attributes in every table (there are many other ways in which database design can suck, though).
How do you recognize good database diagrams? They can be easily translated into natural language (predicates and facts), and two (technically competent) different people would translate them in the same way, i.e., the model is unambiguous. Can you say so for Drupal’s schema?
Ones that ship.
Businesses that trade in thousands of products, employ thousands of people in dozens of countries, with hundreds of different taxation, human relations, timekeeping, payroll, health insurance and retirement savings laws, dozens of sites, inventories in hundreds of locations managed by dozens of contractors, moved by one of potentially thousands of logistics firms with at least a dozen modes of shipment, reporting standards for multiple countries, reporting standards for stock exchanges, internal management reports, bank accounts in multiple countries, in multiple currencies, with a mix of transfers via a variety of means, hundreds of cash alternatives with varying rules about whether they are cash alternatives...
Modern large businesses are very, very complex.