Quick Answer
You can't directly perform foreign-key constraints against a hierarchy. You need to flatten it. Put your data's keys into a table and constrain against that table.
Determining The Key
Let's back up a moment and examine the entities as you've defined them above.
Our hierarchy is like this:
- Vehicle
- Label
- Class
- Label
- Maintenance Frequency
- Function
Now we have some rules:
- Class Cars must have a Maintenance Frequency of either 0.5 or 1.0.
- Class Motorbikes must have a Maintenance Frequency of either 1.5 or 2.0.
- Class Motorbikes must have a Function of 'beach trip'.
So given this setup, what information do we need to uniquely identify any particular vehicle Class, in such a way that we can ensure it complies with the rules? It's all of the attributes of Class:
(label, maintenance_frequency, function)
This represents our key.
(If Rule #3 didn't exist, we could drop function out of the key. Alternatively, if it is the sole exception we could use a check constraint to enforce it, but we're getting ahead of ourselves.)
Since the rules are arbitrary, we will need to define all of the valid keys so new entries can validate against them, I'll call it config:
config
| label | maintenance_frequency | function |
| car | 0.5 | leisure |
| car | 0.5 | work |
| car | 1.0 | leisure |
| car | 1.0 | work |
| motorbike | 1.5 | beach trip |
| motorbike | 2.0 | beach trip |
Building The Constraint
Now that we have a table we can validate against, let's revisit the vehicle table:
| id | label | class | maintenance_frequency | function |
| 1 | Alice | car | 0.5 | leisure |
| 2 | Bob | car | 1.0 | work |
We add a FOREIGN KEY constraint against the config table for our key (label, maintenance_frequency, function) so all of the columns can be validated at insert time.
If we wanted to add a new entry, Charlie, like this:
| 3 | Charlie | car | 1.5 | leisure |
The constraint finds that (car, 1.5, leisure) doesn't exist in config, the row is thus rejected.
To convert to PostgreSQL parlance, here are the table & constraint definitions:
CREATE TABLE config ( label text NOT NULL, maintenance_frequency numeric NOT NULL, function text NOT NULL, PRIMARY KEY (label, maintenance_frequency, function) ); INSERT INTO config VALUES ('car', 0.5, 'leisure'), ('car', 0.5, 'work'), ('car', 1.0, 'leisure'), ('car', 1.0, 'work'), ('motorbike', 1.5, 'beach trip'), ('motorbike', 2.0, 'beach trip'); CREATE TABLE vehicle ( name text, label text, maintenance_frequency numeric, function text, FOREIGN KEY (label, maintenance_frequency, function) REFERENCES config(label, maintenance_frequency, function) );
With config loaded, trying to add the Charlie car is met with an error:
> insert into vehicle values ('Charlie', 'car', 1.5, 'leisure'); ERROR: insert or update on table "vehicle" violates foreign key constraint "vehicle_label_maintenance_frequency_function_fkey" DETAIL: Key (label, maintenance_frequency, function)=(car, 1.5, leisure) is not present in table "config".
From here, if you want to introduce surrogate keys, you can do so with the knowledge that the surrogate keys map to the correct primary keys. You can also add any additional attributes you like.
Summary
You can't directly perform foreign-key checks against a hierarchy, you need to flatten it. With a small number of keys its practical to just enumerate all of them and use a composite foreign key constraint.
This is a simple example and my answer is not well normalized, mostly for illustration purposes. It's possible to have config be the result of a query of the other tables and validated by foreign key constraints itself. If you had 500,000 configurations to maintain, you'd likely do it through a mapping table of some kind.
idcolumns, start over, use composite keys.Ids and think about what makes each entity unique - use that as the primary key and migrate to the child entities. For example, a vehicle class can have a set of values for maintenance frequency. So ('Car',0.5),('Car',1.0),('Motorbike',1.5),('Motorbike',2.0) - in this case the primary key is(Class,Frequency), notid.