Bagaimana cara membuat batasan unik yang juga memungkinkan nulls?

620

Saya ingin memiliki batasan unik pada kolom yang akan saya isi dengan GUID. Namun, data saya mengandung nilai nol untuk kolom ini. Bagaimana cara membuat batasan yang memungkinkan beberapa nilai null?

Berikut ini contoh skenario . Pertimbangkan skema ini:

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)
)

Kemudian lihat kode ini untuk mengetahui apa yang ingin saya capai:

-- 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);

Pernyataan terakhir gagal dengan pesan:

Pelanggaran batasan unik UNIQUE 'UQ_People_LibraryCardId'. Tidak dapat memasukkan kunci duplikat di objek 'dbo.People'.

Bagaimana saya bisa mengubah skema dan / atau batasan keunikan saya sehingga memungkinkan beberapa NULLnilai, sambil tetap memeriksa keunikan pada data aktual?

Stuart
sumber
Hubungkan masalah untuk kompatibilitas standar untuk memilih: connect.microsoft.com/SQLServer/Feedback/Details/299229
Vadzim
Kemungkinan duplikat dari Cara membuat indeks unik pada kolom NULL?
Frédéric
Kendala UNIK dan memungkinkan NULL. ? Itu masuk akal. Itu tidak mungkin
flik
13
@lik, lebih baik tidak merujuk ke "akal sehat". Itu bukan argumen yang valid. Terutama ketika mempertimbangkan bahwa nullitu bukan nilai tetapi tidak adanya nilai. Per standar SQL, nulltidak dianggap sama dengan null. Jadi mengapa multiple nullharus menjadi pelanggaran keunikan?
Frédéric

Jawaban:

144

SQL Server 2008 +

Anda dapat membuat indeks unik yang menerima beberapa NULL dengan WHEREklausa. Lihat jawabannya di bawah .

Sebelum ke SQL Server 2008

Anda tidak dapat membuat batasan UNIK dan mengizinkan NULL. Anda harus menetapkan nilai default NEWID ().

Perbarui nilai yang ada ke NEWID () di mana NULL sebelum membuat batasan UNIK.

Jose Basilio
sumber
2
dan ini akan secara retrospektif menambah nilai pada baris yang ada, jika demikian ini yang perlu saya lakukan, terima kasih?
Stuart
1
Anda perlu menjalankan pernyataan UPDATE untuk mengatur nilai yang ada ke NEWID () di mana bidang yang ada IS NULL
Jose Basilio
54
Jika Anda menggunakan SQL Server 2008 atau lebih baru, lihat jawaban di bawah ini dengan lebih dari 100 upvote. Anda dapat menambahkan klausa WHERE ke Batasan Unik Anda.
Darren Griffith
1
Masalah ini juga mengenai ADO.NET DataTables. Jadi, meskipun saya dapat mengizinkan nol di bidang dukungan menggunakan metode ini, DataTable tidak akan membiarkan saya menyimpan NULL di kolom unik di tempat pertama. Jika ada yang tahu solusi untuk itu, silakan posting di sini
dotNET
6
Kawan pastikan Anda gulir ke bawah dan baca jawabannya dengan 600 upvotes. Tidak lebih dari 100.
Luminous
1288

Apa yang Anda cari memang bagian dari standar ANSI SQL: 92, SQL: 1999 dan SQL: 2003, yaitu batasan UNIK harus melarang duplikat nilai-nilai non-NULL tetapi menerima beberapa nilai NULL.

Dalam dunia Microsoft SQL Server, NULL tunggal diperbolehkan tetapi beberapa NULL tidak ...

Di SQL Server 2008 , Anda dapat menentukan indeks yang difilter unik berdasarkan predikat yang mengecualikan NULLs:

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

Di versi sebelumnya, Anda dapat menggunakan VIEWS dengan predikat NOT NULL untuk menegakkan batasan.

