Search code examples
sqlconstraintscheck-constraintscheck-constraint

How to constrain the number of records allowed in an SQL table?


Say I have two tables, Parent and Child. Parent has a MaxChildren (int) field and Child has an Enabled (bit) field and a ParentID (int) field linking back to the parent record.

I'd like to have a constraint such that there can't be more than MaxChildren records for each parent where Enabled = 1. This would mean that any attempt to insert or update any record in the Child table will fail if it goes over the applicable MaxChildren value, or any attempt to lower MaxChildren to below the current number of applicable Child records will fail.

I'm using MS SQL Server, but I'm hoping there's a standard SQL way.


Solution

  • This is Standard SQL-92 entry level syntax i.e. uses 'vanilla' syntax such as foreign keys and row level CHECK constraints that are widely implemented in SQL products (though notably not mySQL):

    CREATE TABLE Parent
    (
     ParentID INTEGER NOT NULL, 
     MaxChildren INTEGER NOT NULL
        CHECK (MaxChildren > 0), 
     UNIQUE (ParentID),
     UNIQUE (ParentID, MaxChildren)
    );
    
    CREATE TABLE Child
    (
     ParentID INTEGER NOT NULL, 
     MaxChildren INTEGER NOT NULL, 
     FOREIGN KEY (ParentID, MaxChildren)
        REFERENCES Parent (ParentID, MaxChildren)
        ON DELETE CASCADE
        ON UPDATE CASCADE, 
     OccurrenceNumber INTEGER NOT NULL, 
     CHECK (OccurrenceNumber BETWEEN 1 AND MaxChildren), 
     UNIQUE (ParentID, OccurrenceNumber)
    );
    

    I suggest you avoid using bit flag columns. Rather, you could have a second table without the restriction on MaxChildren then imply the Enabled column based on which table a row appears in. You'd probably want three tables to model this: a supertype table for all children with a subtype tables for Enabled. You could then create a VIEW to UNION the two subtypes with an implied Enabled column e.g.

    CREATE TABLE Parents
    (
     ParentID INTEGER NOT NULL, 
     MaxChildren INTEGER NOT NULL
        CHECK (MaxChildren > 0), 
     UNIQUE (ParentID),
     UNIQUE (ParentID, MaxChildren)
    );
    
    CREATE TABLE Children
    (
     ChildID INTEGER NOT NULL, 
     ParentID INTEGER NOT NULL, 
     MaxChildren INTEGER NOT NULL, 
     FOREIGN KEY (ParentID, MaxChildren)
        REFERENCES Parents (ParentID, MaxChildren)
        ON DELETE CASCADE
        ON UPDATE CASCADE, 
     UNIQUE (ChildID), 
     UNIQUE (ChildID, MaxChildren),  
    );
    
    CREATE TABLE EnabledChildren
    (
     ChildID INTEGER NOT NULL, 
     MaxChildren INTEGER NOT NULL, 
     FOREIGN KEY (ChildID, MaxChildren)
        REFERENCES Children (ChildID, MaxChildren)
        ON DELETE CASCADE
        ON UPDATE CASCADE, 
     OccurrenceNumber INTEGER NOT NULL, 
     CHECK (OccurrenceNumber BETWEEN 1 AND MaxChildren), 
     UNIQUE (ChildID)
    );
    
    CREATE VIEW AllChildren
    AS
    SELECT ChildID, 1 AS ENABLED
      FROM EnabledChildren
    UNION
    SELECT ChildID, 0 AS ENABLED
      FROM Children
    EXCEPT
    SELECT ChildID, 0 AS ENABLED
      FROM EnabledChildren;