views:

220

answers:

5

In SQL, is there a way to enforce that only one column out of a group of columns has a value, and the others are null? Maybe a constraint or trigger? This type of thing might be for a lookup table, but are there any alternate table designs that might accomplish this better?

For example:

ID    OtherTable1ID    OtherTable2ID    OtherTable3ID
-----------------------------------------------------
1     23               NULL             NULL
2     NULL             45               NULL
3     33               55               NULL   -- NOT ALLOWED

The main issue is that these columns are all FKs to other tables, so I can't collapse them down to a single column.

I'm using SQL Server, but any answer will do.

+4  A: 

A constraint such as the following should work:

 (column1 is null and column2 is null)
   or (column1 is null and column3 is null)
   or (column2 is null and column3 is null)

This won't force it to contain a non-null column, however. To do that add another constraint:

 column1 is not null
    or column2 is not null
    or column3 is not null
tvanfosson
A: 

It sounds to me as if you want to be using one column for the set of those things. Perhaps you can use some sort of tag to say that it's a Foo,3 or a Bar,7 or a Baz,9?

Curt Sampson
I should have clarified this in my question, but the issue I'm facing is that the columns are FKs to other tables. Let me update my question, sorry
Andy White
Ah, now it gets interesting! Let me think on this one a bit, but it sounds as if it's basically a problem with a poor RDBMS implementation. Unfortunately, in this sense, I don't think that there's a good one out there...
Curt Sampson
(To clarify, there's no reason you shouldn't be able to declare the type of a column to be "either a reference to this, or a reference to that, or a reference to the other," with full FK support on all of that, but current DBMSes come nowhere near that.)
Curt Sampson
+4  A: 

@tvanfosson's suggested constraints work OK for three columns, but for generality I prefer

(cast(col1 is not null, int) +
 cast(col2 is not null, int) +
 cast(col3 is not null, int)) = 1

because it generalizes better to any number of columns with "linearly growing" (instead of "quadratically growing") amount of coding (it's even neater in SQL dialects that don't require explicit casting of boolean aka bit to int, but I'm not sure if SQL Server is one of those).

Alex Martelli
+1  A: 
CREATE TABLE Repro.Entity
(
 entityId INTEGER IDENTITY (1, 1) NOT NULL,
 column1 INTEGER,
 column2 INTEGER,
 column3 INTEGER,
 CONSTRAINT Entity_PK PRIMARY KEY(entityId),
 CONSTRAINT Entity_CK CHECK(
  (column1 IS NOT NULL AND column2 IS NULL AND column3 IS NULL) OR 
  (column1 IS NULL AND column2 IS NOT NULL AND column3 IS NULL) OR 
  (column1 IS NULL AND column2 IS NULL AND column3 IS NOT NULL))
)
John Saunders
+1  A: 

For me it looks like a bad design decision. Since ID is the primary key in this table, it will be a legal value for all foreign key relationship. This means you have to work extra hard in the front end/business layer to guarantee that the values are within accepted range.

For example, the way the tables are set up, it is absolutely legal for table 2 to use 1 as the lookup value instead of 2 it is supposed to use - and database will not trap it.

I'd probably won't go this route. I will just simply create a schema named lookups and will create one lookup table per lookup value. In this way the database will properly enforce all the constraint.

The way you have set up the lookup table, you are currently limited to integer foreign keys. In some cases it might not be a good idea - for example you'd like to store the country code/codes for state rather than the integer values representing them.

no_one
I too prefer to avoid NULLable columns but the OP's design (subject to the constraint working as requested) has an advantage over yours (assuming I've understood correctly) in that they can write a constraint to enforce exactly one non-NULL value whereas yours can have the situation where there could be zero rows in the referencing tables, unless you roll your own 'distributed foreign keys' (google Hugh Darwen) using triggers (or similar).
onedaywhen