Vincent Buck
sumber
3
ini mungkin cara terbaik untuk melakukan ini. tidak yakin apakah ada dampak kinerja? siapa saja?
Simon_Weaver
3
Saya mencoba untuk melakukan hal ini dalam SQL Server 2008 Express edisi dan saya mendapatkan kesalahan sebagai berikut: BUAT INDEKS UND NONCLUSTERED UNIK UC_MailingId PADA [SLS-CP]. Level 15, Status 1, Baris 3 Sintaks salah di dekat kata kunci 'WHERE'. Jika saya menghapus klausa mana DDL berjalan dengan baik, tapi tentu saja, tidak melakukan apa yang saya butuhkan. Ada ide?
Kenneth Baltrinic
4
Kecuali jika saya salah, Anda tidak dapat membuat Kunci Asing dari Indeks Unik seperti Anda dapat dari Batasan Unik. (Setidaknya SSMS mengeluh kepada saya ketika saya mencoba.) Akan menyenangkan untuk dapat memiliki kolom nullable yang selalu unik (bila tidak nol) menjadi sumber hubungan Foreign Key.
Vaccano
8
Benar-benar jawaban yang bagus. Sayang sekali itu disembunyikan oleh orang yang diterima sebagai jawaban. Solusi ini hampir tidak menarik perhatian saya, tetapi berfungsi seperti keajaiban dalam implementasi saya sekarang.
Coral Doe
2
Alternatif lain untuk SQL 2005 dan di bawah ini adalah Computed Column alias trik "Nullbuster". stackoverflow.com/a/191729/132461 Ini menyelamatkan Anda dari mengacaukan database dengan tampilan lain, Anda hanya memiliki kolom lain sebagai gantinya - biasanya Bernama ColumnA-Nullbuster jika ColumnA adalah yang Anda inginkan menjadi ANSI nullable UNIK. Letakkan Indeks UNIK (atau kendala untuk mengekspresikan niat bisnis) pada ColumnA-Nullbuster dan itu akan menegakkan keunikan pada ColumnA
DanO
34

SQL Server 2008 Dan Ke Atas

Cukup filter indeks unik:

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

Dalam Versi Lebih Rendah, Tampilan Terwujud Masih Tidak Diperlukan

Untuk SQL Server 2005 dan sebelumnya, Anda bisa melakukannya tanpa melihat. Saya baru saja menambahkan batasan unik seperti yang Anda minta ke salah satu meja saya. Mengingat bahwa saya ingin keunikan dalam kolom SamAccountName, tetapi saya ingin memperbolehkan beberapa NULL, saya menggunakan kolom terwujud daripada tampilan terwujud:

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

Anda hanya perlu meletakkan sesuatu di kolom yang dihitung yang akan dijamin unik di seluruh tabel ketika kolom unik yang sebenarnya diinginkan adalah NULL. Dalam hal ini, PartyIDadalah kolom identitas dan menjadi numerik tidak akan pernah cocok dengan itu SamAccountName, jadi itu berfungsi untuk saya. Anda dapat mencoba metode Anda sendiri — pastikan Anda memahami domain data Anda sehingga tidak ada kemungkinan persimpangan dengan data nyata. Itu bisa sesederhana menambahkan karakter pembeda seperti ini:

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

Bahkan jika PartyID suatu hari menjadi non-numerik dan bisa bertepatan dengan SamAccountName, sekarang tidak masalah.

Perhatikan bahwa keberadaan indeks termasuk kolom yang dihitung secara implisit menyebabkan setiap hasil ekspresi disimpan ke disk dengan data lain dalam tabel, yang TIDAK mengambil ruang disk tambahan.

Perhatikan bahwa jika Anda tidak ingin indeks, Anda masih dapat menyimpan CPU dengan membuat ekspresi menjadi prakalkulasi ke disk dengan menambahkan kata kunci PERSISTEDke akhir definisi ekspresi kolom.

Di SQL Server 2008 dan lebih tinggi, pasti gunakan solusi yang difilter jika Anda bisa!

Kontroversi

