11

As answered in this question: Cardinality in PostgreSQL, cardinality is enfforced using constraints.

Cardinality rules define the allowable counts of the relationships – one-to-many, many-to-many, etc. Many-to-many is achieved using join-tables and one-to-many using FOREIGN KEY.

But how can one implement one-to-one_or_many (one-to-1+) relationship. Which is same as to ask: How can I enforce minimum cardinality in PostgreSQL?

A practical situation would be where one needs to store say address (or telephone number) which MUST be provided (but can be more that one) by the person (say user or customer).

Edit:

The above mentioned situation is a special case (with cardinality one) of a general problem. The general problem being: How to enforce cardinality of arbitrary number?

As answered by jug a non-null FOREIGN KEY reference can be used as a work-around, if minimum-cardinality is one. It will also provide an additional feature to select default among many.

But consider another situation of relationship between team of Cricket and its players. Every team MUST have a MINIMUM of 11 players to qualify as a team. Here the minimum cardinality is eleven (11).

Similary, a relation between a course and a student in a school, where every student MUST enroll in AT-LEAST 5 courses and every course MUST have a MINIMUM of 10 students.

Community
  • 1
  • 1
user1144616
  • 1,201
  • 2
  • 15
  • 16
  • 1
    Both added examples display the same quality. How do you start to build a cricket team from scratch? How does a student enroll in his first course? Students with less than 5 courses must be allowed to exist, at least temporarily. So this is **not** a good place to use constraints. Use queries, functions or triggers for the purpose. – Erwin Brandstetter Feb 14 '12 at 00:03
  • @ErwinBrandstetter Postgres supports deferred-constraints though - another approach is to have a “capstone” table/entity that represents the completed, valid 11-person cricket-team: to remove players means having to remove the completed-team capstone record first (which is fine, as removing players below the count of 11 means the team is invalid). Another option is to abuse indexed-views, though that’s more of a SQL Server thing. – Dai Oct 08 '22 at 04:25

4 Answers4

3

There's no way to specify this using a CHECK constraint, so I think the best approach is a trigger:

http://www.postgresql.org/docs/9.1/static/sql-createtrigger.html
http://www.postgresql.org/docs/9.1/static/plpgsql-trigger.html

You'd end up with something like (I haven't tested it or anything):

CREATE TRIGGER at_least_one before INSERT, UPDATE, DELETE ON the_one_table  FOR EACH ROW EXECUTE PROCEDURE check_at_least_one();

CREATE OR REPLACE FUNCTION check_at_least_one() RETURNS trigger AS $$
    BEGIN
    nmany := select count(*) from the_many_table where the_many_table.the_one_id=NEW.id;   
    IF nmany > 0 THEN 
        RETURN NEW;
    END IF;
    RETURN NULL;
END;
AdamKG
  • 13,678
  • 3
  • 38
  • 46
  • 1
    How is the initial INSERT done? Because the ONE table must be inserted first (to make sure the referenced foreign key exists), then the first row in the MANY table can be inserted which references the ONE table. But your trigger would forbid this. – A.H. Feb 12 '12 at 15:36
  • Answering myself: by using two (!!) `CONSTRAINT` triggers which are `INITIALLY DEFERRED`. These triggers should not return `NULL` but use `RAISE EXCEPTION`. The first trigger watches the "one" table, the other the "many" table (to catch deletions after the first transaction). – A.H. Feb 13 '12 at 16:57
3

There is no way to enforce such rules using only FOREIGN KEY constraints.

1) One way is by allowing circular references between tables (the "default" column, advised by jug). This results in chicken-and-egg problems that are difficult to manage, you'll have to use deferrable constraints. Plus, this option is simply not available in some DBMS. Another disadvantage is that for a football team, you'll have to add 11 "default" columns (and you'll have to deal with a chicken-and-11-eggs problem)!

2) Another option is to use triggers.

3) Another option is to use a database-level constraint between the 2 tables. Not sure if there is any DBMS that has such functionality. Besides the typical UNIQUE, PRIMARY and FOREIGN KEY constraints, most DBMS have just row level constraints and with limitations (no subqueries, etc).

4) Another option is to enforce such rules by creating appropriate INSERT, UPDATE and DELETE procedures that can only access the two related tables and enforce the integrity according to these rules. This is the better approach (in my opinion).

5) One more option, simpler to implement is to use standard Foreign Key constraints, enforcing the 1-to-many relationship and have a View that shows those Teams that actually have 11 or more players. This off course mean sthat you don't actually enforce the rules you ask for. But it's possible (and may I say probable) that you can afford not too. If the football players get killed in an accident for example, the team can not longer play in tournaments but it's still a team. So, you may define two entities, the Team (the base Table) and the ProperTeam (View), that can play games. Example:

CREATE VIEW AS ProperTeam
( SELECT *
  FROM Team
  WHERE ( SELECT COUNT(*)
          FROM Player
          WHERE Player.TeamId = Team.TeamId
        ) >= 11
) 

Options 1 and 2 look rather "messy" but that's only a personal opinion, many people like triggers.

