Skip to Main Content
  • Questions
  • triggers to enforce referential integrity

Breadcrumb

Question and Answer

Tom Kyte

Thanks for the question.

Asked: December 17, 2005 - 8:04 pm UTC

Last updated: October 12, 2010 - 1:56 pm UTC

Version: 10.2

Viewed 1000+ times

You Asked

Hi Tom,

I would like to start by thanking you for the great job you are doing! Your books and this site is a constant source of wisdom for me and my colleagues.

There is a key database design issue, I have always wanted to have discussed with you: it is the constant wish of people new to sql/oracle to use trigger for enforcing referential integrity in certain situations.
I have actually read some of your articles, strictly opposing the use of triggers to enforce referential integrity but I was never really able to draw a clear line between the disadvantages in using a trigger (instead of the build in declarative integrity constraints) and the needed freedom in choosing a specific data model on the other site.

My question: You once have stated that it is “virtually impossible” to write a correct trigger that guarantees referential integrity and would like to better understand on see as an example why this is the case.

Thank you and cheers from Austria
Dieter

and Tom said...

It is not possible to implement integrity constraints that cross ROWS in a table (eg: make sure that the PROJECT_NAME column is unique for rows in the project table where STATUS = 'ACTIVE') or cross TABLES (eg: foreign keys) using application code (not declarative integrity constraints) without using

LOCK TABLE;
or
SELECT FOR UPDATE on many rows

eg: introduce serialization.


Rather then me go through this (I have, it is in all of the books I have written), let me put forth this challenge. Here are MY tables:

create table P
( x int primary key,
name varchar2(10) NOT NULL,
status varchar2(1) NOT NULL check (status in ('A','I') )
)
/

create unique index p_idx on p(case when status = 'A' then name end);

create table C
( x references p )
/


Now, those are my tables. I want to you to take THESE tables:

create table p ( x int, name varchar2(10), status varchar2(1) );
create table c ( x int );

and enforce all of these constraints:

a) p.x is not null
b) p.x is unique
c) p.name is unique for active projects
d) p.status is one of 'A', 'I'
e) p.status is not null
f) c.x has a value that is in p.x

And I want to understand why the code you write is better, faster, cheaper, more maintainable.

Then I'll show you how many mistakes there are with the

a) reasoning behind better, faster, cheaper, more maintainable
b) the logic used to implement


(hint if you have effective oracle by design, I did write this all out...)


Another question - why, if you can simply "alter table c add constraint c_fk foreign key(x) references p", you would even CONSIDER wanting to write lots of code???

Rating

  (16 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Cool !

Iru, December 19, 2005 - 10:41 am UTC

Hi Tom.

Just cool! As you say, every day i learn one new thing about Oracle. Never knew of the syntax

create table C
( x references p )

to enforce the rule

c.x has a value that is in p.x

But agin, i am a Developer with ocasionally DBA tasks, not backwards. Since what version this works?

Thank you for all of us fighting with Oracle (and loving it) all around the world.

Er.. sorry for putting this here but, do you think some day you will come to Madrid, Spain to talk ? I'd love to hear a presentation of yours.

Tom Kyte
December 19, 2005 - 12:10 pm UTC

This works since version 7.0 - 1992/1993...


Create table is a DEVELOPER command, not a DBA task ;)


I have nothing scheduled in Madrid, but I will be coming to Finland, London, Dublin, Munich, Paris, Zurich in 2006

i agree but..

dieter, December 20, 2005 - 10:29 am UTC

Hi Tom,

I completely agree with you and do not even dare in trying to solve the puzzle. If have read (most of) your book a while ago and remember the drawbacks of the serialization that is needed to work not only in a single user environment.

I still sometimes have a hard time to find the "right" solution to some rather trivial problems without using triggers.
Please have for example a a look at this and let me know how you would solve it:

The table address is a generalize table of addresses each having a date from when on they are valid.:
create table address(
addrid number,
valid_since date,
name varchar2(30));
alter table address (
constraint pk_address primary key (addrid, valid_since));

The table client is one of many tables that reference the address table and the business login of the application automatically reads the most appropriate validity of the address:
create table client(
clientid number primary key,
addrid number);