Harap dicatat bahwa beberapa profesional basis data akan melihat ini sebagai kasus "pengganti NULLs", yang pasti memiliki masalah (sebagian besar disebabkan oleh masalah seputar upaya menentukan kapan sesuatu bernilai nyata atau nilai pengganti untuk data yang hilang) ; ada juga masalah dengan jumlah nilai pengganti non-NULL yang berlipat ganda seperti orang gila).

Namun, saya percaya kasus ini berbeda. Kolom yang dihitung yang saya tambahkan tidak akan pernah digunakan untuk menentukan apa pun. Itu tidak memiliki arti itu sendiri, dan mengkodekan tidak ada informasi yang belum ditemukan secara terpisah di kolom lain yang didefinisikan dengan benar. Seharusnya tidak pernah dipilih atau digunakan.

Jadi, cerita saya adalah bahwa ini bukan NULL pengganti, dan saya berpegang teguh pada itu! Karena kita sebenarnya tidak ingin nilai non-NULL untuk tujuan apa pun selain untuk menipuUNIQUE indeks agar mengabaikan , kasus penggunaan kami tidak memiliki masalah yang muncul dengan pembuatan NULL pengganti yang normal.

Semua yang dikatakan, saya tidak punya masalah dengan menggunakan tampilan yang diindeks sebagai gantinya — tetapi itu membawa beberapa masalah dengan itu seperti persyaratan menggunakan SCHEMABINDING. Bersenang-senang menambahkan kolom baru ke tabel basis Anda (Anda minimal harus menjatuhkan indeks, dan kemudian meninggalkan tampilan atau mengubah tampilan agar tidak terikat skema). Lihat daftar lengkap (panjang) persyaratan untuk membuat tampilan yang diindeks di SQL Server (2005) (juga versi yang lebih baru), (2000) .

Memperbarui

Jika kolom Anda numerik, mungkin ada tantangan untuk memastikan bahwa penggunaan kendala unik Coalescetidak menghasilkan tabrakan. Dalam hal ini, ada beberapa opsi. Satu mungkin menggunakan angka negatif, untuk menempatkan "pengganti NULLs" hanya di kisaran negatif, dan "nilai nyata" hanya di kisaran positif. Sebagai alternatif, pola berikut dapat digunakan. Dalam tabel Issue(di mana IssueIDada PRIMARY KEY), mungkin ada atau tidak ada TicketID, tetapi jika ada, itu harus unik.

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);

Jika IssueID 1 memiliki tiket 123, batasannya UNIQUEadalah nilai (123, NULL). Jika IssueID 2 tidak memiliki tiket, tiket tersebut akan aktif (NULL, 2). Beberapa pemikiran akan menunjukkan bahwa batasan ini tidak dapat diduplikasi untuk setiap baris dalam tabel, dan masih memungkinkan beberapa NULL.

ErikE
sumber
16

Untuk orang-orang yang menggunakan Microsoft SQL Server Manager dan ingin membuat indeks unik tetapi tidak dapat dibatalkan, Anda dapat membuat indeks unik seperti biasa di indeks properti Anda untuk indeks baru, pilih "Filter" dari panel sebelah kiri, lalu masukkan filter Anda (yang merupakan klausa tempat Anda). Seharusnya membaca sesuatu seperti ini:

([YourColumnName] IS NOT NULL)

Ini berfungsi dengan MSSQL 2012

Howard
sumber
Cara membuat indeks disaring di bawah Microsoft SQL Server Management Studio dijelaskan di sini dan bekerja sempurna: msdn.microsoft.com/en-us/library/cc280372.aspx
Jan
9

Ketika saya menerapkan indeks unik di bawah ini:

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

setiap pembaruan dan penyisipan non null gagal dengan kesalahan di bawah ini:

UPDATE gagal karena opsi SET berikut memiliki pengaturan yang salah: 'ARITHABORT'.

Saya menemukan ini di MSDN

SET ARITHABORT harus AKTIF ketika Anda membuat atau mengubah indeks pada kolom yang dihitung atau tampilan yang diindeks. Jika SET ARITHABORT OFF, CREATE, UPDATE, INSERT, dan DELETE pada tabel dengan indeks pada kolom yang dihitung atau tampilan indeks akan gagal.