I would choose Option 4, unless I can ("cheat" and) actually not enforce the constraint with Option 5.

ypercubeᵀᴹ
  • 113,259
  • 19
  • 174
  • 235
  • Option 5 and 4 are good from the database view, but most common persistence frameworks on the client side cannot cope with them - they simply expect plain `INSERT`, `UPDATE`, `SELECT` for one entity. Saying "do creation with this procedure, update with another and fetch data via a view" - have fun ;-) – A.H. Feb 13 '12 at 16:43
  • 3
    @A.H.: That's one example why most common persistence frameworks are evil crutches. Their promise is to simplify things, or even to make your app db-agnostic (which never really works). Effectively, they lock you in on the framework, which itself is guaranteed to be less portable than plain SQL and cut off half the features of your RDBMS. – Erwin Brandstetter Feb 13 '12 at 19:15
  • 1
    @ErwinBrandstetter: Persistence frameworks are like taxes: Few people like them, many hate them, (nearly) nobody can evade them in the long run. :-] – A.H. Feb 13 '12 at 19:54
  • @A.H.: Do you think that persistence frameworks will deal any better with options 1 and 2? – ypercubeᵀᴹ Feb 13 '12 at 20:02
  • @ypercube: Option 2 is transparent to all frameworks, since normal INSERT/UPDATE/DELETE/SELECT is possible (in the success case of course). Option 1 is also transparent for the frameworks but shoves much of the constraint "support" stuff into the application layer. My personal favourite is therefore option 2. It's a pity that we cannot vote for sub-options :-) – A.H. Feb 13 '12 at 20:31
  • @A.H.: Feel free to add this info about frameworks. The question does not ask about them but I think it may be useful for others. It's not a rare question. – ypercubeᵀᴹ Feb 13 '12 at 20:48
2

If you have the addresses in one table adresses, you can define a column "default_address" (in the table customers) that is a non-null foreign key reference to the address that must be provided.

If you have a table shippings that provides an optional many-to-many relationship by referencing a person, an address and maybe an order(-item), then you could use coalesce to overwrite the NULLs for addresses you get in an outer join between (customers inner join orders) and shipping_addresses (a view joining shippings with addresses). But to prevent problems with maybe different numbers of non-null components of addresses, Stephane Faroult recommends in his (strongly recommended!) book The Art of SQL to use the "hidden sort key" technique (assuming that customers_with_default_address is a view joining customers with addresses using "default_address":

select *
  from (select 1 as sortkey,
               line_1,
               line_2,
               city,
               state,
               postal_code,
               country
        from shipping_addresses
          where customer_id = ?
        union
        select 2 as sortkey,
               line_1,
               line_2,
               city,
               state,
               postal_code,
               country
        from customers_with_default_address
          where customer_id = ?
        order by 1) actual_shipping_address
      limit 1
j.p.
  • 1,031
  • 1
  • 18
  • 25
  • Ah, the idea of _default_address_ column is practical and makes sense only if one wants to enforce minimum cardinality of one (1). What I presented as _a practical situation_, as a part of the question is only one of numerous possible situations. Consider another situation of relationship between team of [Cricket](http://en.wikipedia.org/wiki/Cricket) and its players. Every team MUST have a MINIMUM of 11 players to qualify as a team. Here the minimum cardinality is eleven (11). – user1144616 Feb 13 '12 at 09:03
  • @user1144616: Please add this example to the question itself, it describes the problem very clearly. – A.H. Feb 13 '12 at 10:44
  • @A.H.: Added, along with one more example. – user1144616 Feb 13 '12 at 11:45
  • @user1144616: I had a similar problem, see my old question http://stackoverflow.com/questions/1127228/database-design-for-constraint-enforcing-pairing . You can adapt the solution (based on an idea by Alex Kuznetsov) I edited into my question to more than 2 players by replacing OtherNumberInTeam by NextNumberInTeam and adapting the check constraints (for example check(NextNumberInTeam = (NumberInTeam + 1) % TotalNumberInTeam)) – j.p. Feb 13 '12 at 14:03
1

The approach that worked for me and required a reasonable amount of coding was (translated to your team/player question):

  • create a deferrable foreign key constraint to enforce the "each player has one team" relationship.
  • create just one trigger on table team, that checks that at least n players are attached to the team. The trigger should throw an exception if a cardinality is not respected, as pointed out by AdamKG.

This will enforce the constraint, but as a side-effect this will also allow only one way of encoding a new team of players (that is, without rejecting it as a key violation)

start transaction;
set constraints all deferred;
insert player_1 with teamPK --teamPK is not yet in table team, will be below
...
insert player_n with teamPK
insert teamPK --this fires the trigger which is successful.
commit transaction; --this fires the foreign key deferred check, successful.

Note that I do that by using self-defined primary keys (for teamPK), e.g. a unique team name, so that I can know the teamPK before actually inserting the line in table team (as opposed to using the auto-incremented id).

andrewsi
  • 10,807
  • 132
  • 35
  • 51