Maximum Constraint on One to Many Relationship - Oracle SQL
This cannot be done with a check constraint. It should be possible to create a materialized view that count the number of occurences of each manager, with a check constraint on the count, and that refreshes on commit on the original table. The same can be implemented with a compound trigger, as demonstrated by Littlefoot. But this is not very scalable, since the whole table needs be scanned to refresh the materialized view after each commit.
One alternative solution would be to:
create a new table that keeps track of the number of occurences of each manager, say
employee_manager_cnt
set up a trigger on the
employee
table to keep tableemployee_manager_cnt
up to date (no need to scan the whole table, just reflect the changes based on the old and new value ofmanager_id
)add a check constraint to the
employee_manager_cnt
that forbids values above the target count
Here is a step by step demo, which is inspired by the answer by nop77svk on this SO question
Original table:
create table employees (
employee_id number primary key,
manager_id number
);
Insert a few records:
begin
insert into employees values(1, null);
insert into employees values(2, 1);
insert into employees values(3, 1);
insert into employees values(4, 1); -- manager 1 has 3 employees
insert into employees values(5, null);
insert into employees values(6, 5); -- manager 5 has just 1 employee
end;
/
Create the new table:
create table employee_manager_cnt (
manager_id number not null primary key,
cnt number(1, 0) not null check (cnt <= 3)
);
Populate it:
insert into employee_manager_cnt(manager_id, cnt)
select manager_id, count(*)
from employees
where manager_id is not null
group by manager_id
Check the results:
MANAGER_ID CNT
1 3
5 1
Now, create the trigger:
create or replace trigger trg_employee_manager_cnt
after insert or delete or update of manager_id
on employees
for each row
begin
-- decrease the counter when an employee changes manager or is removed
if updating or deleting then
merge into employee_manager_cnt t
using dual
on ( t.manager_id = :old.manager_id )
when matched then
update set t.cnt = t.cnt - 1
delete where t.cnt = 0
;
end if;
-- increase the counter when a employee changes manager or is added
if inserting or updating then
merge into employee_manager_cnt T
using dual
on ( t.manager_id = :new.manager_id )
when matched then
update set t.cnt = t.cnt + 1
when not matched then
insert (manager_id, cnt) values (:new.manager_id, 1)
;
end if;
end;
/
Now try to add a new record that references manager 1 (who already has 3 employees)
insert into employees values(4, 1);
-- error: ORA-00001: unique constraint (FIDDLE_QOWWVSAIOXRDGYREFVKM.SYS_C00276396) violated
Meanwhile it is still possible to affect a new employee to manager 5 (he just has one employee):
insert into employees values(10, 5);
-- 1 rows affected
In order to find number of employees per manager in the table, you have to count them, right? But, if you do that, you'll hit the mutating table error as you can't select from a table which is currently being changed.
Nowadays, we fix that using the compound trigger. Here's an example:
Sample table:
SQL> create table temp
2 (empid number primary key,
3 name varchar2(20),
4 mgrid number references temp (empid)
5 );
Table created.
Compound trigger:
SQL> create or replace trigger trg_3emp
2 for update or insert on temp
3 compound trigger
4
5 type emprec is record (mgrid temp.mgrid%type);
6 type row_t is table of emprec index by pls_integer;
7 g_row_t row_t;
8
9 after each row is
10 begin
11 g_row_t (g_row_t.count + 1).mgrid := :new.mgrid;
12 end after each row;
13
14 after statement is
15 l_cnt number;
16 begin
17 for i in 1 .. g_row_t.count loop
18 select count(*)
19 into l_cnt
20 from temp
21 where mgrid = g_row_t(i).mgrid;
22
23 if l_cnt = 4 then
24 raise_application_error(-20000, 'No more than 3 employees per manager');
25 end if;
26 end loop;
27 end after statement;
28 end;
29 /
Trigger created.
SQL>
Testing:
SQL> -- This will be the manager
SQL> insert into temp values (1, 'Little', null);
1 row created.
SQL> -- Next 3 rows will be OK
SQL> insert into temp values (2, 'Foot' , 1);
1 row created.
SQL> insert into temp values (3, 'Scott' , 1);
1 row created.
SQL> insert into temp values (4, 'Tiger' , 1);
1 row created.
SQL> -- The 4th employee for the same manager should fail
SQL> insert into temp values (5, 'Mike' , 1);
insert into temp values (5, 'Mike' , 1)
*
ERROR at line 1:
ORA-20000: No more than 3 employees per manager
ORA-06512: at "SCOTT.TRG_3EMP", line 22
ORA-04088: error during execution of trigger 'SCOTT.TRG_3EMP'
SQL> -- Someone else can be Mike's manager
SQL> insert into temp values (5, 'Mike', 2);
1 row created.
SQL>