Jadi agar ini berfungsi dengan benar, saya melakukan ini

Klik kanan [Basis Data] -> Properti -> Opsi -> Opsi Lainnya -> Macam-Macam -> Aritmatika Dibatasi Diaktifkan -> true

Saya percaya adalah mungkin untuk mengatur opsi ini menggunakan kode

ALTER DATABASE "DBNAME" SET ARITHABORT ON

tetapi saya belum menguji ini

Mike Taylor
sumber
6

Buat tampilan yang hanya memilih non-NULL kolom dan buat UNIQUE INDEXpada tampilan:

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

CREATE UNIQUE INDEX ux_myview_mycolumn ON myview (mycolumn)

Perhatikan bahwa Anda harus melakukan INSERTdan UPDATEpada tampilan, bukan pada tabel.

Anda dapat melakukannya dengan INSTEAD OFpemicu:

CREATE TRIGGER trg_mytable_insert ON mytable
INSTEAD OF INSERT
AS
BEGIN
        INSERT
        INTO    myview
        SELECT  *
        FROM    inserted
END
Quassnoi
sumber
jadi apakah saya perlu mengubah dal saya untuk menyisipkan ke tampilan?
Stuart
1
Anda dapat membuat pemicu BUKAN INSERT.
Quassnoi
6

Itu bisa dilakukan pada perancang juga

Klik kanan pada Index> Properties untuk mendapatkan jendela ini

menangkap

Yonatan Tuchinsky
sumber
Alternatif yang sangat bagus jika Anda memiliki akses ke perancang
Francisco
Meskipun, seperti yang baru saja saya temukan, begitu Anda memiliki data di meja Anda, Anda tidak dapat lagi menggunakan perancang. Tampaknya mengabaikan filter dan setiap upaya pembaruan tabel dipenuhi dengan pesan "Kunci duplikat tidak diizinkan"
MortimerCat
4

Dimungkinkan untuk membuat batasan unik pada Clustered Indexed View

Anda dapat membuat tampilan seperti ini:

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

dan kendala unik seperti ini:

CREATE UNIQUE CLUSTERED INDEX UIX_VIEW_OFYOURTABLE 
  ON dbo.VIEW_OfYourTable(YourUniqueColumnWithNullValues)
Letnan Keersmaekers
sumber
2

Mungkin pertimbangkan INSTEAD OFpemicu dan lakukan pengecekan sendiri? Dengan indeks non-cluster (non-unik) pada kolom untuk mengaktifkan pencarian.

Marc Gravell
sumber
1

Seperti yang dinyatakan sebelumnya, SQL Server tidak mengimplementasikan standar ANSI ketika datang ke UNIQUE CONSTRAINT. Ada tiket di Microsoft Connect untuk ini sejak 2007. Seperti yang disarankan di sana dan di sini opsi terbaik pada hari ini adalah menggunakan indeks yang difilter sebagaimana dinyatakan dalam jawaban lain atau kolom yang dihitung, misalnya:

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])
)
Baris Akar
sumber
1

Anda dapat membuat BUKAN pemicu untuk memeriksa kondisi dan kesalahan tertentu jika terpenuhi. Membuat indeks bisa mahal pada tabel yang lebih besar.

Ini sebuah contoh:

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
sumber
-1

Anda tidak bisa melakukan ini dengan UNIQUEkendala, tetapi Anda bisa melakukan ini di pemicu.

    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
Michael Brown
sumber
-1
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];
pengguna5536124
sumber
-1

kode ini jika Anda membuat formulir pendaftaran dengan textBox dan gunakan masukkan dan ur textBox Anda kosong dan Anda klik tombol kirim.

CREATE UNIQUE NONCLUSTERED INDEX [IX_tableName_Column]
ON [dbo].[tableName]([columnName] ASC) WHERE [columnName] !=`''`;
Ahmed Soliman Flasha
sumber