Clearly we should guarantee that there is at least one address (the validity would not matter) for each client using this address.

Thank you and cheers
Dieter


Tom Kyte
December 20, 2005 - 10:32 am UTC

if they are all "valid since", which one is "THE VALID" one??

If this is a table maintaining history, perhaps you need a "current address" object and "historical addresses" (eg: data model needs to be looked at)

You are missing a table in the schema.

the valid one?

dieter, December 20, 2005 - 11:09 am UTC

Hi Tom,

The business logic has do decide what the valid one is!
It is less a historical view of addresses then a simple way to create a new address with a validity in the future and the system will automatically pick the "most appropriate" one.
When we send an invoice to the client, we simply take the "most current" one after the printing date and this date can change all the time. In other cases we might be able to select a specific one of those available.
I'm not pretendiong, that the model is correct but I would also not be able to come up with a "better" one.

Cheers
Dieter


Tom Kyte
December 20, 2005 - 12:40 pm UTC

you are missing a table then - the parent table of address, address is the child table of something else and it is missing.


seems whacky - cannot imagine how the software would pick the "most appropriate" but basically, you are missing a table.

Model needs to be right first

Jon Barwell, December 20, 2005 - 11:17 am UTC

Your Client should be located at a Site and then the Site has multiple Addresses, one of which can be valid at any one time (this may be where there are questions about how you can ensure that only one is valid at any one time)

Client -> Site <- Address

With this model you only have to find the current address at the site not keep messing around with changing the integrity of the data!

ok but...

dieter, December 20, 2005 - 1:14 pm UTC

I'm fine with the fact that we should split up the model in three tables as suggested by Jon Barwell:
Client -> Site <- Address
The problem is actually not that only one can be valid but rather how to guarantee that every Site has at least one Address and here I do not see a way without trigger.
Cheers
Dieter

Tom Kyte
December 20, 2005 - 1:40 pm UTC

.... Clearly we should guarantee that there is at least one address (the validity
would not matter) for each client using this address. ...



You can use an on commit refresh materialzed view with a check constraint if you like.

Or you can use a trigger, trigger would maintain a count in the "site" table. It would effectively serialize on the site row (avoids multi-user concurrency issues).

You would not be counting rows in a trigger, you would be maintaining a rolled up count that the database would serialize on and verify for us.


It would not be enforcing a constraint across rows- the constraint would boil down to a check constraint on A row.

Something like this:

create table p
( x int primary key,
/* other stuff */
addr_cnt number default 0
not null
constraint addr_cnt_gt_zero
check ( addr_cnt>0 )
deferrable initially deferred
)
/

create table c
( x,
y int,
data varchar2(10),
constraint c_fk foreign key (x) references p,
constraint c_pk primary key(x,y)
)
/

create or replace trigger c_trigger
after insert or update or delete on c for each row
begin
if (inserting or ( updating and :new.x <> :old.x ) )
then
update p set addr_cnt = addr_cnt+1 where x = :new.x;
end if;
if (deleting or (updating and :new.x <> :old.x ) )
then
update p set addr_cnt = addr_cnt-1 where x = :old.x;
end if;
end;
/

you might even look into materialized views with check constraints:
</code> https://www.oracle.com/technetwork/issue-archive/2005/05-mar/o25asktom-089621.html <code>

as well.

As always: You are the best

dieter, December 20, 2005 - 2:03 pm UTC

Thank you
Dieter

Oracle Applications and Referential Integrity

Su Baba, February 21, 2006 - 9:21 pm UTC

You have mentioned countless times that we should always use database itself to enforce referential integrity. As far as I know, Oracle Applications (ERP/CRM) do not enforce referential integrity at all at the database level. RI is always enforced at the application level. Isn't this very bad design? Are there any specific reasons that they do it this way?

Tom Kyte
February 22, 2006 - 8:33 am UTC

I've also said this countless times. Apps like that are "not quite fully baked" when they are shipped. They are "not done". They are 80% of the way there - the rest is called "implementation" (that is why implementing these packaged apps takes, well, longer than a weekend).

