Question

Possible Duplicate:
Foreign key constraint may cause cycles or multiple cascade paths?

I have a database of contact details that contains four primary tables:

  • Organisations
    • Departments
      • Sections
        • People

Here is a diagram (the arrows indicate foreign key constraints):

IMPORTANT: I accidentally marked the green arrow as pointing to per_PerID. It should point to per_SecID

enter image description here

I have created constraints that ensure each department record falls under an organisation, each section falls under a department, and so forth. These foreign key constraints have their action set to CASCADE, so that deleting an organisation will delete all corresponding departments etc.

The problem is that each organisation needs to have a person in charge. This translates to a foreign key constraint on the field that will contain the ID of the person in charge. Creating the constraint was easy, but when I tried to set the ON DELETE action to SET NULL, I got the following error:

Introducing FOREIGN KEY constraint ... on table ... may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.

My reasoning was that cycles would only occur if I set the action to CASCADE, not SET NULL, but evidently this was incorrect.

  1. Person record deleted
  2. org_incharge_PerID set to null in corresponding organisation record
  3. No further propagation occurs. If the organisation had been deleted instead there would be a problem, since departments also refer to organisations. As things stand there shouldn't be an issue.

Is there another way to ensure referential integrity, or should I rethink my table structure, in a way that can avoid the cycles problem entirely?

What I want is for org_incharge_PerID to be constrained to the set of per_PerIDs in the persons table. Additionally, I want for the value to be set to NULL if that person is deleted.

Was it helpful?

Solution

Once you have run into the cycles warning, the only other way to ensure referential integrity without changing your database structure is to use a trigger for one of the constraints. The problem triggering the warning is twofold:

  1. The Sections table shares two opposite-direction foreign key constraints with the Persons table.
  2. All the four tables have a full cycle that progresses through all of them in the same direction, Person->Section->Department->Organization.

Additionally, there is an issue with the table design: the way it is constructed, a person could be in charge of a section that he is not a member of.

You can get around two these three problems by removing InCharge column from Sections and introducing another table, SectionInCharge, containing columns PerID and SecID, with a clustered index/unique constraint on SecID, and a composite foreign key to the Persons table on both columns. You will need to add a UNIQUE constraint to the two columns in the Persons table so the FK can relate to them.

Now you can set the new FK to ON DELETE CASCADE and delete away.

It is often a superior database design pattern to use the presence or absence of rows to signify something rather than to use a nullable column.

Now that I think about it, the other tables with a "person in charge" column have a similar problem in regards to allowing the assignment of someone who is not properly a member. The only way I know to solve this using FKs (which in my mind is best practice over triggers) is by propagating columns through tables and including them in composite keys. This has the unfortunate side effect of making things a little awkward, but it is not too bad as you can put the most-specific column first and join only on it when possible. So you would put OrgID in the Section table, with a composite FK to the Dept table on (DeptID, OrgID), and then put DeptID and OrgID also in the Persons table. You will then need DeptInCharge and OrgInCharge tables.

One more note about this (that's not really about solving the core problem you presented) that you might be interested in is the possibility of combining your three bottom tables into a single table, OrgUnit. Add an OrgUnitType column and a ParentOrgID column. Either add columns that will be NULL when the thing does not possess that property (as in Sections not having a fax number), or make the existing Organizations, Departments, and Sections tables subtypes of the OrgUnit (example of DB supertype/subtype). The advantage of this is that then you can refer to any one of the three types anywhere you want to: if the FK is to OrgUnit, it can be any one of them. But if it is only allowed to be a Section, then you would FK to that table instead. In fact, that is then perfect for a single OrgUnitInCharge table, with an FK to OrgUnit (OrgUnitID, OrgUnitTypeID).

I know there are some loose ends here that I haven't tied up completely into a pretty package for you, but I hope that what I've described so far is helpful.

P.S. I hope you will forgive me for adding a stylistic consideration. To me it is a lot of extra needless decoration to put a table-signifying prefix on every column in each table. I type 90-110 wpm depending on how good I feel, but I still hate typing any more than I have to. Also, I believe very strongly in keeping a high signal-to-noise ratio in my code, and for me, the prefixes exploding all over the place drastically reduce that. In SQL queries, best practice is to use table aliases and then use those aliases on each column, as in SELECT P.Name FROM dbo.Person P. To resolve the issue of multiple tables having the same column names, I have started making the typical WidgetName column in my Widget table to also be Widget. As one example of why a high signal-to-noise is so important: vrbauxCan proYou verRead artThis nouSentence advQuickly conAnd advEasily? True, the additional prefixes contain useful information, but that useful information is only a little useful, and a lot distracting. If you're designing your database from scratch now, I'd like to request, as someone who could by a strange coincidence some day end up maintaining it (stranger things have happened), that you not use those prefixes. :)

P.P.S. I like PascalCase over under_scores because it's easier to type. The underscore is on the top row and hit by the weakest finger--so it's slower and error prone.

P.P.P.S. I also prefer singular table names, but that's neither here nor there is it? I apologize in advance for hitting you up with style critique.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top