What are Unique Constraints? How can we enforce them and finally, is there anyway that we can have modified uniqueness…can we have Semi Unique Constraints?
There are many use cases that require that a column in a table only have unique values. The main example of this is a table’s
PRIMARY KEY which, needing to uniquely identify a record, needs to be unique by default.
In fact, SQL Server enforces this uniqueness by creating a unique index on whatever column(s) the Primary key is defined on.
-- Drop Table if Exists. IF OBJECT_ID(N'dbo.SemiUniqueColumn', N'U') IS NOT NULL DROP TABLE dbo.SemiUniqueColumn; -- Create Table. CREATE TABLE dbo.SemiUniqueColumn ( ID int IDENTITY(1, 1), UniqueColumn int, SemiUniqueColumn int, CONSTRAINT [PK_SemiUniqueColumn] PRIMARY KEY CLUSTERED (ID) ); -- Check Primary Key Exists. EXEC sp_helpindex N'dbo.SemiUniqueColumn';
Unique But Not Primary
Primary keys are not the only options that can be unique, SQL Server recognizes this, and so there is the option of marking other columns as unique as well. Whether this be actioned by a
UNIQUE CONSTRAINT or a
UNIQUE INDEX is user’s perogative.
I’ll be creating this using a
UNIQUE INDEX but for the purpose of completeness, the syntax for
UNIQUE CONSTRAINTs is
ALTER TABLE table_name ADD CONSTRAINT constraint_name UNIQUE (column(s))
Now say we are forced to ensure that the column
UniqueColumn is unique, so we create a
UNIQUE INDEX on this.
-- Ensure UniqueColumn is Unique by Creating Unique Index on it. CREATE UNIQUE NONCLUSTERED INDEX [UIX_SemiUniqueColumn_UniqueColumn] ON dbo.SemiUniqueColumn (UniqueColumn);
We insert values into this table and, as long as the uniqueness of these rows are satisfied, we’re going to have a good time.
-- Insert Data. WITH RecordSet (SeqNum) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM sys.all_columns) INSERT INTO dbo.SemiUniqueColumn (UniqueColumn) SELECT SeqNum FROM RecordSet ORDER BY SeqNum DESC; -- Select Data. SELECT ID, UniqueColumn, SemiUniqueColumn FROM dbo.SemiUniqueColumn;
When Is A NULL Not a NULL?
Short answer, when you try and compare it to another one.
Now you may think that I’ve lost the plot and gone off on a tangent but bear with me here a second and I’ll explain.
A NULL is UNKNOWN and an unknown value should not equal another unknown value. What happens if your first unknown turns out to be the number 1 and the second unknown is the number 2? 1 <> 2 so comparing them then would be pretty foolish.
If you ever look at some generated script and see at the very top of the script, hiding amongst the
XACT_ABORTs and the
NOCOUNTs is another option called
ANSI_NULLs (although not anymore as it’s on by default and should be left that way IMHO).
This ensures that NULLs are treated as unknown and cannot be compared to another unknown except for some specific cases (
What Does This Mean For Us?
Good question! Remember our Unique Index on UniqueColumn? What happens if we run the following?
-- Insert NULL into UniqueColumn. INSERT INTO dbo.SemiUniqueColumn ( UniqueColumn ) VALUES ( NULL );
It inserts no problem, going all the way from 7442, 7441, … 2, 1, NULL. What about if we run it again?
Semi Unique Constraints.
Now ask yourself the question, what happens if we are told that our other column
SemiUniqueColumn can have as many NULLs as it wants but if it gets a value, that value must be unique?
-- Generate Semi Unique Values. -- Every Sixteenth Value is NULL. UPDATE original SET SemiUniqueColumn = IIF((original.UniqueColumn % 16) = 0, NULL, original.UniqueColumn) FROM dbo.SemiUniqueColumn AS [created] JOIN dbo.SemiUniqueColumn AS [original] ON created.ID = original.ID; -- Select Data. SELECT ID, UniqueColumn, SemiUniqueColumn FROM dbo.SemiUniqueColumn;
I used to think that this would be a complex requirement, possibly requiring a
TRIGGER or two to check the inserted value against whatever is already there; but there is a way to have this functionality and have it the way that SQL Server normally would enforce a uniqueness on a column; by using a
In case you’re thinking…
“Oh, a unique index doesn’t check what’s already there, is that it?”
I’m afraid that’s not the case.
-- Standard Unique Index Treats NULLs = NULLs. CREATE UNIQUE NONCLUSTERED INDEX [UIX_SemiUniqueColumn_SemiUniqueColumn] ON dbo.SemiUniqueColumn (SemiUniqueColumn);
Yes, I know that we just showed that SQL Server treats NULLs as equal on Indexes but there is a way around this, and that’s to not use a simple unique index.
We simply ignore the NULLs altogether by using a
UNIQUE FILTERED INDEX.
-- Filtered Index. CREATE UNIQUE NONCLUSTERED INDEX [UFIX_SemiUniqueColumn_SemiUniqueColumn] ON dbo.SemiUniqueColumn (SemiUniqueColumn) WHERE SemiUniqueColumn IS NOT NULL;
Don’t trust me? Trust but verify!
First attempt to insert a duplicate value in SemiUniqueColumn:
-- Test it (Duplicate). INSERT INTO dbo.SemiUniqueColumn (UniqueColumn, SemiUniqueColumn) VALUES (0, 7439);
Now we attempt to insert a duplicate value in SemiUniqueColumn but attempt to insert a duplicate NULL.
-- Test it (Not Duplicate). INSERT INTO dbo.SemiUniqueColumn (UniqueColumn, SemiUniqueColumn) VALUES (0, NULL);
Finally, NULL was chosen just for example purposes, filtering can be done on any value but, at the moment, there are a couple of got’cha’s with them…
I think you’ll agree that this would be so much easier to manage and troubleshoot than multiple triggers.
In fact, indexes are extremely useful aspects of databases and, once you start to realise how they work, you can start to use them for various different things than simple seeks and scans.
But the main aspect here is that complexity is not king. There are a multitude of ways to achieve something, so take a few moments to think about the problem you are trying to face and you’ll be thankful in the long run.