SQL Constraint to check the value of another table's column - sql

Suppose I have 2 tables, Employee and Sale.
Employee table has EMP_ID int as PK, and ACTIVE_STATUS bit (0 for inactive and 1 for active).
Sale table has SALE_ID as PK, EMP_ID as FK referencing Employee.EMP_ID and DATE_OF_SALE
Now, I want a constraint that checks if the EMP_ID I'm trying to insert into Sale has a value of 1 for the ACTIVE column in the Employee table, because I don't want to register a sale that is being attempted by an Inactive user.
How would I go about that? I tried CONSTRAINT CHECK_IF_ACTIVE CHECK(Employee.ACTIVE = 1) but it's not a valid statement.

You can do what you are specifically asking for using foreign keys and computed columns. First, define a redundant unique constraint in employees:
alter table employees add constraint unq_employees_empid_active_status on (empid, active_status);
Then, define a computed column in sales. Alas, this needs to be persisted, I think:
alter table sales add active_status as (convert(bit, 1)) persisted;
Then, define the foreign key constraint using both:
alter table sales add foreign key fk_sales_employees_active
foreign key (empid, active_status) references employees(empid, active_status);
Voila! The employee id can only reference active employees.
Now, you will have a problem with this -- be careful what you ask for. It is not really what you want. This enforces the constraint over all time. So, you won't be able to change the status on employees who have sales. That suggests that you need an insert trigger instead -- or a user defined function and check constraint:
create function is_employee_active (
#empid int
) returns bit as
begin
return (select active_status from employees e where e.empid = #empid);
end;
alter table sales add constraint chk_sales_employee_active
check (is_employee_active(empid) = convert(bit, 1));
Voila! This only does the check on insertion or updates. Note that once an employee is not active, you won't be able to update the row either.
You'll notice that I usually name my tables in the plural, because they contain lots of examples of an entity. My fingers just add the "s" when I'm thinking about tables.

Related

Foreign key without correspondence in the referenced table

I'm trying to come up with a solution for an exercise in which I have to create 3 tables: one for employees, one for projects, and one for projects and employees, in which I have to insert the employee's ID and associate with a project ID. In the project_employee table, since it has only 2 columns that reference other tables, I thought I could set them bot as foreign keys, like this:
CREATE TABLE employee_project
(
id_employee numeric FOREIGN KEY REFERENCES employee(id_employee),
id_project numeric FOREIGN KEY REFERENCES project(id_project)
)
Then I stumbled upon a problem: when inserting values on the 3 tables, I noticed that one of the employee's ID number was 4, but on the table employee there was no line with ID 4. Of course, this line wasn't created, but I want to understand: is there a way I could create a line whose ID has no matching record in the referenced table? Could it be a possible mistake in the question, or is there something I'm missing? Thanks in advance for your time!
If there is no rows in employee table with id_employee value 4 then there should not be rows in employee_project table with id_employee value 4. SQL Server will give you an error like below:
The INSERT statement conflicted with the FOREIGN KEY constraint "FK__employee__id__75F77EB0". The conflict occurred in database "test", table "dbo.employee", column 'id_employee'.
But if you want to create employee_project with composite primary key on both the column you can try this:
CREATE TABLE employee_project
(
id_employee int not null,
id_project int not null,
primary key(id_employee, id_project )
)

How to link three tables?

I'm new to SQL and ask for your help.
There are 3 tables, these are "Employees", "Positions" and "EmployeesPositions".
For example, 2 positions can be attached to one employee.
How to link tables so that duplicates do not occur? I read about foreign keys and JOIN, but I have not yet figured out how to do it correctly.
Table structure:
Employees (id, Name);
Positions (id, Post, Rate); EmployeesPositions - I do not know how to make it right.
What I need: when adding an employee to the "Employees" table, associate an entry with posts from the "Positions" table, but as I wrote above, one employee can be associated with 2 posts (but not always). How correctly to implement the third table (EmployeesPositions), because in Positions only posts and rates are stored, and in EmployeesPositions there should be records, for example, Name1 => Post1 and Post2, and Name2 only Post 1?
If I thought something wrong, tell me please how best to implement it.
There are several ways to solve your problem, each with their own pros and cons.
First if we simplify your problem to "an employee has zero or more positions", then you can use the following table to associate an employee with a position:
create table employeespositions (
employee_id integer not null,
position_id integer not null,
constraint pk_employeespositions
primary key (employee_id, position_id),
constraint fk_employeespositions_employee
foreign key (employee_id) references employees (id),
constraint fk_employeespositions_position
foreign key (position_id) references positions (id)
)
The foreign keys enforce the existence of the employee and the position, while the primary key ensures a combination of employee and position only exists once.
This solution has two downsides:
It does not enforce that an employee has at least one position
It allows an employee to have more than two positions
The second problem is easily fixed by adding a trigger that checks if there is at most 1 position for an employee when attempting to insert (this allows a maximum of two):
create exception tooManyPositions 'Too many positions for employee';
set term #;
recreate trigger employeespositions_bi
active before insert on employeespositions
as
declare position_count integer;
begin
select count(*)
from employeespositions
where employee_id = new.employee_id
into position_count;
if (position_count > 1) then
exception tooManyPositions;
end#
set term ;#
However this solution does not enforce that an employee has at least one position. You could add a before delete trigger that ensures that the last position cannot be deleted, but that does not ensure that a newly created employee has at least one position. If you want to enforce that, you may want to consider using stored procedures for inserting and updating employees and their positions, and have the code of those stored procedures enforce that (eg by requiring a position when creating an employee).
Alternatively, you could also consider denormalizing your design, and making the positions part of the employees record, where the employee has a 'primary' and (optionally) a 'secondary' position.
create table employees (
-- using Firebird 3 identity column, change if necessary
id integer generated by default as identity primary key,
name varchar(100),
primary_position_id integer not null,
secondary_position_id integer,
constraint fk_employees_primary_position
foreign key (primary_position_id) references positions (id),
constraint fk_employees_secondary_position
foreign key (secondary_position_id) references positions (id),
constraint chk_no_duplicate_position
check (secondary_position_id <> primary_position_id)
)
The not null constraint on primary_position_id enforces the existence of this position, while the check constraint prevents assignment of the same position to both columns. Optionally you could consider adding a before insert or update trigger that when primary_position_id is set null, will set it to the value of secondary_position_id and sets secondary_position_id to null.
This solution has the advantage of allowing the enforcement of the existence of a primary position, but may lead to additional complexities when querying positions. This disadvantage can be overcome by creating a view:
create view employeespositions
as
select id as employee_id, primary_position_id as position_id
from employees
union all
select id as employee_id, secondary_position_id as position_id
from employees
where secondary_position_id is not null;
This view can then be used as if it is a table (although you can't insert into it).

How to reference foreign key from more than one column (Inconsistent values)

I Have table three tables:
The first one is emps:
create table emps (id number primary key , name nvarchar2(20));
The second one is cars:
create table cars (id number primary key , car_name varchar2(20));
The third one is accounts:
create table accounts (acc_id number primary key, woner_table nvarchar2(20) ,
woner_id number references emps(id) references cars(id));
Now I Have these values for selected tables:
Emps:
ID Name
-------------------
1 Ali
2 Ahmed
Cars:
ID Name
------------------------
107 Camery 2016
108 Ford 2012
I Want to
Insert values in accounts table so its data should be like this:
Accounts:
Acc_no Woner_Table Woner_ID
------------------------------------------
11013 EMPS 1
12010 CARS 107
I tried to perform this SQL statement:
Insert into accounts (acc_id , woner_table , woner_id) values (11013,'EMPS',1);
BUT I get this error:
ERROR at line 1:
ORA-02291: integrity constraint (HR.SYS_C0016548) violated - parent key not found.
This error occurs because the value of woner_id column doesn't exist in cars table.
My work require link tables in this way.
How Can I Solve This Problem Please ?!..
Mean: How can I reference tables in previous way and Insert values without this problem ?..
One-of relationships are tricky in SQL. With your data structure here is one possibility:
create table accounts (
acc_id number primary key,
emp_id number references emps(id),
car_id number references car(id),
id as (coalesce(emp_id, car_id)),
woner_table as (case when emp_id is not null then 'Emps'
when car_id is not null then 'Cars'
end),
constraint chk_accounts_car_emp check (emp_id is null or car_id is null)
);
You can fetch the id in a select. However, for the insert, you need to be explicit:
Insert into accounts (acc_id , emp_id)
values (11013, 1);
Note: Earlier versions of Oracle do not support virtual columns, but you can do almost the same thing using a view.
Your approach should be changed such that your Account table contains two foreign key fields - one for each foreign table. Like this:
create table accounts (acc_id number primary key,
empsId number references emps(id),
carsId number references cars(id));
The easiest, most straightforward method to do this is as STLDeveloper says, add additional FK columns, one for each table. This also bring along with it the benefit of the database being able to enforce Referential Integrity.
BUT, if you choose not to do, then the next option is to use one FK column for the the FK values and a second column to indicate what table the value refers to. This keeps the number of columns small = 2 max, regardless of number of tables with FKs. But, this significantly increases the programming burden for the application logic and/or PL/SQL, SQL. And, of course, you completely lose Database enforcement of RI.

Why is a "references" privilege required?

In order to create a FOREIGN KEY Constraint, the <AuthorizationID> that owns the referencing Table must be the current <AuthorizationID> and must have the REFERENCES Privilege on every referenced Column named. Source: https://mariadb.com/kb/en/constraint_type-foreign-key-constraint/
WHY?
The reason for the references privilege is that the table referencing another imposes a constraint on the other table, and the owner of that other table may not be willing to allow that constraint.
Let's see what could happen if there was no references privilege.
CREATE TABLE Employees
(
EmpID INTEGER NOT NULL PRIMARY KEY,
Name VARCHAR(32) NOT NULL,
…
);
INSERT INTO Employees VALUES (1, "Hernandez", …);
Now along comes Joe and creates a table:
CREATE TABLE EmployeeRefs
(
EmpID INTEGER NOT NULL PRIMARY KEY REFERENCES Employees
);
INSERT INTO EmployeeRefs SELECT EmpID FROM Employees;
Now suppose that the HR department needs to let Hernandez go and work elsewhere, and the code knows that the relevant EmpID is 1:
DELETE FROM Employees WHERE EmpID = 1;
This delete fails; the primary key value is still being referenced from the EmployeeRefs table.
This is what is supposed to happen as far as the DBMS is concerned. It was told that values in the EmployeeRefs.EmpID column must match a value in the Employee.EmpID column, and if it deletes Hernandez's record, that won't be true, so it must prevent the deletion (since Joe didn't set up a cascading delete).
Clearly, if any user (with privilege to create tables in the DB) can prevent the HR department from doing its work, there is a problem. The problem is resolved by saying that Joe can only create the reference to the Employee table if he has been given the REFERENCES privilege. And the owner/creator of each table gets to choose who has REFERENCES privilege for the tables that they create.

How do I check constraints between two tables when inserting into a third table that references the other two tables?

Consider this example schema:
Customer ( int CustomerId pk, .... )
Employee ( int EmployeeId pk,
int CustomerId references Customer.CustomerId, .... )
WorkItem ( int WorkItemId pk,
int CustomerId references Customer.CustomerId,
null int EmployeeId references Employee.EmployeeId, .... )
Basically, three tables:
A customer table with a primary key and some additional columns
A employee table with a primary key, a foreign key constraint reference to the customer tables primary key, representing an employee of the customer.
A work item table, which stores work done for the customer, and also info about the specific employee who the work was performed for.
My question is. How do I, on a database level, test if an employee is actually associated with a customer, when adding new work items.
If for example Scott (employee) works at Microsoft (customer), and Jeff (employee) works at StackOverflow (customer), how do I prevent somebody from adding a work item into the database, with customer = Microsoft, and employee = Jeff, which do not make sense?
Can I do it with check constraints or foreign keys or do I need a trigger to test for it manually?
Should mention that I use SQL Server 2008.
UPDATE: I should add that WorkItem.EmployeeId can be null.
Thanks, Egil.
Wouldn't a foreign key on a composite column (CustomerId, EmployeeId) work?
ALTER TABLE WorkItem
ADD CONSTRAINT FK_Customer_Employee FOREIGN KEY (CustomerId, EmployeeId)
REFERENCES Employee (CustomerId, EmployeeId);
You might be able to do this by creating a view "WITH SCHEMABINDING" that spans those tables and enforces the collective constraints of the individual tables.
Why do you want employeeId to be null int WorkItem? Maybe you should add another table to avoid that particular oddity. From what I can see the easiest thing to do is to add a unique constraint on employeeid in workItem, and maybe even unique on customerId if that is what you want.
A more general way to add constraints spanning many tables is to define a view that should always be empty, and add the constraint that it is empty.
What are you trying to model here?
You're a contracting agency or the like, and you have a bunch of contractors who are (for some period of time) assigned to a customer.
You're actually storing information about other company's employees (maybe you're providing outsources payroll services, for example).
In case (1), it looks like you have a problem with the Employee table. In particular, when Scott's contract with MS is up and he gets contracted to someone else, you can't keep the historical data, because you need to change the CustomerId. Which also invalidates all the WorkItems. Instead, you should have a fourth table, e.g., CustomerEmployee to store that. Then WorkItem should reference that table.
In case (2), your primary key on Employee should really be CustomerId, EmployeeId. Two customers could have the same employee ID number. Then Kieron's foreign key will work.
I recently pass to a similar situation, consider the schema:
Table company (id_cia PK) Table product_group (id_cia FK to company, id_group PK) Table products (id_group FK to product_group, id_product PK, id_used_by_the_client null)
Rule: The database must allow only one id_used_by_the_client for each product of a company but this filed can be null. Example:
Insert into company (1) = allowed
Insert into company (2) = allowed
Insert into product_group (1, 1) = allowed
Insert into product_group (1,2) = allowed
Insert into product_group (2,3) = allowed
Insert into products values (1, 1, null) = allowed
Insert into products values (1, 2, null) = allowed
Insert into products values (1, 3, 1) = allowed
Insert into products values (1, 4, 1) = not allowed, in the group 1 that belongs to company 1 already exists an id_used_by_the_client = 1.
Insert into products values (2, 4, 1) = not allowed, in the group 2 that belongs to company 1 already exists an id_used_by_the_client = 1.
Insert into products values (3, 4, 1) = allowed, in the group 3 that belongs to company 2 there is no id_used_by_the_client = 1.
I decided to use a trigger to control this integrity.
Either:
make the EmployeeID column the Primary Key of Employee (and possibly an auto-id) and store the EmployeeID in the WorkItem record as a foreign key, instead of storing the Employee and Customer IDs in WorkItem. You can retrieve a WorkItem's Customer details by joining to the Customer table via the Employee table.
Or:
make the WorkItem's EmployeeID and CustomerID columns a composite foreign key to Employee.
I favour the first approach, personally.