When you are writing your application for your company, you will be "done", you will have a 100% implemented application. You *know* what is to happen to everything.


And do I agree 100% with the way everything is implemented in the applications (ours, theirs, yours, anyones?) of course not.

Foreign key to multiple tables

A reader, September 04, 2006 - 5:18 pm UTC

Suppose I have a "generic" child table that needs to capture detail information about (1 of) M master tables (MASTER1, MASTER2, ..., MASTERm). The attributes in the detail table are the same, regardless of the master.

A declarative RI constraint can only point to 1 "master" table at a time. It seems like a waste to create M child tables, each one pointing to the corresponding MASTERm table.

In such cases, it is very tempting to create a BEFORE INSERT OR UPDATE (for each row) trigger and try to do the M lookups against the M master tables and raise an user-defined RI error if not found.

Comments? How would this be implemented using the built-in declarative constraints?

Thanks

Tom Kyte
September 04, 2006 - 7:57 pm UTC

"generic" = "loss of data integrity" & "bad performance" & "inability for mere mortals to query" & "other bad stuff"

(C programers will tell you what && means..)

tell you what, you share with me the 'trigger' that does this and I'll tell you why it "won't work"

It won't be done via declarative constraints, a child has a parent, you would need to make all of the "possible parents" children of some "master parent" and the "generic details" the child of this master parent and then figure out how to provide for mutual exclusion amongst the "possible parents" and --- well, you aren't doing a relational database anymore.

RI using trigger

A reader, September 04, 2006 - 9:19 pm UTC

you share with me the 'trigger' that does this

Child table pointing to 3 "masters"

create or replace trigger trg
before insert or update on generic_child
for each row
l_count1 pls_integer := 0;
l_count2 pls_integer := 0;
l_count3 pls_integer := 0;
begin
select count(*) into l_count1
from master1 m where m.pk=:new.pk;

select count(*) into l_count2
from master2 m where m.pk=:new.pk;

select count(*) into l_count3
from master3 m where m.pk=:new.pk;

if (l_count1 + l_count2 + l_count3 != 1) then
raise_application_error(-20000,:new.pk||' needs to have 1 and only 1 master record');
end if;
end;
/

[Don't get me wrong, I agree with you, I am just playing devil's advocate]

Tom Kyte
September 05, 2006 - 1:49 pm UTC

that'll never work.

a) where is the trigger on the child tables (for completeness)

b) describe what happens when:

