views:

311

answers:

4

Let's say you have a table for branches in your organization. Some of them are "main" branches, and others are satellite offices that roll up to a main branch. Other than this distinction, which only impacts a few things in the system, the branches are all peers and have the same attributes (address, etc.). One way to model this is in a table like:

CREATE TABLE Branch (
    branch_id INT NOT NULL PRIMARY KEY IDENTITY(1,1),
    branch_name VARCHAR(80) NOT NULL,
    street VARCHAR(80) NULL,
    city VARCHAR(30) NULL,
    state CHAR(2) NULL,
    zip CHAR(5) NULL,
    is_satellite_office BIT NOT NULL DEFAULT(0),
    satellite_to_branch_id INT NULL REFERENCES Branch(branch_id)
)

Where is_satellite_office = 1 iff this record is a satellite to another branch, and satellite_to_branch_id refers to which branch you're a satellite of, if any.

It's easy enough to put a constraint on the table so that those two columns agree on any given record:

CONSTRAINT [CK_Branch] CHECK 
  (
    (is_satellite_office = 0 AND satellite_to_branch_id IS NULL) 
    OR (is_satellite_office = 1 AND satellite_to_branch_id IS NOT NULL)
  )

However, what I really want is a way to guarantee that this recursion only goes one level deep ... that is, that if I point to a branch as my parent, it must not have a parent itself, and its value for is_satellite_office must be 0. Put differently, I don't really want a fully recursive tree structure, I just want to limit it to a single parent / child relationship. That's how I'm going to write the code, and if there's a way to enforce it in the database that won't perform like total crap, I'd like to.

Any ideas? I'm working on MSSQL 2005, but general (non-vendor-specific) solutions are preferred. And no triggers need apply, unless there's truly no other way to do it.

EDIT: To be clear, satellite_to_branch_id is the recursive pointer to another record in the same Branch table. I know that I could remove the is_satellite_office BIT and rely on IsNull(satellite_to_branch_id) to give me the same information, but I find it's a little clearer to be explicit, and besides which, that's not the gist of the question. I'm really looking for a pure SQL-constraint way to prevent recursion-depth of greater than 1.

+1  A: 

Seems to me like a business constraint, difficult to enforce at the data definition level. I don't believe the relational algebra has any support to determine a limit for self references depth.

Otávio Décio
+1. Kind of what I was afraid of, but I'm hoping someone has a cool trick to do it.
Ian Varley
I'll be curious to see if anyone comes up with a purely data definition constraint and I'll delete my answer if that solution holds water.
Otávio Décio
A: 

What about this slightly different structure?

CREATE TABLE Branch (
    branch_id INT NOT NULL PRIMARY KEY IDENTITY(1,1),
    branch_name VARCHAR(80) NOT NULL,
    street VARCHAR(80) NULL,
    city VARCHAR(30) NULL,
    state CHAR(2) NULL,
    zip CHAR(5) NULL,
    parent_id int NULL
)

PARENT_ID will simply point to the BRANCH_ID of another record in the same table. If it is null, then you know it has no parents.

Then, to get one level of recursion, you can just join the table to itself once, like this:

SELECT
  PARENT.BRANCH_NAME AS PARENT_BRANCH
 ,CHILD.BRANCH_NAME AS CHILD_BRANCH
FROM
  BRANCH PARENT
 ,BRANCH CHILD
WHERE CHILD.PARENT_ID PARENT.BRANCH_ID

If you want to enforce one level of depth in your tree, make an on-insert/update trigger that will raise an exception if this query returns anything.

SELECT *
FROM
  BRANCH B1
 ,BRANCH B2
 ,BRANCH B3
WHERE B1.PARENT_ID = :NEW.NEW_PARENT_ID
  AND B2.PARENT_ID = B1.BRANCH_ID
  AND B2.PARENT_ID = B3.BRANCH_ID;
JosephStyons
Sorry if my description wasn't clear; that's exactly what it's already doing, table-wise (I just named it something other than "parent_id"). The question was just about how to enforce the single-depth recursion in SQL, ideally without a trigger. But thanks for the trigger solution!
Ian Varley
+1  A: 

Aren't you allowed to refer to a stored procedure in your constraint? You can in PostgreSQL, so I'd be surprised if 2005 didn't allow this.

Kev
This shouldn't hurt performance much. After all, your SP would bail after one iteration at most, and how often will you be inserting/updating branches? (If more than once a day, let me know so I can buy stocks.)
Kev
Agreed ... I've not seen that done in MS SQL Server, and I don't know if it's allowed. Anybody else know?
Ian Varley
+1  A: 

You can bind a check constraint to the return value of a UDF. Create a UDF that takes the columns involved as input parameters, and then check your desired state using a select in the UDF.

cmsjr
+1 - sounds like a good plan, I'll try it.
Ian Varley
One gotcha that slowed me down, the UDF will see the row as having been inserted.
cmsjr
User Defined Function
cmsjr
Just tried this, and it worked. My check constraint passes the value of the parent pointer column into a UDF which checks whether the record pointed to by that id itself has a parent, and returns accordingly. Thanks!
Ian Varley