kendala unik bersyarat

93

Saya memiliki situasi di mana saya perlu menerapkan batasan unik pada sekumpulan kolom, tetapi hanya untuk satu nilai kolom.

Jadi misalnya saya punya tabel seperti Tabel (ID, Nama, RecordStatus).

RecordStatus hanya dapat memiliki nilai 1 atau 2 (aktif atau dihapus), dan saya ingin membuat batasan unik pada (ID, RecordStatus) hanya ketika RecordStatus = 1, karena saya tidak peduli jika ada beberapa catatan yang dihapus dengan yang sama INDO.

Selain menulis pemicu, bisakah saya melakukan itu?

Saya menggunakan SQL Server 2005.

np-keras
sumber
1
Desain ini adalah rasa sakit yang umum. Pernahkah Anda mempertimbangkan untuk mengubah desain sehingga catatan yang 'dihapus' secara fisik dihapus dari tabel dan mungkin dipindahkan ke tabel 'arsip'?
onedaywhen
1
... karena ketidakmampuan untuk menulis kendala UNIK untuk menegakkan kunci sederhana harus dianggap sebagai 'bau kode', IMO. Jika Anda tidak dapat mengubah desain (SQL DDL) karena banyak tabel lain yang mereferensikan tabel ini maka saya berani bertaruh bahwa SQL DML Anda juga menderita sebagai akibatnya, yaitu Anda harus ingat untuk menambahkan ... AND Table.RecordStatus = 1 ' ke sebagian besar kondisi pencarian dan kondisi gabungan yang melibatkan tabel ini dan mengalami bug halus ketika terkadang tabel ini dihilangkan.
onedaywhen

Jawaban:

36

Tambahkan batasan cek seperti ini. Perbedaannya adalah, Anda akan mengembalikan false jika Status = 1 dan Count> 0.

http://msdn.microsoft.com/en-us/library/ms188258.aspx

CREATE TABLE CheckConstraint
(
  Id TINYINT,
  Name VARCHAR(50),
  RecordStatus TINYINT
)
GO

CREATE FUNCTION CheckActiveCount(
  @Id INT
) RETURNS INT AS BEGIN

  DECLARE @ret INT;
  SELECT @ret = COUNT(*) FROM CheckConstraint WHERE Id = @Id AND RecordStatus = 1;
  RETURN @ret;

END;
GO

ALTER TABLE CheckConstraint
  ADD CONSTRAINT CheckActiveCountConstraint CHECK (NOT (dbo.CheckActiveCount(Id) > 1 AND RecordStatus = 1));

INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 1);

INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 2);
-- Msg 547, Level 16, State 0, Line 14
-- The INSERT statement conflicted with the CHECK constraint "CheckActiveCountConstraint". The conflict occurred in database "TestSchema", table "dbo.CheckConstraint".
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);

SELECT * FROM CheckConstraint;
-- Id   Name         RecordStatus
-- ---- ------------ ------------
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  1
-- 2    Oh no!       1
-- 2    Oh no!       2

ALTER TABLE CheckConstraint
  DROP CONSTRAINT CheckActiveCountConstraint;

DROP FUNCTION CheckActiveCount;
DROP TABLE CheckConstraint;
D. Patrick
sumber
saya melihat batasan pemeriksaan tingkat tabel tetapi tidak terlihat ada cara untuk meneruskan nilai yang dimasukkan atau diperbarui ke fungsi, apakah Anda tahu caranya?
np-hard
Oke, saya memposting contoh skrip yang akan membantu Anda membuktikan apa yang saya bicarakan. Saya mengujinya dan berhasil. Jika Anda melihat pada dua baris komentar, Anda akan melihat pesan yang saya terima. Nota bene, dalam implementasi saya, saya hanya memastikan bahwa Anda tidak dapat menambahkan item kedua dengan Id yang sama yang aktif jika sudah ada yang aktif. Anda dapat mengubah logika sedemikian rupa sehingga jika ada yang aktif, Anda tidak dapat menambahkan item apa pun dengan id yang sama. Dengan pola ini, kemungkinannya tidak terbatas.
D. Patrick
Saya lebih suka logika yang sama dalam pemicu. "kueri dalam fungsi skalar ... dapat menimbulkan masalah besar jika batasan PERIKSA Anda bergantung pada kueri dan jika lebih dari satu baris dipengaruhi oleh pembaruan apa pun. Apa yang terjadi adalah bahwa batasan diperiksa sekali untuk setiap baris sebelum pernyataan selesai . Itu berarti atomicity pernyataan rusak dan fungsi akan diekspos ke database dalam keadaan tidak konsisten. Hasilnya tidak dapat diprediksi dan tidak akurat. " Lihat: blogs.conchango.com/davidportas/archive/2007/02/19/…
onedaywhen
Itu hanya sebagian benar suatu hari nanti. Basis data berperilaku secara konsisten dan dapat diprediksi. Batasan cek akan dieksekusi setelah baris ditambahkan ke tabel dan sebelum transaksi dilakukan oleh dbms dan Anda dapat mengandalkannya. Blog itu berbicara tentang masalah yang cukup unik di mana Anda perlu mengeksekusi batasan terhadap satu set sisipan, bukan hanya satu sisipan pada satu waktu. ashish meminta batasan pada satu penyisipan pada satu waktu dan batasan ini akan bekerja secara akurat, dapat diprediksi, dan konsisten. Saya minta maaf jika ini terdengar singkat; Saya kehabisan karakter.
D. Patrick
3
Ini berfungsi dengan baik untuk sisipan tetapi tampaknya tidak berfungsi untuk pembaruan. EG Menambahkan ini setelah sisipan lain berfungsi di sini ketika saya tidak mengharapkannya. SISIPKAN KE NILAI-NILAI CheckConstraint (1, 'No ProblemsA', 2); perbarui CheckConstraint set Recordstatus = 1 di mana name = 'No ProblemsA'
dwidel
149