time 1: I insert a row into master1 with pk = 1
time 2: you run your trigger (which doesn't see my insert)
time 3: I commit
time 4: you commit


where is your LOCK TABLE on all of the tables? Even Oracle would use a LOCK table command for RI enforcement if you don't index the foreign keys.

Or how about?

A reader, September 04, 2006 - 10:51 pm UTC

Or how about

create table master1
(
pk int primary key,
c1 int,
c2 int
);

create table master2
(
pk int primary key,
c1 int,
c2 int
);

create table master3
(
pk int primary key,
c1 int,
c2 int
);

create table generic_detail
(
pk int primary key,
typ number(1) not null check (typ in (1,2,3)),
master1_pk int references master1(pk),
master2_pk int references master2(pk),
master3_pk int references master3(pk),
constraint ck check
(
(typ=1 and (master1_pk is not null and master2_pk is null and master3_pk is null))
or
(typ=2 and (master2_pk is not null and master1_pk is null and master3_pk is null))
or
(typ=3 and (master3_pk is not null and master1_pk is null and master2_pk is null))
)
);


Tom Kyte
September 05, 2006 - 1:51 pm UTC

now we start to wonder "why" since they have to be joined very differently? why have it "generic" - if I specify a tablename or specify a specific where clause.

I'd rather specify a tablename.

Take your generic thing up a notch to 10 parents for example, it gets rather cumbersome rather quickly.

quick question

Kenny Roytman, September 05, 2006 - 1:48 pm UTC

hi tom,

in your earlier response, you used an interesting CREATE INDEX statement:

create unique index p_idx on p(case when status = 'A' then name end);

This is the first time I've seen this type of technique. Is p_idx considered to be a regular unique index with selective enforcement? Or is it a special case of a function-based index?

Tom Kyte
September 05, 2006 - 5:29 pm UTC

it is a NORMAL function based index. the function is "CASE"

Check constraint vs trigger

Lakshmi, October 28, 2006 - 8:17 am UTC

Hi Tom

The following data is present in one of the tables:
StartDate
EndDate
....

Our requirement is to check if StartDate is greater than or equal to the current date. Can I achieve this without the help of a trigger?

[I read in oracle documentation that "Calls to the functions that are not deterministic" are not allowed in check constraint.]

Thanks


Tom Kyte
October 28, 2006 - 10:40 am UTC

well, what happens when you export/import this data?

Seems to be a constraint that only makes sense in CERTAIN conditions. Therefore, I would not suggest a constraint or trigger at all - but rather a TRANSACTIONAL API written in plsql that applications call (eg: no one ever inserts into this table except for well formed business transactions that do the right thing)


So, write a procedure....

Thanks for the quick response !!

A reader, November 02, 2006 - 4:05 am UTC


Constant in foreign key constraints

Dieter, December 30, 2006 - 2:30 pm UTC

Dear Tom,
I wanted to follow up on my original question and ask for your opinion on why Oracle does not allow using constants in a foreign key constraint? I'm aware, that a database can be designed to make this "feature" obsolete but I have made the experience that (especially in legacy systems) this would allow to specify FK constraints without redesign.
Thank you for all your help and happy new year.
Dieter
Tom Kyte
December 30, 2006 - 5:23 pm UTC

how would this be useful? can you give a strawman example of what use a constant in a foreign key constraint would be?

Example

Dieter, December 31, 2006 - 2:58 am UTC

Hi Tom,

I have been trying to find one of the original cases I had to review and I came across (and have simplified) the following example.
In this original database design, the table client table does twice reference the product_version table for two products that must be available. Because this two products have constant id's, the id's have not been added to the client table itself. The idea was, that if Oracle would allow using a constant in the FK declaration, the constraint could have been added directly. We had to solve this, by adding the two product id's to the client table and then using the id's in the two FK constraints. Allowing to use a constant in the FK constraint seemed to be a fair option and I would be unable to see a major problem doing so.

create table client (
client_id number primary key,
backoffice_product_version varchar2(30) not null,
frontend_product_version varchar2(30) not null
);
create table product (
product_id number primary key
);
create table product_version (
product_id number,
version_id varchar2(30)
);

alter table product_version constraint pk_product_version primary key (product_id, product_version);
alter table client add constraint fk_client_prod_backoffice foreign key (10, backoffice_product_version) references product_version (product_id, version_id);
alter table client add constraint fk_client_prod_frontend foreign key (11, frontend_product_version) references product_version (product_id, version_id);

Tom Kyte
December 31, 2006 - 4:01 pm UTC

well, the design is "wrong", so yes you would have to propagate that down. Sorry, not everything should be "fixed" for every bad design decision.

the 10 and 11 need to be attributes down into the client table, period. I would not ever want this "constant" feature to even be considered for the reason given.

Oracle's method of checking FK violation

Narendra, October 12, 2010 - 1:37 pm UTC

Tom,

I learned from a recent thread on OTN that oracle internally issues a "SELECT /*+ ALL_ROWS */ COUNT(1)" in order to check the FK violation. The URL is http://forums.oracle.com/forums/thread.jspa?messageID=6508964#6508964
I am bit surprised about a few things
1) Why is ALL_ROWS hint needed when only an existance of a child record is to be checked ?
2) Why a COUNT(1) in the code by Oracle and not count(*) or count(FK column) ?
3) Why a COUNT of child table records, in first place?

I agree that ideally FK columns should be indexed but that should not mean the check not be optimized, especially when it is done by oracle.

Thanks
Tom Kyte
October 12, 2010 - 1:56 pm UTC

read the other comments - it could have been done more optimally - a simple

select count(*) from t where fk = :bv and rownum = 1;

would have been better in my opinion.

More to Explore

PL/SQL demos

Check out more PL/SQL tutorials on our LiveSQL tool.

PL/SQL docs

PL/SQL reference manual from the Oracle documentation library