Tim Cooke
Tim Cooke

Reputation: 872

SQL Foreign Key across multiple tables

I am modeling inspections. Inspection forms are arbitrary, and essentially are a list of fields.

CREATE TABLE InspectionForms
(
    InspectionFormId INT PRIMARY KEY,
    InspectionFormName NVARCHAR(64),

    ... (CreatorId, TimeCreated, etc.)
);
CREATE TABLE InspectionFormFields
(
    InspectionFormId INT FOREIGN KEY REFERENCES InspectionForms,
    InspectionFormFieldId INT,
    FieldName NVARCHAR(64),
    PRIMARY KEY(InspectionFormId, InspectionFormFieldId)
);

So, with an InspectionForm named Bathroom, with fields for Toilet, Shower, and Floor, we would have values that look like this:

InspectionForms
---------------
0    Bathroom


InspectionFormFields
--------------------
0    0    Toilet
0    1    Shower
0    2    Floor

Then there are actual completed Inspections:

CREATE TABLE Inspections
(
    InspectionId INT PRIMARY KEY,
    InspectionFormId INT FOREIGN KEY REFERENCES InspectionForms,

    ... (InspectorId, TimeOfInspection, etc.)
);

CREATE TABLE InspectionValues
(
    InspectionId INT FOREIGN KEY REFERENCES Inspections,
    InspectionFormFieldId INT,
    Rating TINYINT NOT NULL,
    PRIMARY KEY(InspectionId, InspectionFormFieldId)
);

Here are some sample values:

Inspections
-----------
0    0    ...


InspectionValues
----------------
0    0    5    (Inspection 0 scored a 5 in the Toilet)
0    1    3    (Inspection 0 scored a 3 in the Shower)
0    2    4    (Inspection 0 scored a 4 in the Floor)

Here's the kicker: I want InspectionValues to have a FOREIGN KEY referencing InspectionFormFields. But it doesn't have an InspectionFormId column. I can think of two theoretical solutions, but I don't know how to implement either one.

Solution 1: I could simply move the InspectionFormId column from Inspections to InspectionValues, and add my foreign key. That would make our table look like this:

CREATE TABLE InspectionValues
(
    InspectionId INT FOREIGN KEY REFERENCES Inspections,
    InspectionFormId INT FOREIGN KEY REFERENCES InspectionForms,
    InspectionFormFieldId INT,
    Rating TINYINT NOT NULL,
    PRIMARY KEY(InspectionId, InspectionFormId, InspectionFormFieldId),
    FOREIGN KEY(InspectionFormId, InspectionFormFieldId) REFERENCES InspectionFormFields
);

If I do this, I want to somehow enforce that all InspectionValues with a given InspectionId share a common value for InspectionFormId (i.e. I don't want an Inspection to span across multiple InspectionForms). An easy, efficient way to do this would be to make sure that on each update, this query doesn't return any rows:

SELECT InspectionId
FROM InspectionValues a
GROUP BY InspectionId
HAVING MIN(InspectionFormId) < MAX(InspectionFormId);

Solution 2: The InspectionValues table stores a reference to a specific Inspection, which in turn stores a reference to a specific InspectionForm. I could simply create a foreign key pairing InspectionFormFields with InspectionValue-Inspection combinations

If that is not possible, perhaps I could somehow enforce on every update that this query returns no rows:

SELECT *
FROM InspectionValues a
JOIN Inspections b ON a.InspectionId = b.InspectionId
JOIN InspectionForms c ON b.InspectionFormId = c.InspectionFormId
WHERE NOT EXISTS (
    SELECT *
    FROM InspectionFormFields d
    WHERE a.InspectionFormFieldId = d.InspectionFormFieldId AND b.InspectionFormId = d.InspectionFormId
);

I'm using SQL Server 2014, and don't need to support any other version of SQL. What's the right thing to do here?

Upvotes: 1

Views: 163

Answers (1)

dan b
dan b

Reputation: 1172

You should leave InspectionFormId in the Inspections table (rather then move it as you suggest). Since InspectionId is the primary key you cannot have only one inspectionformid per inspectionid.

CREATE TABLE Inspections
(
InspectionId INT PRIMARY KEY,
InspectionFormId INT FOREIGN KEY REFERENCES InspectionForms,
);

Then add the inspectionformid to the inspectionvalues table:

CREATE TABLE InspectionValues
(
InspectionId INT FOREIGN KEY REFERENCES Inspections,
InspectionFormId INT,
InspectionFormFieldId INT,
Rating TINYINT NOT NULL,
PRIMARY KEY(InspectionId, InspectionFormId, InspectionFormFieldId),
FOREIGN KEY(InspectionFormId, InspectionFormFieldId) 
       REFERENCES InspectionFormFields(InspectionFormId, InspectionFormFieldId)

);

In your solution1 above, InspectionValues you had two foreign keys containing InspectionFormId, but you only need the multi-column one as show above.

Upvotes: 2

Related Questions