SQL Foreign Key across multiple tables - sql

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?

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.

Related

Keep getting SQL error : ORA-02270: no matching unique or primary key for this column-list

I have the following tables:
CREATE TABLE PROPERTY
(PropertyID SMALLINT PRIMARY KEY,
Location VARCHAR(10),
)
CREATE TABLE OFFICE
(OfficeID SMALLINT,
PersonID SMALLINT,
Area VARCHAR(5),
PRIMARY KEY(OfficeID, PersonID)
)
Now, I want to create the following table but I keep getting the mentioned error (ORA-02270: no matching unique or primary key for this column-list)
CREATE TABLE FOR_RENT
(PropertyID SMALLINT,
OfficeID SMALLINT,
FOREIGN KEY(PropertyID) REFERENCES PROPERTY(PropertyID),
FOREIGN KEY(OfficeID) REFERENCES OFFICE(OfficeID),
PRIMARY KEY(PropertyID, OfficeID)
)
I want to have PropertyID and OfficeID as the foreign keys in FOR_RENT table but since OfficeID is a composite key in Office table, I belive I am getting the error. How do I fix this? Any input would be great
As Alex commented, that's most probably wrong data model. Should've been something like this:
SQL> CREATE TABLE property
2 (
3 propertyid SMALLINT PRIMARY KEY,
4 location VARCHAR (10)
5 );
Table created.
SQL> CREATE TABLE person
2 (
3 personid SMALLINT PRIMARY KEY,
4 name VARCHAR2 (20)
5 );
Table created.
SQL> CREATE TABLE office
2 (
3 officeid SMALLINT PRIMARY KEY,
4 area VARCHAR (5)
5 );
Table created.
SQL> -- people work in offices
SQL> CREATE TABLE person_x_office
2 (
3 personid SMALLINT CONSTRAINT fk_pxo_per REFERENCES person (personid),
4 officeid SMALLINT CONSTRAINT fk_pxo_off REFERENCES office (officeid),
5 CONSTRAINT pk_pxo PRIMARY KEY (personid, officeid)
6 );
Table created.
SQL> CREATE TABLE for_rent
2 (
3 propertyid SMALLINT,
4 officeid SMALLINT,
5 FOREIGN KEY (propertyid) REFERENCES property (propertyid),
6 FOREIGN KEY (officeid) REFERENCES office (officeid),
7 PRIMARY KEY (propertyid, officeid)
8 );
Table created.
SQL>
... that's the way the question has been given to me
is a poor excuse, from my point of view. You shouldn't do it in a wrong manner just because someone told you so - explain them that what they wanted isn't right and should be changed.

SQL only allow foreign keys where other column matches

I have two tables,
CREATE TABLE ActivityCodes (
ActivityCodeID INT NOT NULL PRIMARY KEY,
LocationID INT NOT NULL
);
and
CREATE TABLE LocationSettings (
LocationID INT NOT NULL PRIMARY KEY,
DefaultFooActivityCodeID INT,
FOREIGN KEY (DefaultFooActivityCodeID) REFERENCES ActivityCodes(ActivityCodeID)
);
with the foreign key relationship as indicated. Activity codes are valid only for the given LocationID and DefaultFooActivityCodeID in the LocationSettings table should be an ActivityCodeID where ActivityCodes.LocationID == LocationSettings.LocationID. How can I enforce that in SQL? Can it be done with constraints or foreign keys? Is it possible at all?
Edit: Just to add some clarification, this what valid data in these table should look like:
ActivityCodes
ActivityCodeID
LocationID
1
123
2
123
3
456
4
456
LocationSettings
LocationID
DefaultFooActivityCodeID
123
1
456
4
A location can have multiple activity codes. The default activity code for a location must be an activity code for that location. #Charlieface I tried using a composite foreign key as suggested in the answer you linked but I get an error saying LocationID on ActivityCodes is neither unique nor a primary key (I'm using MS SQL Server).
The DDL you provide, do not means what you have describe.
The DDL describe this:
The ActivityCodes are independent of LocationSettings.
The LocationSettings instead depends on an ActivityCodes (one ActivityCodes has many LocationSettings)
The foreign key is well defined with this like FOREIGN KEY (DefaultFooActivityCodeID) REFERENCES ActivityCodes(ActivityCodeID). If you try to insert a LocationSettings without first insert an ActivityCodes it will fail due constraint violation.
A foreign key reference does not have to be a primary key. This allows you to have two foreign key references to the same table, even if they are redundant:
CREATE TABLE ActivityCodes (
ActivityCodeID INT NOT NULL PRIMARY KEY,
LocationID INT NOT NULL,
UNIQUE (LocationID, ActivityCodeID)
);
CREATE TABLE LocationSettings (
LocationID INT NOT NULL PRIMARY KEY,
DefaultFooActivityCodeID INT,
FOREIGN KEY (DefaultFooActivityCodeID) REFERENCES ActivityCodes(ActivityCodeID),
FOREIGN KEY (LocationID, DefaultFooActivityCodeID) REFERENCES ActivityCodes(LocationID, ActivityCodeID)
);
Although this expresses what you want, you will find that this is a bit tricky to maintain in practice. Setting the default requires the following steps:
Insert a location with a NULL default.
Insert a row into ActivityCodes with the default activity.
Update the default in LocationSettings.

