views:

1106

answers:

7

I'm using SQL Server 2005.

I have a field that must either contain a unique value or a NULL value. I think I should be enforcing this with either a CHECK CONSTRAINT or a TRIGGER for INSERT, UPDATE.

Is there an advantage to using a constraint here over a trigger (or vice-versa)? What might such a constraint/trigger look like?

Or is there another, more appropriate option that I haven't considered?

+2  A: 

In Oracle, a unique key will permit multiple NULLs.

In SQL Server 2005, a good approach is to do your inserts through a view, and disable direct inserts into the table.

Here is some sample code.

JosephStyons
Where is the whitepaper to say it's a good approach?This if for DB2 but still applies: http://www.craigsmullins.com/viewnw.htm
gbn
+2  A: 

Is there a primary key on this table, maybe an Identity column? You could create a unique key that is a composite of the field you are enforcing uniqueness on in combination with the primary key.

There is a discussion about just this kind of issue here: http://blog.sqlauthority.com/2008/09/07/sql-server-explanation-about-usage-of-unique-index-and-unique-constraint/

FYI - SQL Server 2008 introduces filtered indexes which would allow you to approach this a bit differently.

keithwarren7
I don't know who and why downvoted a good solution. I am upvoting it, as it works.
AlexKuznetsov
This answer was probably voted down because of the first section relating to the composite key. This would allow duplicate nulls but it would also allow duplicate values in a column that is supposed to contain only unique values.
Scott Munro
A: 

Usually a trigger will allow you to provide a more verbose and explanatory message than a check constraint, so I have used those to avoid the "which column was bad" game in debugging.

StingyJack
A: 

A constraint is far lighter than a trigger, even though a unique constraint is effectively an index.

However, you are only allowed one NULL in a unique constraint/index. So, you'll have to use a trigger to detect duplicates.

It's been requested from MS to ignore NULLS, but SQL 2008 has filtered indexes (as mentioned while I type this)

gbn
+2  A: 

Here is an alternative way to do it with a constraint. In order to enforce this constraint you'll need a function that counts the number of occurrences of the field value. In your constraint, simply make sure this maximum is 1.

Constraint:

   field is null or dbo.fn_count_maximum_of_field(field) < 2

EDIT I can't remember right now -- and can't check it either -- whether the constraint check is done before the insert/update or after. I think after with the insert/update being rolled back on failure. If it turns out I'm wrong, the 2 above should be a 1.

Table function returns an int and uses the following select to derive it

   declare @retVal int

   select @retVal = max(occurrences)
   from ( 
        select field, count(*) as occurrences
        from dbo.tbl
        where field = @field
        group by field
   ) tmp

This should be reasonably fast if your column as a (non-unique) index on it.

tvanfosson
I've marked this answer as correct because, although Joe's solution involving an indexed view will work and is arguably more elegant, I consider this to be a more "obvious" way of constraining data in my table and thus easier to maintain.
kristian
this will only work for single row modifications. May fail for multirow updates:http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/07/01/when-check-constraints-using-udfs-fail-for-multirow-updates.aspx
AlexKuznetsov
When you say fail, you mean that erroneously disallows an update that would be legal if all of the changes occurred at the same time. I don't consider that to be so much a failure as a shortcoming of the mechanism. A failure would be if it allowed the constraint to be violated. The fact that it prevents illegal intermediate states as well as final states is just something you have to work around.
tvanfosson
Also I did not notice this: "This should be reasonably fast if your column as a (non-unique) index on it". In fact, scalar UDFs wrapped in CHECK constraints are _very_ slow. I provided benchmarks.
AlexKuznetsov
+5  A: 

I create a view with the an index that ignores the nulls through the where clause...i.e. if you insert null into the table the view doesn't care but if you insert a non null value the view will enforce the constraint.

create view dbo.UniqueAssetTag with schemabinding
as
select asset_tag
from dbo.equipment
where asset_tag is not null

GO

create unique clustered index ix_UniqueAssetTag
on UniqueAssetTag(asset_tag)

GO

So now my equipment table has an asset_tag column that allows multiple nulls but only unique non null values.

Note: If using mssql 2000, you'll need to "SET ARITHABORT ON" right before any insert, update or delete is performed on the table. Pretty sure this is not required on mssql 2005 and up.

dotjoe
+2  A: 

You can accomplish this by creating a computed column and put the unique index on that column.

ALTER TABLE MYTABLE 
ADD COL2 AS (CASE WHEN COL1 IS NULL THEN CAST(ID AS NVARCHAR(255)) ELSE COL1 END)

CREATE UNIQUE INDEX UQ_COL2 ON MYTABLE (COL2)

This is assuming that ID is the PK of your table and COL1 is the "unique or null" column.

The computed column (COL2) will use the PK's value if your "unique" column is null.

There is still the possibility of collisions between the ID column and COL1 in the following example:

ID     COL1    COL2
1     [NULL]    1
2        1      1

To get around this I usually create another computed column which stores whether the value in COL2 comes from the ID column or the COL1 column:

 ALTER TABLE MYTABLE 
 ADD COL3 AS (CASE WHEN COL1 IS NULL THEN 1 ELSE 0 END)

The index should be changed to:

CREATE UNIQUE INDEX UQ_COL2 ON MYTABLE (COL2, COL3)

Now the index is on both computed columns COL2 and COL3 so there is no issue:

ID     COL1    COL2   COL3
1     [NULL]    1       1
2        1      1       0
mtpettyp