1. FK constraint
If you want to enforce referential integrity with FK constraints, you'll have to include the type
in table b
redundantly, add a (redundant) UNIQUE
constraint on a(id, type)
and make that a multicolumn FK constraint on (a_id, type) REFERENCES a(id, type)
.
Related (with code):
2. Partitioning
A less strict design could be built with table partitioning. Have a master table a
with list partitioning on partition key type
. Thus you'd have a partition a_x
with all type 'X', and a partition a_y
with all type 'Y'. Then create FK constraints to the respective partition and you can be sure of the type.
I suggest list partitioning in Postgres 11 or later. Related (with code example for list partitioning):
Note that you can't have a FK constraint to the master table. The manual:
foreign keys referencing partitioned tables are not supported.
The FK solution occupies more disk space due to a redundant column and a redundant index. With partitioning OTOH, depending on your version of Postgres and the partitioning method chosen, you have to deal with the limitations / caveats of that particular setup, though.
3. Trigger
You posted another trigger-based solution, which does not occupy extra space nor complicate the relational design. But it's easier to break / circumvent. Not as bullet-proof as the FK solution. Either way, here is an improved version of your idea:
CREATE TABLE a (
id int PRIMARY KEY,
type enum_type NOT NULL -- ! NOT NULL or adapt the check in the trigger
);
CREATE UNIQUE INDEX a_type_id_idx ON a(type, id); -- ! see below
CREATE TABLE b (
a_id int REFERENCES a(id)
);
CREATE FUNCTION type_check() -- ! rewritten
RETURNS trigger AS
$type_check$
BEGIN
IF EXISTS (
SELECT
FROM a
WHERE a.id = NEW.a_id
AND a.type = TG_ARGV[0]) THEN
-- do nothing, all good
ELSE
RAISE EXCEPTION 'Type does not match table type!';
END IF;
RETURN NEW;
END
$type_check$ LANGUAGE plpgsql;
CREATE TRIGGER tg_check_type_b
BEFORE INSERT OR UPDATE ON b
FOR EACH ROW EXECUTE PROCEDURE type_check('X');
The index on a(type, id)
allows index-only scans for the frequent lookups. Index expressions in this order. Here's why:
You checked with IF var_type <> var_allowed_type
, but a.type
wasn't defined NOT NULL
. That would allow mismatches with NULL
.
Assignments are comparatively costly in plpgsql. I simplified the function to make it fast. IF EXISTS ...
replacing variables, additional query with assignments should be substantially faster - and also catches a potential mismatch with NULL
by inverting the logic.
4. Abuse a CHECK
constraint
Even faster, and more hacky, yet. Make it NOT VALID
to be largely legit. Related cases with code examples and rationale: