Database design issue. Column value pointing to different tables - sql

Description of what I have to do
I have a table that should be related to Table1 OR Table2 OR Table3
For instance, there's a table Employees and it has:
Id,
Name,
Address,
Age,
Salary,
EmployerId
The second table is RegisterEmployeeRequirements:
Id,
RequirementType,
EmployerId,
EntryId.
Where requirement type could be CreditStatusRequirement or EmployeeDegreeRequirement).
The Problem: CreditStatusRequirement includes both the CreditStatus and the date it was acquired (to check if it was in the last year). I also have additional table which is named CreditStatusRequirements with columns:
CreditStatus,
DateTimeAcquired
On the other hand, the degree requirement which has the following properties: DegreeName and MinGpa.
To solve this I created another table with these properties. If the requirement type in the RegisterEmployeeRequirements is CreditStatusRequirement I will use the entryId column to look at the CreditStatusRequirements table and then to check if it is completed.
Otherwise, if it is EmployeeDegreeRequirement, I will use the entryId column to look into DegreeRequirements table. I suppose it is not a good practice to use such a column like entryId.
What is the way to solve this architecture issue?

It's fairly simple. Don't have the FK equivalent in the RegisterEmployeeRequirements table. Have the FKs in each of the Credit and Degree requirements details tables to RegisterEmployeeRequirements.
Like this:
create table RegisterEmployeeRequirements(
EmployeeId int references ( ID ),
RequirementType char( 1 ) not null,
..., -- Other common fields
constraint PK_RegisterEmployeeRequirements primary key( EmployeeID, RequirementType ),
constraint FK_RegisterEmployeeRequirements_Empe( EmployeeId )
references Employees( ID ),
constraint FK_RegisterEmployeeRequirements_Type( RequirementType )
references RequirementTypes( ID ),
);
Notice the key is the combination of employee id and requirement id. This ensures each employee can have no more than one of each of the two defined requirements. I assume that is in line with your database requirements.
Then each requirements detail table can be defined something like this:
create table CreditRequirements(
EmployeeId int primary key,
RequirementType char( 1 ) check( CreditType = 'C' ),
Status ...,
Acquired datetime,
constraint FK_CreditRequirements_Emp foreign key( EmployeeID, RequirementType )
references RegisterEmployeeRequirements( EmployeeID, RequirementType )
);
create table DegreeRequirements(
EmployeeId int primary key,
RequirementType char( 1 ) check( DegreeType = 'D' ),
DegreeName varchar( 64 ),
MinGPA float,
constraint FK_DegreeRequirements_Emp foreign key( EmployeeID, RequirementType )
references RegisterEmployeeRequirements( EmployeeID, RequirementType )
);
An entry in the credit details table can only be made for an employee that has a credit type entry made in the RegisterEmployeeRequirements table. Same for the degree details table for a degree type entry in RegisterEmployeeRequirements. No more than one of each type of requirement can be inserted in RegisterEmployeeRequirements and only one entry for each employee can be inserted in each of the details table.
Your data integrity is sound and the design is scalable. If a third requirement type is created, the type entry is inserted in the RequirementTypes table and a new details table is created for that type. None of the existing tables would need altering.

Why not just use separate tables for credit status and degree requirements, and remove the need for a RequirementType column?
table RegisterEmployeeCreditStatusRequirements has:
Id, EmployerId, CreditStatus, DateTimeAcquired
table RegisterEmployeeEmployeeDegreeRequirements has:
Id,EmployerId,DegreeName,MinGpa

Related

Check constraint depending on another table (mutually exclusive relation)

I need help with a project, a DB for hotel administration .
I have three tables, bookings, check_ins and cancellations. I consider that any booking can end either as an cancellation or a check-in, but not both simultaneously.
The fact is that I would need a constraint to verify, at check-in, whether the booking was cancelled, and, at cancellation, if people actually came and checked-in.
I used the same id for the three PKs, but it turns out that a check constraint, in Oracle, can't have either a user-defined-function or a select clause inside.
I.e:
I have tried both
ALTER TABLE chek_ins
ADD CONSTRAINT verif_ci CHECK(id_book NOT IN (SELECT id_book FROM bookings));
and creating a function verif_cancelled(id char(a_number))
In the same time I cannot write all of them in the same table, for the guest's identification is unknown until check-in and I use the tables bookings and check-ins as a sort of two intersection tables to link the guest and the room (to solve a M:M relation).
Do you have any suggestions? (this semester I am studying SQL for the first time)
Thank you in advance!
To start with, have a table of possible booking statuses that enumerates all the possible states a booking can be in:
CREATE TABLE statuses (
id NUMBER(1,0)
GENERATED ALWAYS AS IDENTITY
CONSTRAINT statuses__id__pk PRIMARY KEY,
description VARCHAR2(10)
CONSTRAINT statuses__desc__nn NOT NULL
CONSTRAINT statuses__desc__u UNIQUE
);
INSERT INTO statuses ( description )
SELECT 'Booked' FROM DUAL UNION ALL
SELECT 'Checked-In' FROM DUAL UNION ALL
SELECT 'Annulled' FROM DUAL;
ALTER TABLE statuses READ ONLY;
Then add a status column to your bookings table:
CREATE TABLE Bookings (
id NUMBER(10,0)
GENERATED ALWAYS AS IDENTITY
CONSTRAINT bookings__id__pk PRIMARY KEY,
status NUMBER(1,0)
CONSTRAINT bookings__status__nn NOT NULL
CONSTRAINT bookings__status__fk REFERENCES statuses ( id ),
CONSTRAINT bookings__id__status__u UNIQUE ( id, status )
)
Then you can add a virtual column to your CheckIns and Annullments tables that have the appropriate statuses so that an entry cannot be made into the table unless the booking is in the correct status:
CREATE TABLE CheckIns (
id NUMBER(10,0)
CONSTRAINT CheckIns__id__pk PRIMARY KEY,
status NUMBER(1,0)
GENERATED ALWAYS AS ( 2 )
CONSTRAINT CheckIns__status__nn NOT NULL
CONSTRAINT CheckIns__status__fk REFERENCES statuses ( id ),
CONSTRAINT CheckIns__id__status__fk
FOREIGN KEY ( id, status )
REFERENCES bookings ( id, status )
);
CREATE TABLE Annullments (
id NUMBER(10,0)
CONSTRAINT annullments__id__pk PRIMARY KEY,
status NUMBER(1,0)
GENERATED ALWAYS AS ( 3 )
CONSTRAINT annullments__status__nn NOT NULL
CONSTRAINT annullments__status__fk REFERENCES statuses ( id ),
CONSTRAINT annullments__id__status__fk
FOREIGN KEY ( id, status )
REFERENCES bookings ( id, status )
);
Then if you try to enter a booking with the Checked-In status into the Annullments table it will fail (and vice versa):
DECLARE
p_id Bookings.id%type;
BEGIN
INSERT INTO bookings ( status )
VALUES( ( SELECT id FROM statuses WHERE description = 'Checked-In' ) )
RETURNING id INTO p_id;
BEGIN
INSERT INTO CheckIns( id )
VALUES ( p_id );
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE( 'Could not check-in' );
END;
BEGIN
INSERT INTO Annullments ( id )
VALUES ( p_id );
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE( 'Could not annul booking' );
END;
END;
/
outputs:
Could not annul booking
and if you try to change a booking to another status while there is a dependent entry in another table then it will fail:
UPDATE Bookings
SET status = ( SELECT id FROM statuses WHERE description = 'Annulled' )
WHERE status = ( SELECT id FROM statuses WHERE description = 'Checked-In' )
Outputs:
ORA-02292: integrity constraint (FIDDLE_CWUTVSMRLOQQQVDQLCGR.CHECKINS__ID__STATUS__FK) violated - child record found
You could further improve the API by creating a package with functions to:
create a booking;
check-in a booking;
annul a booking;
etc.
db<>fiddle
Your check-in table has a booking ID, so that the check-in refers to a booking. This booking ID has probably a unique constraint (maybe it's even the table's primary key), in order to create the desired 1:{0,1} relation. Something along the lines of:
create table check_in
(
booking_id number(9),
check_in_time date,
number_of_persons number(2),
main_person_name varchar2(100),
constraint pk_check_in primary key (booking_id),
constraint fk_check_in_booking foreign key (booking_id) references booking (booking_id)
);
Your annulment table is probably constructed in the same way. However, as these are two different tables that the booking table is not aware of, you can both insert a check-in and an annulment for a booking, which you want to avoid. A check constraint is not possible, because it can only refer to the table itself, not to other tables. (Triggers in the check-in and annulment tables that check the other table to throw an exception in case of an entry there would be possible, but I don't recommend them in this scenario.)
1:1 relatated tables are a rare thing. I see that they are useful here, because you can make their columns mandatory or optional, e.g. the check-in time and main person name may be mandatory (NOT NULL) while the number of persons may be optional. If these were just attributes directly placed in the booking table instead, you could not make check-in time and main person name mandotory, because they must only be mandatory in case of a check-in.
Luckily, with 1:1 you can simply relate them the other way round. I.e. remove the booking ID from the two satellite tables, give them independent primary keys and refer to these primary keys in the booking table instead:
create table check_in
(
check_in_id number(9),
check_in_time date,
number_of_persons number(2),
main_person_name varchar2(100),
constraint pk_check_in primary key (check_in_id)
);
create table annulment
(
annulment_id number(9),
...,
constraint pk_annulment primary key (annulment_id)
);
create table booking
(
booking_id number(9),
room_no number(3),
start_date date,
end_date date,
person_name varchar2(100),
check_in_id number(9),
annulment_id number(9),
constraint pk_booking primary key (booking_id),
constraint fk_booking_check_in foreign key (check_in_id) references check_in (check_in_id),
constraint fk_booking_annulment foreign key (annulment_id) references annulment (annulment_id),
constraint uq_booking_check_in unique (check_in_id),
constraint uq_booking_annulment unique (annulment_id),
constraint chk_booking check (check_in_id is null or annulment_id is null)
);
You see how easy it is to place the desired check constraint in the booking table now.
A last remark: While it is great to have all those integrity checks in place and be able to, say, select all annulments placed in March, you might be able to do with a much simpler database. Instead of two satellite tables, you could just put the information in single columns, e.g. a CLOB, JSON, or Oracle object. It would be like using a mere note rather than a form in real live. Less reliable, but maybe sufficient for the job. The database would reduce to a single table:
create table booking
(
booking_id number(9),
room_no number(3),
start_date date,
end_date date,
person_name varchar2(100),
check_in_data varchar2(4000),
annulment_data varchar2(4000),
constraint pk_booking primary key (booking_id),
constraint chk_booking check (check_in_data is null or annulment_data is null)
);
As mentioned, the DBMS can not guarantee here the check-in and annulment data to be complete (i.e. to the DBMS it makes no difference if annulment_data contains 'Cancelled on Feb 9, 2019 by Mr. Miller, because of sickness' or just 'Cancelled by Mr. Miller' or even 'dum deedle dum'), but maybe you are fine with this - your app will make sure only complete data gets written to the database.

Is there a way that an attribute can reference another attribute from either of 2 other tables?

Sorry for that title, english isn't my first language.
I'm wondering if there is a way to structure my tables/cardinality so that when a certain job is performed, the job_location (JOB table) will either be a home_address from the HOME Table or a work_address from the WORK table and not just a VARCHAR like it is at the moment?
I'm not sure if that makes sense, I'm new and I'm trying to see if I can do it without using things like triggers.
For example consider the picture above, where job_location can be either a home_address or a work_address, not both and not none.
As mentioned in the comments, you should have one address table. Here's why.
Many data modelers, when going through the original design, will take a human language description of the real-world objects being modeled and make lists of the nouns and adjectives. The nouns are generally the entities and the adjectives are generally the attributes. So when you come across "home address" and "work address", you should have placed "address" in the noun list and "home" and "work" in the adjective list. This means you should have a table called Addresses with one field called something like "AddressType" which would designate the address as home or work.
Because the same user (or employee or whatever) can have one home address and/or one work address, the address table would look like this:
create table Addresses(
EmpID int not null references Employees,
AddrType char( 1 ) check( AddrType in( 'H', 'W' ),
..., -- other address data
constraint PK_Addresses primary key( EmpID, AddrType )
);
So the Jobs table would look like this:
create table Jobs(
ID int identity primary key,
LocOwner int not null,
LocType char( 1 ),
..., -- other job data
constraint FK_Jobs_Location foreign key( LocOwner, LocType )
references Addresses( EmpID, AddrType )
);
I've shown the PK of the Addresses table as the combination of employee id and type (H or W). This allows the same employee to have two addresses, one of each kind. This simplifies greatly the search for all addresses associated with an employee. Notice that this is a closed solution: the address data in the job tuple must refer to an existing address in the Addresses table with the correct type designation.
However, you may be stuck with the design you have. That's unfortunate, but there is still a solution, though not as simple and straightforward as having it well designed in the first place. Here is how it would look and how to make it.
First, create an address type table like the first two columns of the address table above. However, since my design incorporated the employee id but yours has a surrogate address key, let's stick with your design.
create table AddresseTypes(
Addr_ID int not null,
Addr_Type char( 1 ) check( Addr_Type in( 'H', 'W' ),
constraint PK_AddresseTypes primary key( Addr_ID, Addr_Type )
};
Second, each address table must have a separate table with the type as shown above.
create table HomeAddresses(
Home_ID int not null references Home( Home_ID ),
Home_Type char( 1 ) check( HomeType = 'H' )
);
create table WorkAddresses(
Work_ID int not null references Work( Work_ID ),
Work_Type char( 1 ) check( WorkType = 'W' )
);
These tables contain either all the addresses listed in their respective address table or just the ones associated with a job. Copy the entries from these table into the AddresseTypes table. Then:
alter table HomeAddresses add constraint FK_HomeAddressType
foreign key( Home_ID, Home_Type )
references AddressTypes( Addr_ID, AddrType );
alter table WorkAddresses add constraint FK_WorkAddressType
foreign key( WorkID, WorkType )
references AddressTypes( Addr_ID, AddrType );
alter table Jobs add constraint FK_JobAddress
foreign key( Loc_ID, Loc_Type )
references AddressTypes( Addr_ID, AddrType );
No entry can exist in HomeAddresses or WorkAddresses that do not exist in Home or Work respectively and in AddressTypes. No Job entry can refer to an address that is not listed in AddressTypes.
Unfortunately, this is not a closed solution: AddressTypes can contain spurious entries not found in either HomeAddress or WorkAddresses. This makes for extra maintenance effort, but it wouldn't be difficult.
To query a job and get the address from whichever table contains it, the query has to outer join with both Home and Work tables.
select j.*,
case t.Addr_Type -- repeated for each address field
when 'W' then w.Street
else h.Street end as Street,
case t.Addr_Type
when 'W' then w.City
else h.City end as City,
case <etc.>
from Jobs j
join AddressTypes t
on t.Addr_ID = j.Loc_ID
and t.Addr_Type = j.Loc_Type
left join HomeAddresses ha
on ha.Home_ID = t.Addr_ID
and ha.Home_Type = t.Addr_Type
left join WorkAddresses wa
on wa.Work_ID = t.Addr_ID
and wa.Work_Type = t.Addr_Type
left join Home h
on h.Home_ID = t.Addr_ID
left join Work w
on w.Work_ID = t.Addr_ID;
If the address data shows up as a series of NULLs, it means the entry in AddressTypes is spurious. Easily fixed. As the same address id can exist in both Home and Work, the case statements select the correct source.
Obviously, you are much better off if you are free to change the design.

Architecture for audits

I am designing a database to capture audits that my company performs. I am having a bit of trouble finding an efficient way to capture all of the audit points without making 60 columns in a single table. Is there an efficient way to capture multiple data points in a single column and still be able to query without trouble.
Each audit may have anywhere from 0 to 60 unique citations. I will make a reference table to hold every regulatory citation, but how do I design the central table so that the 'citation' column can have , or , or any number of other combinations?
I usually try to keep auditing info in a single table.
In order to do this, I go something like this:
TABLE: Audit
**Id** (PK)
**EntityClass** (the Class, or type, or whatever you want to identify your entities by)
**EntityId** (the id of the entity in it's own table)
**PropertyChanged** (the name of the property of the entity that changed)
**OldValue** (the old value of the property)
**NewValue** (the revised value of the property)
**TimeStamp** (moment of the revision)
**RevisionType** (transaction type: Insert, Update, Delete)
This is the simplest schema, you can build on that with additional columns if you wish.
Hope this helps. Cheers!
In this example, I'm assuming, since you refer to a specific number if citations, there is -- or can be -- a taxonomic table holding 60 definitions or references, one for each kind of citation.
The Audits table contains the relevant info about each audit. I'm guessing most of this, but note there is no reference to any citation.
create table Audits(
ID int identity( 1, 1 ),
Started date,
Completed date,
CustomerID int,
AuditorID int, -- More than one possible auditor? Normalize.
VerifierID int,
Details ...,
constraint PK_Audits primary key( ID ),
constraint FK_Audits_Customer( foreign key( CustomerID )
references Customers( ID ),
constraint FK_Audits_Auditor( foreign key( AuditorID )
references Auditors( ID ),
constraint FK_Audits_Verifier( foreign key( VerifierID )
references Auditors( ID ),
constraint CK_Audits_Auditor_Verifier check( AuditorID <> VerifierID )
);
The AuditCitations table contains each citation for each audit, one entry for each citation. Note that the PK will prevent the same audit from having more than one reference to the same citation (if, of course, that is your rule).
create table AuditCitations(
AuditID int,
CitID int,
Details ...,
constraint FK_AuditCitations_Audit( foreign key( AuditID )
references Audits( ID ),
constraint FK_AuditCitations_Citation( foreign key( CitID )
references Citations( ID ),
constraint PK_AuditCitations primary key( AuditID, CitID )
);
A citation may well have its own auditor and verifier/checker or just about anything that applies to the particular citation. This example mainly just shows the relationship between the two tables.

Schema for db to store current and edit history of multi-field data

I'm trying to design a database schema to keep records on for sale / want ads. I'd like to be able to query, for instance "what was the asking price of posts with this keyword in the title for the past 6 months?"
But the posts themselves can be updated and modified by the posters, so I'd also like to be able to ask questions like, "What revisions did this specific post go through? How did the asking price change?"
My plan with the schema below was to have separate tables for each field I took from the post, keyed by post id and the time they were last updated. I'd also have one table (the last one) that showed how the post looked at each update.
So if there was a post with id, date, price, title, and description, there would be one entry in the post_id, post_date, post_price, post_title, and description tables, as well as one in the post_state table. If the user updated just the price, new entries would be added to the post_date, post_price and post_states table, and the new row in the post_states table would show the new price, but the original values in the other fields.
Schema follows. The problem I'm getting is that the post_states table cannot be created, because the fields in the tables it references are not unique. I'm new at this, and it may be that I'm doing it completely wrong. Also, I'm using PostgreSQL.
CREATE TABLE IF NOT EXISTS post_id (
id integer PRIMARY KEY,
url varchar
);
CREATE TABLE IF NOT EXISTS modify_date (
mod_date time,
pid integer REFERENCES post_id,
UNIQUE (mod_date, pid),
PRIMARY KEY(mod_date, pid)
);
CREATE TABLE IF NOT EXISTS post_price (
pid integer,
mod_date time,
price money,
FOREIGN KEY(mod_date, pid) references modify_date
);
CREATE TABLE IF NOT EXISTS post_title (
pid integer,
mod_date time,
title varchar,
FOREIGN KEY(mod_date, pid) references modify_date
);
CREATE TABLE IF NOT EXISTS post_location (
pid integer,
mod_date time,
location varchar,
FOREIGN KEY(mod_date, pid) references modify_date
);
CREATE TABLE IF NOT EXISTS post_email (
pid integer,
mod_date time,
email varchar,
FOREIGN KEY(mod_date, pid) references modify_date
);
CREATE TABLE IF NOT EXISTS post_description (
pid integer,
mod_date time,
description varchar,
FOREIGN KEY(mod_date, pid) references modify_date
);
CREATE TABLE IF NOT EXISTS post_state (
pid integer,
mod_date time,
title varchar REFERENCES post_title(title),
description varchar REFERENCES post_description(description),
price money REFERENCES post_price(price),
location varchar REFERENCES post_location(price),
email varchar REFERENCES post_email(email),
url varchar REFERENCES post_id(url),
FOREIGN KEY(mod_date, pid) references modify_date
);
Two quick points.
For revision tracking as long as you have a date tracked and a revision number, I think you would do well to look at table_log, which can be used to log previous versions of the log.
Secondly not having primary keys on most of your tables is asking for trouble down the road. I prefer natural primary keys where applicable but you should at least add a surrogate key if this is not possible.

How to add a unique constraint using a column from another table?

I have 3 tables in SQL Server 2008 R2 that look like these:
A COMPANY may have many LSPs. An LSP may have many SERVICEs.
And I need to make sure that SERVICE_CODE uniquely identifies a SERVICE record within a COMPANY. In other words, COMPANY_ID + SERVICE_CODE should uniquely identify a SERVICE record in the entire system.
For example: COMPANY-A may NOT have 2 services (with 2 different SERVICE_IDs) with the same SERVICE_CODE. But COMPANY-A and COMPANY-B may both have 2 separate SERVICES (again, with different SERVICE_IDs) with SERVICE_CODE = "PREMIUM".
I need something like this:
alter table "SERVICE"
add constraint "SERVICE_Index01"
unique ("COMPANY_ID", "SERVICE_CODE")
But (obviously) this fails because the COMPANY_ID column is not in the SERVICE table.
Thanks in advance for any help.
You could use an indexed view as an external constraint:
CREATE VIEW dbo.CompanyServices
WITH SCHEMABINDING
AS
SELECT
c.COMPANY_ID,
s.SERVICE_CODE
FROM dbo.COMPANY c
INNER JOIN dbo.LSP l ON c.COMPANY_ID = l.COMPANY_ID
INNER JOIN dbo.SERVICE s ON l.LSP_ID = s.LSP_ID
GO
CREATE UNIQUE CLUSTERED INDEX UQ_CompanyServices
ON dbo.CompanyServices (COMPANY_ID, SERVICE_CODE);
The index will make sure there's no duplicates of (COMPANY_ID, SERVICE_CODE) in your data.
Is each company limited to a single LSP? Is Service_Code unique (or could there be two service codes "PREMIUM" with different Service_IDs)?
CREATE TABLE dbo.Company
(
CompanyID INT PRIMARY KEY
-- , ...
);
CREATE TABLE dbo.LSP
(
LSPID INT PRIMARY KEY,
CompanyID INT FOREIGN KEY REFERENCES dbo.Company(CompanyID) -- UNIQUE?
-- , ...
);
CREATE TABLE dbo.Service
(
ServiceID INT PRIMARY KEY
-- , ...
);
CREATE TABLE dbo.LSP_Service
(
LSPID INT FOREIGN KEY REFERENCES dbo.LSP(LSPID),
ServiceID INT FOREIGN KEY REFERENCES dbo.Service(ServiceID),
PRIMARY KEY (LSPID, ServiceID)
);
Add COMPANY_ID to service table.
If you need rows in Service table to be unique by this id it makes sense to keep a foreign key reference in this table.