What is difference between Primary Key and Composite Key?

I Would like to know the difference between these two Queries:
1)
CREATE TABLE tblSample
(
T_Id INT PRIMARY KEY,
ID INT ,
BNO INT
)
2)
CREATE TABLE tblSample
(
T_Id INT,
ID INT,
BNO INT,
CONSTRAINT pk_id PRIMARY KEY(T_Id,ID,BNO)
)
First one is not correct, you can have only one primary key. However you can have multiple unique keys per table.
For second one you specify composite key which means that combination of all 3 values should be unique e.g.
1 2 3
1 2 4
considers as valid values even that 2 first columns have same values.

How do I create a table whose rows reference 1 (and only 1) of 2 existing tables?

Here's my situation: I have two tables created with
CREATE DATABASE JsPracticeDb;
/* Create tables corresponding to the problems, solutions to
problems, and ratings of problems or solutions */
CREATE TABLE Problems (
id INT PRIMARY KEY NOT NULL,
prompt_code VARCHAR(3000),
test_func_code VARCHAR(3000),
test_input_code VARCHAR(3000)
);
CREATE TABLE Solutions (
id INT PRIMARY KEY NOT NULL,
problem_id INT,
solver_name VARCHAR(50),
code VARCHAR(3000),
FOREIGN KEY (problem_id) REFERENCES Problems(id) ON DELETE CASCADE,
);
and I was thinking about creating a table for rating Solutions, which I wrote as
CREATE TABLE Ratings (
id INT PRIMARY KEY NOT NULL,
solution_id INT,
stars TINYINT,
FOREIGN KEY (solution_id) REFERENCES Solutions(id) ON DELETE CASCADE
);
but then I realized I might actually want to have Problems rated as well. The "brute force" solution, as I see it, is
CREATE TABLE SolutionRatings (
id INT PRIMARY KEY NOT NULL,
solution_id INT,
stars TINYINT,
FOREIGN KEY (solution_id) REFERENCES Solutions(id) ON DELETE CASCADE
);
CREATE TABLE ProblemRatings (
id INT PRIMARY KEY NOT NULL,
problem_id INT,
stars TINYINT,
FOREIGN KEY (problem_id) REFERENCES Problems(id) ON DELETE CASCADE
);
but my programming intuition says there's a problem with the fact that I used copy-paste to write two sections of code that are almost identical. However, I can't think of any alternative solution that uses an intersection table or something like that also allows me to do a cascade delete. For example, I know I could do
CREATE TABLE RatedTables (
id TINYINT PRIMARY KEY NOT NULL,
table_name VARCHAR(9)
);
INSERT INTO RatedTables (table_name) VALUES ('Problems','Solutions');
CREATE TABLE Ratings (
id INT PRIMARY KEY NOT NULL,
rated_table_id TINYINT NOT NULL,
stars TINYINT,
FOREIGN KEY (rated_table_id) REFERENCES RatedTables(id)
);
but then how would I make it so that if a Solution with corresponding Ratings was deleted then those ratings would be too?????
You basically have two options but this is a good opportunity to go back and review your db structure.
The first option is to do something like this:
CREATE TABLE potential_link1 (
id int primary key,
...
);
CREATE TABLE potential_link2 (
id int primary key,
....
);
CREATE TABLE ratings (
id int primary key,
potential_link1 int references potential_link1(id) on delete cascade,
potential_link2 int references potential_link2(id) on delete cascade,
....
check(potential_link1 is null or potential_link2 is null),
check(potential_link2 is not null or potential_link1 is not null)
);
This works but as you can see it is a bit complex.
The second possibility is that since there are clear cases where a is dependent on the union of b and c then you may think about whether you can refactor your db structure to reflect that so you only need one table to link against.
There is nothing wrong with two tables looking so much alike. They contain different things and you won't want to select all three-star ratings no matter whether on problems or solutions for instance - you would always work with solution ratings or problem ratings.
But to have both ratings in one table is also not wrong and can be a good idea when you want ratings to behave the same, no matter whether on problem or solution (e.g. both shall have 1 to 5 stars, both can have a comment no longer then 200 chars, ...).
This could be done by simply giving the ratings table both a problem_id and a solution_id with foreign keys on the tables and fill always one or the other. With natural keys, the same would feel even more, well, natural:
problem(problem_no, data)
solution(problem_no, solution_no, data)
rating(problem_no, solution_no, data)
with rating.solution_no nullable and foreign keys on both parent tables.

