For example, given 3 tables:
and assuming we want to enforce that
I'd go with
DROP TABLE GASTROPOD PURGE;
DROP TABLE SNAIL PURGE;
CREATE TABLE GASTROPOD
(GASTROPOD_ID NUMBER,
GASTROPOD_TYPE VARCHAR2(5),
SNAIL_ID NUMBER,
SLUG_ID NUMBER,
CONSTRAINT GASTROPOD_PK PRIMARY KEY (GASTROPOD_ID),
CONSTRAINT GASTROPOD_TYPE_CK CHECK (GASTROPOD_TYPE IN ('SLUG','SNAIL')),
CONSTRAINT GASTROPOD_SLUG_CK CHECK
(SNAIL_ID IS NOT NULL OR SLUG_ID IS NOT NULL),
CONSTRAINT GASTROPOD_SNAIL_CK1 CHECK
(GASTROPOD_TYPE = 'SNAIL' OR SLUG_ID IS NULL),
CONSTRAINT GASTROPOD_SLUG_CK1 CHECK
(GASTROPOD_TYPE = 'SLUG' OR SNAIL_ID IS NULL),
CONSTRAINT GASTROPOD_SNAIL_CK2 CHECK (SNAIL_ID = GASTROPOD_ID),
CONSTRAINT GASTROPOD_SLUG_CK2 CHECK (SLUG_ID = GASTROPOD_ID),
CONSTRAINT GASTROPOD_SNAIL_UK UNIQUE (SNAIL_ID),
CONSTRAINT GASTROPOD_SLUG_UK UNIQUE (SLUG_ID)
);
So you check that a gastropod is a snail or slug and either slug_id or snail_id is set. If it is a snail, then slug_id must be null, and for a slug then snail_id must be null. Make sure slug and snail ids are unique (I've added checks to match them to gastropod_id too).
CREATE TABLE SNAIL
(SNAIL_ID NUMBER,
CONSTRAINT SNAIL_PK PRIMARY KEY (SNAIL_ID),
CONSTRAINT SNAIL_FK FOREIGN KEY (SNAIL_ID)
REFERENCES GASTROPOD (SNAIL_ID));
Snails must point to a row in gastropod where snail_id is not null, and it is also the primary key (and therefore unique)
ALTER TABLE GASTROPOD ADD CONSTRAINT SNAIL_GS_FK FOREIGN KEY (SNAIL_ID)
REFERENCES SNAIL (SNAIL_ID) DEFERRABLE INITIALLY DEFERRED;
Gastropods with a snail_id set must also have a corresponding row in snail. I've made this direction deferrable, otherwise you'll never get any new data it.
My own solution for postgres (but I have no idea if it is the best way):
enum:
create type gastropod_type as enum ('slug', 'snail');
tables and constraints:
create table gastropod(
gastropod_id serial unique,
gastropod_type gastropod_type,
slug_gastropod_id integer,
snail_gastropod_id integer,
average_length numeric,
primary key(gastropod_id, gastropod_type),
check( (case when slug_gastropod_id is null then 0 else 1 end)+
(case when snail_gastropod_id is null then 0 else 1 end)=1) );
create table slug(
gastropod_id integer unique,
gastropod_type gastropod_type check (gastropod_type='slug'),
is_mantle_visible boolean,
primary key(gastropod_id, gastropod_type),
foreign key(gastropod_id, gastropod_type)
references gastropod deferrable initially deferred );
create table snail(
gastropod_id integer unique,
gastropod_type gastropod_type check (gastropod_type='snail'),
average_shell_volume numeric,
primary key(gastropod_id, gastropod_type),
foreign key(gastropod_id, gastropod_type)
references gastropod deferrable initially deferred );
alter table gastropod
add foreign key(slug_gastropod_id, gastropod_type)
references slug deferrable initially deferred;
alter table gastropod
add foreign key(snail_gastropod_id, gastropod_type)
references snail deferrable initially deferred;
test:
insert into gastropod(gastropod_type, slug_gastropod_id, average_length)
values ('slug', currval('gastropod_gastropod_id_seq'), 100);
insert into slug(gastropod_id, gastropod_type, is_mantle_visible)
values (currval('gastropod_gastropod_id_seq'), 'slug', true);
select gastropod_id, gastropod_type, average_length, is_mantle_visible
from gastropod left outer join slug using(gastropod_id, gastropod_type)
left outer join snail using(gastropod_id, gastropod_type);
gastropod_id | gastropod_type | average_length | is_mantle_visible
--------------+----------------+----------------+-------------------
1 | slug | 100 | t
(1 row)
This is a case where using a trigger is of value to have complex constraints like this enforced.
"and assuming we want to enforce that (1) every row in 'gastropod' has exactly one corresponding row in 'snail' or 'slug' (but not both) (2) every row in 'slug' has exactly one corresponding row in 'gastropod' (3) every row in 'snail' has exactly one corresponding row in 'gastropod'"
(1) is an inclusion dependency (aka "foreign key dependency") between 'GASTROPOD' and a virtual relvar (aka "view") defined as SLUG UNION SNAIL. (2) and (3) are the same kind of inclusion dependencies between 'SLUG' (/'SNAIL') and 'GASTROPOD'.
All of them taken together mean that you have an "equality dependence" between 'GASTROPOD' and 'SLUG UNION SNAIL' (at least as far as the identifiers are concerned).
Note that to be able update a database that is under such constraints, you are likely to need either a DBMS engine that supports this thing called "Multiple Assignment", or else one that supports "Deferred Constraint Checking".
Chapter 11 of the book "Applied Mathematics for Database Professionals" goes into great depth on the subject of how to enforce such constraints (and in fact, just any constraint, however complex) in SQL environments. The answer to your question is almost the entire contents of that chapter, and I hope you don't expect me to summarize it all here in a few words (the essence of the answer is "triggers" - as StarShip3000 also indicated).
One of the problems with SQL is its poor level of support for integrity constraints, especially referential constraints.
For all practical purposes your problem cannot be solved using SQL constraints unless you disable the constraints when you want to insert a row to a table. The reason is that SQL requires tables to be updated one at a time and so the constraint must be violated whenever new rows are inserted. This is a fundamental limitation of SQL and all the major DBMSs suffer from it.
There are some workarounds but none of them is perfect. You could use DEFERRABLE constraints if your DBMS has them (Oracle for example). A DEFERRABLE constraint is really just an easy way of disabling a constraint. Or you could use triggers, which means the rule is enforced procedurally rather than through a proper database constraint.
Ideally, I would make a single table "gastropod" with a "type" field, and then have views "gastropod" (selecting all fields except "type", with no "where" clause), "snail" (using a "where" clause to limit to type snail), and "slug" (using a "where" clause to limit to type slug). Exceptions may exist if one of the two types is much smaller and there are many fields relevant to only the smaller type, but for the most part making it different views from a single table will ensure the proper integrity constraints.