771

I want to have a unique constraint on a column which I am going to populate with GUIDs. However, my data contains null values for this columns. How do I create the constraint that allows multiple null values?

Here's an example scenario. Consider this schema:

CREATE TABLE People (
  Id INT CONSTRAINT PK_MyTable PRIMARY KEY IDENTITY,
  Name NVARCHAR(250) NOT NULL,
  LibraryCardId UNIQUEIDENTIFIER NULL,
  CONSTRAINT UQ_People_LibraryCardId UNIQUE (LibraryCardId)
)

Then see this code for what I'm trying to achieve:

-- This works fine:
INSERT INTO People (Name, LibraryCardId) 
 VALUES ('John Doe', 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA');

-- This also works fine, obviously:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Marie Doe', 'BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB');

-- This would *correctly* fail:
--INSERT INTO People (Name, LibraryCardId) 
--VALUES ('John Doe the Second', 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA');

-- This works fine this one first time:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Richard Roe', NULL);

-- THE PROBLEM: This fails even though I'd like to be able to do this:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Marcus Roe', NULL);

The final statement fails with a message:

Violation of UNIQUE KEY constraint 'UQ_People_LibraryCardId'. Cannot insert duplicate key in object 'dbo.People'.

How can I change my schema and/or uniqueness constraint so that it allows multiple NULL values, while still checking for uniqueness on actual data?

Jeroen
  • 60,696
  • 40
  • 206
  • 339
Stuart
  • 11,775
  • 6
  • 33
  • 31
  • Connect issue for standard compatibility to vote for: https://connect.microsoft.com/SQLServer/Feedback/Details/299229 – Vadzim Apr 03 '15 at 13:02
  • Possible duplicate of [How to create a unique index on a NULL column?](http://stackoverflow.com/questions/191421/how-to-create-a-unique-index-on-a-null-column) – Frédéric Oct 05 '15 at 17:29
  • UNIQUE constraint and allow NULLs. ? It is common sense. It is not possible – flik Mar 26 '18 at 18:02
  • 59
    @flik, better not refer to "common sense". That is no valid argument. Especially when considering that `null` is not a value but the absence of value. Per the SQL standard, `null` is not considered equal to `null`. So why multiple `null` should be an uniqueness violation? – Frédéric Sep 24 '18 at 10:04

15 Answers15

1521

What you're looking for is indeed part of the ANSI standards SQL:92, SQL:1999 and SQL:2003, ie a UNIQUE constraint must disallow duplicate non-NULL values but accept multiple NULL values.

In the Microsoft world of SQL Server however, a single NULL is allowed but multiple NULLs are not...

In SQL Server 2008, you can define a unique filtered index based on a predicate that excludes NULLs:

CREATE UNIQUE NONCLUSTERED INDEX idx_yourcolumn_notnull
ON YourTable(yourcolumn)
WHERE yourcolumn IS NOT NULL;

In earlier versions, you can resort to VIEWS with a NOT NULL predicate to enforce the constraint.

Vincent Buck
  • 16,462
  • 2
  • 21
  • 21
  • 9
    this is probably the best way to do this. not sure if there are any performance impacts? anyone? – Simon_Weaver Mar 13 '10 at 03:20
  • 4
    I am trying to do exactly this in SQL Server 2008 Express edition and I get an error as follows: CREATE UNIQUE NONCLUSTERED INDEX UC_MailingId ON [SLS-CP].dbo.MasterFileEntry(MailingId) WHERE MailingId IS NOT NULL Results in: Msg 156, Level 15, State 1, Line 3 Incorrect syntax near the keyword 'WHERE'. If I remove the where clause the DDL runs fine, but of course, doesn't do what I need it to. Any ideas? – Kenneth Baltrinic Jul 16 '10 at 18:07
  • Kenneth I also had the same issue. Works fine on a production (enterprise or professional) version so I'm guessing it's just a bug or intended crippling. – Max Feb 10 '11 at 22:01
  • 2
    @Simon_Weaver "There are no significant differences between creating a UNIQUE constraint and creating a unique index independent of a constraint." http://msdn.microsoft.com/en-us/library/ms187019.aspx – Factor Mystic Jul 28 '11 at 17:30
  • 6
    Unless I am mistaken, you cannot create a Foreign Key off a Unique Index like you can off a Unique Constraint. (At least SSMS complained at me when I tried.) It would be nice to be able to have a nullable column that is always unique (when not null) be the source of a Foreign Key relationship. – Vaccano Nov 29 '11 at 22:54
  • 8
    Truly a great answer. Too bad it was hidden by the one accepted as answer. This solution almost didn't get to my attention, but it works like wonders in my implementation now. – Coral Doe Oct 24 '12 at 07:55
  • 2
    Another alternative for SQL 2005 and below is a Computed Column aka "Nullbuster" trick. http://stackoverflow.com/a/191729/132461 It saves you from cluttering the database with another view, you just have another column instead - usually Named ColumnA-Nullbuster if ColumnA is the one you want to be ANSI nullable UNIQUE. Put a UNIQUE Index (or constraint to express business intent) on ColumnA-Nullbuster and it will enforce uniqueness on ColumnA – DanO Apr 22 '13 at 21:33
  • It seems that SM SQL MANAGEMENT STUDIO Accidentially removes this WHERE-clause when you go to the index view and just update anything on the index? – Stephan Møller Jun 05 '13 at 11:31
  • Can the `WHERE` clause be configured in the GUI? – PeterX May 27 '14 at 07:09
  • 1
    @PeterX - yes it can. Go to the "Filter" page on Index Properties and put the where clause in there (without the WHERE keyword). Example: `([yourcolumn] IS NOT NULL)` – Iain Fraser Oct 06 '14 at 07:22
  • 2
    Connect issue for standard compatibility to vote for: https://connect.microsoft.com/SQLServer/Feedback/Details/299229 – Vadzim Apr 03 '15 at 13:03
  • 1
    Great!!! After 6 years it's exactly I Search.This syntax is ok on SQLServer 2012/2014 – YannickIngenierie Oct 08 '15 at 13:17
  • In my case, since this is "filtered" (has a where clause), this requires "SET ANSI_PADDING ON" before the index creation and then "SET ANSI_PADDING OFF" afterwards. But then, if I create an index with those flags, EVERY SINGLE INSERT afterwards also requires those flags. Terrible database design--why is an index fundamentally changing how I interact with my table? – Alkanshel Apr 27 '16 at 00:43
  • Entity Framework users, check this: http://stackoverflow.com/questions/24361518/ef-6-1-unique-nullable-index – karlingen Jul 05 '16 at 09:24
  • 1
    It's 2017, can this not be made the accepted solution already? Do we really need advice for pre-2008 SQL :) – Caleb Seadon Aug 17 '17 at 02:34
  • I wonder if a non-unique index with yourcolumn IS NULL is needed in addition. Because else the performance will be terrible for queries that uses IS NULL compared to a regular non-unique (and non filtered) index covering all rows. – osexpert Apr 12 '22 at 07:11
  • 1
    and if i don't want it to be an index? – giammin Dec 07 '22 at 13:37
  • great solution. Don't understand why in Sql server management the index is not visible in the design mode. It is visible in the tree. – Oliver Voutat Sep 01 '23 at 09:09
184

SQL Server 2008 +

You can create a unique index that accept multiple NULLs with a WHERE clause. See this answer.

Prior to SQL Server 2008

You cannot create a UNIQUE constraint and allow NULLs. You need to set a default value of NEWID().

Update the existing values to NEWID() where NULL before creating the UNIQUE constraint.

Pang
  • 9,564
  • 146
  • 81
  • 122
Jose Basilio
  • 50,714
  • 13
  • 121
  • 117
  • 2
    and this will retrospectively add values to existing rows, if so this is what I need to do, thanks? – Stuart Apr 20 '09 at 10:26
  • 1
    You would need to run an UPDATE statement to set the existing values to NEWID() where the existing field IS NULL – Jose Basilio Apr 20 '09 at 10:32
  • ok, think I'll go for this, Seems simpler solution for what I need than creating a view, thanks – Stuart Apr 20 '09 at 10:36
  • 61
    If you are using SQL Server 2008 or later, see the answer below with over 100 upvotes. You can add a WHERE clause to your Unique Constraint. – Darren Griffith Feb 08 '13 at 22:01
  • 1
    This very problem hits ADO.NET DataTables too. So even that I can allow nulls in the backing field using this method, the DataTable won't let me store NULLs in a unique column in the first place. If anyone knows a solution for that, please post it [here](http://stackoverflow.com/questions/28286327/column-x-is-constrained-to-be-unique-value-is-already-present) – dotNET Feb 02 '15 at 22:01
  • 6
    Guys make sure you scroll down and read the answer with 600 upvotes. It's no longer just over 100. – Luminous May 20 '15 at 11:33
  • Just be careful about mismatch index warning... When using UNIQUE indexes combined to where clauses, the execution plains can increases CPU usage. – Thiago Daher Sep 20 '18 at 19:59
  • Even though this is a practical solution to the problem at hand it's important not to lose sight that this is a "defect" of SQL Server. It seems SQL Server considers `null` as a value, whereas it is not (it a "non-value"). It seems that Sybase/SAPASE suffers from the same defect. – The Impaler May 30 '20 at 20:38
59

SQL Server 2008 And Up

Just filter a unique index:

CREATE UNIQUE NONCLUSTERED INDEX UQ_Party_SamAccountName
ON dbo.Party(SamAccountName)
WHERE SamAccountName IS NOT NULL;

In Lower Versions, A Materialized View Is Still Not Required

For SQL Server 2005 and earlier, you can do it without a view. I just added a unique constraint like you're asking for to one of my tables. Given that I want uniqueness in column SamAccountName, but I want to allow multiple NULLs, I used a materialized column rather than a materialized view:

ALTER TABLE dbo.Party ADD SamAccountNameUnique
   AS (Coalesce(SamAccountName, Convert(varchar(11), PartyID)))
ALTER TABLE dbo.Party ADD CONSTRAINT UQ_Party_SamAccountName
   UNIQUE (SamAccountNameUnique)

You simply have to put something in the computed column that will be guaranteed unique across the whole table when the actual desired unique column is NULL. In this case, PartyID is an identity column and being numeric will never match any SamAccountName, so it worked for me. You can try your own method—be sure you understand the domain of your data so that there is no possibility of intersection with real data. That could be as simple as prepending a differentiator character like this:

Coalesce('n' + SamAccountName, 'p' + Convert(varchar(11), PartyID))

Even if PartyID became non-numeric someday and could coincide with a SamAccountName, now it won't matter.

Note that the presence of an index including the computed column implicitly causes each expression result to be saved to disk with the other data in the table, which DOES take additional disk space.

Note that if you don't want an index, you can still save CPU by making the expression be precalculated to disk by adding the keyword PERSISTED to the end of the column expression definition.

In SQL Server 2008 and up, definitely use the filtered solution instead if you possibly can!

Controversy

Please note that some database professionals will see this as a case of "surrogate NULLs", which definitely have problems (mostly due to issues around trying to determine when something is a real value or a surrogate value for missing data; there can also be issues with the number of non-NULL surrogate values multiplying like crazy).

However, I believe this case is different. The computed column I'm adding will never be used to determine anything. It has no meaning of itself, and encodes no information that isn't already found separately in other, properly defined columns. It should never be selected or used.

So, my story is that this is not a surrogate NULL, and I'm sticking to it! Since we don't actually want the non-NULL value for any purpose other than to trick the UNIQUE index to ignore NULLs, our use case has none of the problems that arise with normal surrogate NULL creation.

All that said, I have no problem with using an indexed view instead—but it brings some issues with it such as the requirement of using SCHEMABINDING. Have fun adding a new column to your base table (you'll at minimum have to drop the index, and then drop the view or alter the view to not be schema bound). See the full (long) list of requirements for creating an indexed view in SQL Server (2005) (also later versions), (2000).

Update

If your column is numeric, there may be the challenge of ensuring that the unique constraint using Coalesce does not result in collisions. In that case, there are some options. One might be to use a negative number, to put the "surrogate NULLs" only in the negative range, and the "real values" only in the positive range. Alternately, the following pattern could be used. In table Issue (where IssueID is the PRIMARY KEY), there may or may not be a TicketID, but if there is one, it must be unique.

ALTER TABLE dbo.Issue ADD TicketUnique
   AS (CASE WHEN TicketID IS NULL THEN IssueID END);
ALTER TABLE dbo.Issue ADD CONSTRAINT UQ_Issue_Ticket_AllowNull
   UNIQUE (TicketID, TicketUnique);

If IssueID 1 has ticket 123, the UNIQUE constraint will be on values (123, NULL). If IssueID 2 has no ticket, it will be on (NULL, 2). Some thought will show that this constraint cannot be duplicated for any row in the table, and still allows multiple NULLs.

ErikE
  • 48,881
  • 23
  • 151
  • 196
  • Good workaround for Server 2005. I'd like to point out however that one of the possible benefits of the ANSI unique indexes is lost: the ability to skip records whose column value is null. For example, if your table has millions of records but only a fraction have non-null values, a true ANSI unique index would be very small, while an index with this workaround will take a great deal of space. – Guillermo Prandi Jan 23 '21 at 23:00
  • @GuillermoPrandi Those are valid thoughts. It seems to me that if a table has millions of rows and the majority of them have a NULL in a particular column, that the table itself is likely not normalized as far as it could be. Perhaps the rows that do have a value in that column should move to another table (with the original table having a one-to-zero-or-one relationship with it). Then, drop the column in the original table. An efficient unique index can now be put on the second table. A materialized view that combines the two tables with a `LEFT JOIN` can re-construct the original table. – ErikE Jul 03 '21 at 22:11
20

For people who are using Microsoft SQL Server Manager and want to create a Unique but Nullable index you can create your unique index as you normally would then in your Index Properties for your new index, select "Filter" from the left hand panel, then enter your filter (which is your where clause). It should read something like this:

([YourColumnName] IS NOT NULL)

This works with MSSQL 2012

Howard
  • 1,389
  • 11
  • 11
  • How to make a filtered index under Microsoft SQL Server Management Studio is described here and works perfectly: https://msdn.microsoft.com/en-us/library/cc280372.aspx – Jan Mar 14 '17 at 08:35
12

It can be done in the designer as well

Right click on the Index > Properties to get this window

capture

  • Very nice alternative if you have access to the designer – Francisco Jul 30 '17 at 21:08
  • Although, as I have just discovered, once you have data in your table, you can no longer use the designer. It seems to ignore the filter and any attempted table updates are met with the message "Duplicate key not allowed" – MortimerCat Aug 29 '18 at 14:23
10

When I applied the unique index below:

CREATE UNIQUE NONCLUSTERED INDEX idx_badgeid_notnull
ON employee(badgeid)
WHERE badgeid IS NOT NULL;

every non null update and insert failed with the error below:

UPDATE failed because the following SET options have incorrect settings: 'ARITHABORT'.

I found this on MSDN

SET ARITHABORT must be ON when you are creating or changing indexes on computed columns or indexed views. If SET ARITHABORT is OFF, CREATE, UPDATE, INSERT, and DELETE statements on tables with indexes on computed columns or indexed views will fail.

So to get this to work correctly I did this

Right click [Database]-->Properties-->Options-->Other Options-->Misscellaneous-->Arithmetic Abort Enabled -->true

I believe it is possible to set this option in code using

ALTER DATABASE "DBNAME" SET ARITHABORT ON

but i have not tested this

Mike Taylor
  • 2,376
  • 2
  • 17
  • 33
6

Create a view that selects only non-NULL columns and create the UNIQUE INDEX on the view:

CREATE VIEW myview
AS
SELECT  *
FROM    mytable
WHERE   mycolumn IS NOT NULL

CREATE UNIQUE INDEX ux_myview_mycolumn ON myview (mycolumn)

Note that you'll need to perform INSERT's and UPDATE's on the view instead of table.

You may do it with an INSTEAD OF trigger:

CREATE TRIGGER trg_mytable_insert ON mytable
INSTEAD OF INSERT
AS
BEGIN
        INSERT
        INTO    myview
        SELECT  *
        FROM    inserted
END
Quassnoi
  • 413,100
  • 91
  • 616
  • 614
6

It is possible to create a unique constraint on a Clustered Indexed View

You can create the View like this:

CREATE VIEW dbo.VIEW_OfYourTable WITH SCHEMABINDING AS
SELECT YourUniqueColumnWithNullValues FROM dbo.YourTable
WHERE YourUniqueColumnWithNullValues IS NOT NULL;

and the unique constraint like this:

CREATE UNIQUE CLUSTERED INDEX UIX_VIEW_OFYOURTABLE 
  ON dbo.VIEW_OfYourTable(YourUniqueColumnWithNullValues)
Lieven Keersmaekers
  • 57,207
  • 13
  • 112
  • 146
4

In my experience - if you're thinking a column needs to allow NULLs but also needs to be UNIQUE for values where they exist, you may be modelling the data incorrectly. This often suggests you're creating a separate sub-entity within the same table as a different entity. It probably makes more sense to have this entity in a second table.

In the provided example, I would put LibraryCardId in a separate LibraryCards table with a unique not-null foreign key to the People table:

CREATE TABLE People (
  Id INT CONSTRAINT PK_MyTable PRIMARY KEY IDENTITY,
  Name NVARCHAR(250) NOT NULL,
)
CREATE TABLE LibraryCards (    
  LibraryCardId UNIQUEIDENTIFIER CONSTRAINT PK_LibraryCards PRIMARY KEY,
  PersonId INT NOT NULL
  CONSTRAINT UQ_LibraryCardId_PersonId UNIQUE (PersonId),
  FOREIGN KEY (PersonId) REFERENCES People(id)
)

This way you don't need to bother with a column being both unique and nullable. If a person doesn't have a library card, they just won't have a record in the library cards table. Also, if there are additional attributes about the library card (perhaps Expiration Date or something), you now have a logical place to put those fields.

Trent Hibbard
  • 49
  • 1
  • 1
  • 8
    Seriously disagree with your first statement. In Australia every employee has a thing called a Tax File Number, which is, of course unique. By law you are not required to supply it to your employee. This means that the column may be null, but should otherwise be unique. In this case an additional table might be regarded as over-engineering. – Manngo Aug 29 '21 at 00:21
2

As stated before, SQL Server doesn't implement the ANSI standard when it comes to UNIQUE CONSTRAINT. There is a ticket on Microsoft Connect for this since 2007. As suggested there and here the best options as of today are to use a filtered index as stated in another answer or a computed column, e.g.:

CREATE TABLE [Orders] (
  [OrderId] INT IDENTITY(1,1) NOT NULL,
  [TrackingId] varchar(11) NULL,
  ...
  [ComputedUniqueTrackingId] AS (
      CASE WHEN [TrackingId] IS NULL
      THEN '#' + cast([OrderId] as varchar(12))
      ELSE [TrackingId_Unique] END
  ),
  CONSTRAINT [UQ_TrackingId] UNIQUE ([ComputedUniqueTrackingId])
)
Community
  • 1
  • 1
Baris Akar
  • 4,895
  • 1
  • 26
  • 54
  • I used this but added a view that has a column like this: `(CASE WHEN [ComputedUniqueTrackingId]=CONCAT('#',[OrderId]) THEN NULL ELSE [ComputedUniqueTrackingId] END) AS [ComputedUniqueTrackingId]` – paulguy Dec 14 '22 at 03:17
2

Maybe consider an "INSTEAD OF" trigger and do the check yourself? With a non-clustered (non-unique) index on the column to enable the lookup.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
1

You can create an INSTEAD OF trigger to check for specific conditions and error if they are met. Creating an index can be costly on larger tables.

Here's an example:

CREATE TRIGGER PONY.trg_pony_unique_name ON PONY.tbl_pony
 INSTEAD OF INSERT, UPDATE
 AS
BEGIN
 IF EXISTS(
    SELECT TOP (1) 1 
    FROM inserted i
    GROUP BY i.pony_name
    HAVING COUNT(1) > 1     
    ) 
     OR EXISTS(
    SELECT TOP (1) 1 
    FROM PONY.tbl_pony t
    INNER JOIN inserted i
    ON i.pony_name = t.pony_name
    )
    THROW 911911, 'A pony must have a name as unique as s/he is. --PAS', 16;
 ELSE
    INSERT INTO PONY.tbl_pony (pony_name, stable_id, pet_human_id)
    SELECT pony_name, stable_id, pet_human_id
    FROM inserted
 END
Paul
  • 3,634
  • 1
  • 18
  • 23
0

You can't do this with a UNIQUE constraint, but you can do this in a trigger.

    CREATE TRIGGER [dbo].[OnInsertMyTableTrigger]
   ON  [dbo].[MyTable]
   INSTEAD OF INSERT
AS 
BEGIN
    SET NOCOUNT ON;

    DECLARE @Column1 INT;
    DECLARE @Column2 INT; -- allow nulls on this column

    SELECT @Column1=Column1, @Column2=Column2 FROM inserted;

    -- Check if an existing record already exists, if not allow the insert.
    IF NOT EXISTS(SELECT * FROM dbo.MyTable WHERE Column1=@Column1 AND Column2=@Column2 @Column2 IS NOT NULL)
    BEGIN
        INSERT INTO dbo.MyTable (Column1, Column2)
            SELECT @Column2, @Column2;
    END
    ELSE
    BEGIN
        RAISERROR('The unique constraint applies on Column1 %d, AND Column2 %d, unless Column2 is NULL.', 16, 1, @Column1, @Column2);
        ROLLBACK TRANSACTION;   
    END

END
Pang
  • 9,564
  • 146
  • 81
  • 122
Michael Brown
  • 1,585
  • 1
  • 22
  • 36
-1

this code if u make a register form with textBox and use insert and ur textBox is empty and u click on submit button .

CREATE UNIQUE NONCLUSTERED INDEX [IX_tableName_Column]
ON [dbo].[tableName]([columnName] ASC) WHERE [columnName] !=`''`;
Nikolay Kostov
  • 16,433
  • 23
  • 85
  • 123
Ahmed Soliman
  • 416
  • 5
  • 11
-2
CREATE UNIQUE NONCLUSTERED INDEX [UIX_COLUMN_NAME]
ON [dbo].[Employee]([Username] ASC) WHERE ([Username] IS NOT NULL) 
WITH (ALLOW_PAGE_LOCKS = ON, ALLOW_ROW_LOCKS = ON, PAD_INDEX = OFF, SORT_IN_TEMPDB = OFF, 
DROP_EXISTING = OFF, IGNORE_DUP_KEY = OFF, STATISTICS_NORECOMPUTE = OFF, ONLINE = OFF, 
MAXDOP = 0) ON [PRIMARY];
solarissmoke
  • 30,039
  • 14
  • 71
  • 73