Lihatlah, indeks yang difilter . Dari dokumentasi (penekanan saya):

Indeks yang difilter adalah indeks tidak terkluster yang dioptimalkan yang khususnya cocok untuk mencakup kueri yang memilih dari subset data yang ditentukan dengan baik. Ini menggunakan predikat filter untuk mengindeks sebagian baris dalam tabel. Indeks terfilter yang dirancang dengan baik dapat meningkatkan kinerja kueri serta mengurangi pemeliharaan indeks dan biaya penyimpanan dibandingkan dengan indeks tabel penuh.

Dan inilah contoh yang menggabungkan indeks unik dengan predikat filter:

create unique index MyIndex
on MyTable(ID)
where RecordStatus = 1;

Hal ini pada dasarnya memaksa keunikan IDsaat RecordStatusini1 .

Setelah indeks tersebut dibuat, pelanggaran keunikan akan menimbulkan kesalahan:

Msg 2601, Level 14,
Status 1, Baris 13 Tidak dapat menyisipkan baris kunci duplikat dalam objek 'dbo.MyTable' dengan indeks unik 'MyIndex'. Nilai kunci duplikatnya adalah (9999).

Catatan: indeks yang difilter diperkenalkan di SQL Server 2008. Untuk versi SQL Server sebelumnya, harap lihat jawaban ini .

kanon
sumber
Perhatikan bahwa SQL Server memerlukan ansi_paddingindeks yang difilter, jadi pastikan bahwa opsi ini diaktifkan dengan menjalankan SET ANSI_PADDING ONsebelum membuat indeks yang difilter.
naXa
10

Anda bisa memindahkan rekaman yang dihapus ke tabel yang tidak memiliki batasan, dan mungkin menggunakan tampilan dengan UNION dari dua tabel untuk mempertahankan tampilan tabel tunggal.

Carl Manaster
sumber
2
Itu sebenarnya Carl yang cukup pintar. Ini bukan jawaban atas pertanyaan itu sendiri, tapi ini solusi yang bagus. Jika tabel memiliki banyak baris, itu juga dapat mempercepat pencarian rekaman aktif karena Anda dapat melihat tabel rekaman aktif. Ini juga akan mempercepat batasan karena batasan unik menggunakan indeks sebagai lawan dari batasan cek yang saya tulis di bawah ini yang harus mengeksekusi penghitungan. Saya suka itu.
D. Patrick
3

Anda dapat melakukan ini dengan cara yang sangat hacky ...

Buat tampilan skemabound di meja Anda.

BUAT LIHAT Apapun SELECT * FROM Table WHERE RecordStatus = 1

Sekarang buat batasan unik pada tampilan dengan bidang yang Anda inginkan.

Satu catatan tentang tampilan skemabound, jika Anda mengubah tabel yang mendasari Anda harus membuat ulang tampilan. Banyak gotcha karena itu.

Min
sumber
Ini adalah saran yang cukup bagus, dan bukan yang "hacky". Berikut adalah informasi selengkapnya tentang alternatif indeks yang difilter ini .
Scott Whitlock
Itu ide yang buruk. Pertanyaannya bukan.
FabianoLothor
Saya menggunakan tampilan skemabound sekali, dan tidak pernah mengulangi kesalahan. Mereka bisa menjadi masalah kerajaan untuk diajak bekerja sama. Bukan berarti Anda harus membuat ulang tampilan jika Anda mengubah tabel yang mendasarinya - Anda mungkin harus melakukannya untuk semua tampilan, setidaknya di SQL server. Anda tidak dapat mengubah tabel tanpa melepaskan tampilan terlebih dahulu, yang mungkin tidak dapat Anda lakukan tanpa memberikan referensi ke tabel terlebih dahulu. Oh, ditambah lagi penyimpanannya bisa jadi bermasalah - entah karena ruangnya, atau karena biaya yang ditambahkan untuk memasukkan dan memperbarui.
MattW
1

Karena, Anda akan mengizinkan duplikat, batasan unik tidak akan berfungsi. Anda dapat membuat batasan pemeriksaan untuk kolom RecordStatus dan prosedur tersimpan untuk INSERT yang memeriksa catatan aktif yang ada sebelum memasukkan ID duplikat.

ichiban
sumber
1

Jika Anda tidak dapat menggunakan NULL sebagai RecordStatus seperti yang disarankan Bill, Anda dapat menggabungkan idenya dengan indeks berbasis fungsi. Buat fungsi yang mengembalikan NULL jika RecordStatus bukan salah satu nilai yang ingin Anda pertimbangkan dalam batasan Anda (dan RecordStatus sebaliknya) dan buat indeks di atasnya.

Keuntungannya adalah Anda tidak perlu memeriksa baris lain secara eksplisit dalam tabel dalam batasan Anda, yang dapat menyebabkan masalah performa.

Saya harus mengatakan saya tidak tahu SQL server sama sekali, tetapi saya telah berhasil menggunakan pendekatan ini di Oracle.

Batak
sumber
ide bagus, tetapi tidak ada berbasis fungsi yang diindeks di server sql, terima kasih atas jawabannya
np-hard