how do I enforce referential integrity between a many to many table and its parent tables

I have 4 tables.
Table 1 stores data for Airlines
Table 2 stores data for Destinations
Table 3 stores unique Routes(each route is a unique combination of airline and destination).
Table 4 records the prices of all flights on the routes in table 3. This is a many to many table as there are many flights by each airline to many destinations.
Table 1 = Airlines
Airline_ICAO_Code varchar(3) Not NULL Primary key,
Airline varchar(22) NULL
Table 2 = Destinations
Airport_ICAO_Code varchar(4) Not NULL Primary key,
Destination varchar(30) NULL
Table 3 = Airlines2Destinations
ID int IDENTITY(1,1) NOT NULL,
Airport_ICAO_Code varchar(4) Not NULL Foreign Key References Destinations(Airport_ICAO_Code),
Destination varchar(30) NULL,
Airline_ICAO_Code varchar(3) Not NULL Foreign Key References Airlines(Airline_ICAO_Code),
Airline varchar(22) NULL
Table 4 = Airlines2DestinationsPrices
ID int IDENTITY(1,1) NOT NULL,
Airport_ICAO_Code varchar(4) Not NULL Foreign Key References Destinations(Airport_ICAO_Code),
Destination varchar(30) NULL,
Airline varchar(22) NULL,
Airline_ICAO_Code varchar(3) Not NULL Foreign Key References Airlines(Airline_ICAO_Code),
Departure smalldatetime,
Price smallmoney
My problem is how do I enforce referential integrity between tables 3 and 4. This is necessary as the routes entered in table 4 must be present in table 3. I need advice as to what way to set up the Primary keys on Table 3 and Table 4 that will enable me to do this.
I am considering using a composite key on table 3 like this:
Primary Key(Airport_ICAO_Code,Airline_ICAO_Code)
or concatenating the Airport_ICAO_Code and Airport_ICAO_Code columns into a new column and dropping the ID columns.
If I use the Identity column as the primary key for both tables there is no guarantee that the ID for a route in Table 4 will match the ID for a route in Table 3.
Because of all these options I'm unsure of the best way forward. If someone could take the time to help out on this it would really be appreciated.
Thanks for any help offered
Edit:
After receiving advice from Marc in the post below I set up Table 4 as follows. However there is no primary key on this table. Would the ID column be better for the Primary key or could I use a Composite Key like this Primary Key (Airlines2DestinationsID,Departure).
Table 4 = Airlines2DestinationsPrices
ID int IDENTITY(1,1) NOT NULL,
Airlines2DestinationsID INT NOT NULL
FOREIGN KEY REFERENCES dbo.Airlines2Destiations(ID),
Departure smalldatetime,
Price smallmoney
Basically, if any one entry in table 4 always belongs to a single entry in table 3, I would just store the Airlines2Destiations.ID as a foreign key into table 4.
That way, each entry of table 4 is always clearly and uniquely connected to a single entry in table 3. Also: drop all the redudancy from table 4 - you only need the reference to table 3 - you don't need to repeat airline or airport codes and names.
Table 3 = Airlines2Destinations
ID int IDENTITY(1,1) NOT NULL,
Airport_ICAO_Code CHAR(4) NOT NULL
Foreign Key References Destinations(Airport_ICAO_Code),
Destination VARCHAR(30) NULL,
Airline_ICAO_Code CHAR(3) NOT NULL
Foreign Key References Airlines(Airline_ICAO_Code),
Airline VARCHAR(22) NULL
Table 4 = Airlines2DestinationsPrices
ID int IDENTITY(1,1) NOT NULL,
Airlines2DestinationsID INT NOT NULL
FOREIGN KEY REFERENCES dbo.Airlines2Destiations(ID),
Departure smalldatetime,
Price smallmoney