Cara menerapkan bendera 'default' yang hanya dapat ditetapkan pada satu baris

31

Misalnya, dengan tabel yang mirip dengan ini:

create table foo(bar int identity, chk char(1) check (chk in('Y', 'N')));

Tidak masalah jika bendera diimplementasikan sebagai char(1), a, bitatau apa pun. Saya hanya ingin dapat menegakkan batasan yang hanya dapat ditetapkan pada satu baris.

Jack Douglas
sumber
terinspirasi oleh pertanyaan ini yang terbatas pada MySQL
Jack Douglas
2
Cara pertanyaan diucapkan menunjukkan bahwa menggunakan tabel harus menjadi jawaban yang salah. Tapi kadang-kadang (paling sering?) Menambahkan tabel lain adalah ide yang bagus. Dan menambahkan tabel adalah agnostik basis data sepenuhnya.
Mike Sherrill 'Cat Recall'

Jawaban:

31

SQL Server 2008 - Indeks unik yang difilter

CREATE UNIQUE INDEX IX_Foo_chk ON dbo.Foo(chk) WHERE chk = 'Y'
Mark Storey-Smith
sumber
16

SQL Server 2000, 2005:

Anda dapat memanfaatkan fakta bahwa hanya satu nol yang diizinkan dalam indeks unik:

create table t( id int identity, 
                chk1 char(1) not null default 'N' check(chk1 in('Y', 'N')), 
                chk2 as case chk1 when 'Y' then null else id end );
create unique index u_chk on t(chk2);

untuk tahun 2000, Anda mungkin perlu SET ARITHABORT ON(terima kasih kepada @gbn untuk info ini)

Jack Douglas
sumber
14

Peramal:

Karena Oracle tidak mengindeks entri di mana semua kolom yang diindeks adalah nol, Anda dapat menggunakan indeks unik berbasis fungsi:

create table foo(bar integer, chk char(1) not null check (chk in('Y', 'N')));
create unique index idx on foo(case when chk='Y' then 'Y' end);

Indeks ini paling banyak hanya akan mengindeks satu baris.

Mengetahui fakta indeks ini, Anda juga dapat mengimplementasikan kolom bit sedikit berbeda:

create table foo(bar integer, chk char(1) check (chk ='Y') UNIQUE);

Di sini nilai yang mungkin untuk kolom chkadalah Ydan NULL. Hanya satu baris paling banyak yang dapat memiliki nilaiY.

Vincent Malgrat
sumber
chk butuh not nullkendala?
Jack Douglas
@jack: Anda dapat menambahkan not nullbatasan jika Anda tidak ingin nulls (Tidak jelas bagi saya dari spesifikasi pertanyaan). Hanya satu baris yang dapat memiliki nilai 'Y' dalam hal apa pun.
Vincent Malgrat
+1 Saya mengerti maksud Anda - Anda benar itu tidak perlu (tapi mungkin sedikit lebih rapi, terutama jika dikombinasikan dengan a default)?
Jack Douglas
2
@jack: komentar Anda membuat saya menyadari bahwa kemungkinan yang lebih sederhana tersedia jika Anda menerima bahwa kolom dapat berupa Yatau null, lihat pembaruan saya.
Vincent Malgrat
1
Opsi 2 memiliki manfaat tambahan, indeks akan menjadi kecil seperti nullyang dilewati - dengan biaya beberapa kejelasan mungkin
Jack Douglas
13

Saya pikir ini adalah kasus penataan tabel database Anda dengan benar. Untuk membuatnya lebih konkret, jika Anda memiliki seseorang dengan beberapa alamat dan Anda ingin seseorang menjadi default, saya pikir Anda harus menyimpan addressID dari alamat default di tabel orang, tidak memiliki kolom default di tabel alamat:

Person
-------
PersonID
Name
etc.
DefaultAddressID (fk to addressID)

Address
--------
AddressID
Street
City, State, Zip, etc.

Anda dapat membuat DefaultAddressID nullable, tetapi dengan cara ini struktur memberlakukan kendala Anda.

Decker97
sumber
12

MySQL:

create table foo(bar serial, chk boolean unique);
insert into foo(chk) values(null);
insert into foo(chk) values(null);
insert into foo(chk) values(false);
insert into foo(chk) values(true);

select * from foo;
+-----+------+
| bar | chk  |
+-----+------+
|   1 | NULL |
|   2 | NULL |
|   3 |    0 |
|   4 |    1 |
+-----+------+

insert into foo(chk) values(true);
ERROR 1062 (23000): Duplicate entry '1' for key 2
insert into foo(chk) values(false);
ERROR 1062 (23000): Duplicate entry '0' for key 2

Batasan pemeriksaan diabaikan di MySQL sehingga kami harus mempertimbangkan nullatau falsesalah dan truebenar. Paling banyak 1 baris dapat memilikichk=true

Anda mungkin menganggapnya sebagai peningkatan untuk menambahkan pemicu untuk diubah falsemenjadi truepada saat memasukkan / memperbarui sebagai solusi untuk kurangnya batasan pemeriksaan - IMO itu bukan perbaikan.

Saya berharap dapat menggunakan char (0) karena itu

juga cukup bagus ketika Anda membutuhkan kolom yang hanya dapat mengambil dua nilai: Kolom yang didefinisikan sebagai CHAR (0) NULL hanya menempati satu bit dan hanya dapat mengambil nilai-nilai NULL dan ''

Sayangnya, dengan MyISAM dan InnoDB setidaknya, saya mengerti

ERROR 1167 (42000): The used storage engine can't index column 'chk'

--edit

ini bukan solusi yang baik karena pada MySQL, booleanadalah sinonim untuktinyint(1) , dan memungkinkan nilai-nilai non-nol dari 0 atau 1. Ada kemungkinan bahwa itu bitakan menjadi pilihan yang lebih baik

Jack Douglas
sumber
Ini bisa menjawab komentar saya untuk jawaban RolandoMySQLDBA: bisakah kita memiliki solusi MySQL dengan DRI?
gbn
Ini agak jelek meskipun karena null, false, true- saya lakukan heran jika ada sesuatu yang lebih rapi ...
Jack Douglas
@Jack - +1 untuk percobaan yang bagus pada pendekatan DRI murni di MySQL.
RolandoMySQLDBA
Saya akan menyarankan menghindari penggunaan false di sini karena batasan unik hanya akan memungkinkan satu nilai palsu tersebut diberikan. Jika null menunjukkan false maka harus digunakan secara konsisten di seluruh - penghindaran false dapat ditegakkan jika validasi tambahan tersedia (misalnya JSR-303 / hibernate-validator).
Steve Chambers
1
Versi terbaru dari MySQL / MariaDB mengimplementasikan kolom virtual yang saya percaya memungkinkan solusi yang sedikit lebih elegan diuraikan di bawah di dba.stackexchange.com/a/144847/94908
MattW.
10

SQL Server:

Bagaimana cara melakukannya:

  1. Cara terbaik adalah indeks yang difilter. Menggunakan DRI
    SQL Server 2008+

  2. Kolom yang dihitung dengan keunikan. Menggunakan DRI.
    Lihat jawaban Jack Douglas. SQL Server 2005 dan sebelumnya

  3. Tampilan terindeks / terwujud yang seperti indeks yang difilter. Menggunakan DRI
    Semua versi.

  4. Pelatuk. Menggunakan Kode, bukan DRI.
    Semua versi

Bagaimana tidak melakukannya:

  1. Periksa kendala dengan UDF. Ini tidak aman untuk konkurensi dan isolasi snapshot.
    Lihat Satu Dua Tiga Empat
gbn
sumber
10

PostgreSQL:

create table foo(bar serial, chk char(1) unique check(chk='Y'));
insert into foo default values;
insert into foo default values;
insert into foo(chk) values('Y');

select * from foo;
 bar | chk
-----+-----
   1 |
   2 |
   3 | Y

insert into foo(chk) values('Y');
ERROR:  duplicate key value violates unique constraint "foo_chk_key"

--edit

atau (jauh lebih baik), gunakan indeks parsial unik :

create table foo(bar serial, chk boolean not null default false);
create unique index foo_i on foo(chk) where chk;
insert into foo default values;
insert into foo default values;
insert into foo(chk) values(true);

select * from foo;
 bar | chk
-----+-----
   1 | f
   2 | f
   3 | t
(3 rows)

insert into foo(chk) values(true);
ERROR:  duplicate key value violates unique constraint "foo_i"
Jack Douglas
sumber
6

Masalah semacam ini adalah alasan lain mengapa saya menanyakan pertanyaan ini:

Pengaturan Aplikasi dalam Database

Jika Anda memiliki tabel pengaturan aplikasi di database Anda, Anda bisa memiliki entri yang akan merujuk ID dari satu catatan yang Anda ingin dianggap 'istimewa'. Maka Anda hanya akan mencari apa ID dari tabel pengaturan Anda, dengan cara ini Anda tidak perlu seluruh kolom untuk hanya satu item yang ditetapkan.

CenterOrbit
sumber
Ini adalah saran yang bagus: Ini lebih sesuai dengan desain yang dinormalisasi, bekerja dengan platform basis data apa pun, dan paling mudah diterapkan.
Nick Chammas
+1 tetapi perhatikan bahwa "seluruh kolom" mungkin tidak menggunakan ruang fisik apa pun tergantung pada RDBMS Anda :)
Jack Douglas
6

Kemungkinan pendekatan menggunakan teknologi yang diimplementasikan secara luas:

1) Cabut hak istimewa 'penulis' di atas meja. Buat prosedur CRUD yang memastikan kendala diberlakukan pada batas-batas transaksi.

2) 6NF: jatuhkan CHAR(1)kolom. Tambahkan tabel referensi dibatasi untuk memastikan kardinalitasnya tidak dapat melebihi satu:

alter table foo ADD UNIQUE (bar);

create table foo_Y
(
 x CHAR(1) DEFAULT 'x' NOT NULL UNIQUE CHECK (x = 'x'), 
 bar int references foo (bar)
);

Ubah semantik aplikasi sehingga 'default' yang dianggap adalah baris dalam tabel baru. Mungkin menggunakan tampilan untuk merangkum logika ini.

3) Jatuhkan CHAR(1)kolom. Tambahkan seqkolom bilangan bulat. Beri batasan unik seq. Ubah aplikasi semantik sehingga dianggap 'default' adalah baris di mana seqnilainya adalah satu atau seqnilai terbesar / nilai terkecil atau serupa. Mungkin menggunakan tampilan untuk merangkum logika ini.

suatu hari nanti
sumber
5

Bagi mereka yang menggunakan MySQL, berikut ini adalah Prosedur Tersimpan yang sesuai:

DELIMITER $$
DROP PROCEDURE IF EXISTS SetDefaultForZip;
CREATE PROCEDURE SetDefaultForZip (NEWID INT)
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;

    SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
    IF FOUND_TRUE = 1 THEN
        SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
        IF NEWID <> OLDID THEN
            UPDATE PostalCode SET isDefault = FALSE WHERE ID = OLDID;
            UPDATE PostalCode SET isDefault = TRUE  WHERE ID = NEWID;
        END IF;
    ELSE
        UPDATE PostalCode SET isDefault = TRUE WHERE ID = NEWID;
    END IF;
END;
$$
DELIMITER ;

Untuk memastikan meja Anda bersih dan prosedur tersimpan berfungsi, dengan anggapan ID 200 adalah default, jalankan langkah-langkah ini:

ALTER TABLE PostalCode DROP INDEX isDefault_ndx;
UPDATE PostalCodes SET isDefault = FALSE;
ALTER TABLE PostalCode ADD INDEX isDefault_ndx (isDefault);
CALL SetDefaultForZip(200);
SELECT ID FROM PostalCodes WHERE isDefault = TRUE;

Berikut adalah Pemicu yang membantu juga:

DELIMITER $$
CREATE TRIGGER postalcodes_bu BEFORE UPDATE ON PostalCodes FOR EACH ROW
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;
    IF NEW.isDefault = TRUE THEN
        SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
        IF FOUND_TRUE = 1 THEN
            SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
            UPDATE PostalCodes SET isDefault = FALSE WHERE ID = OLDID;
        END IF;
    END IF;
END;
$$
DELIMITER ;

Untuk memastikan meja Anda bersih dan pemicu berfungsi, dengan anggapan ID 200 adalah default, jalankan langkah-langkah ini:

DROP TRIGGER postalcodes_bu;
ALTER TABLE PostalCode DROP INDEX isDefault_ndx;
UPDATE PostalCodes SET isDefault = FALSE;
ALTER TABLE PostalCode ADD INDEX isDefault_ndx (isDefault);
DELIMITER $$
CREATE TRIGGER postalcodes_bu BEFORE UPDATE ON PostalCodes FOR EACH ROW
BEGIN
    DECLARE FOUND_TRUE,OLDID INT;
    IF NEW.isDefault = TRUE THEN
        SELECT COUNT(1) INTO FOUND_TRUE FROM PostalCode WHERE isDefault = TRUE;
        IF FOUND_TRUE = 1 THEN
            SELECT ID INTO OLDID FROM PostalCode WHERE isDefault = TRUE;
            UPDATE PostalCodes SET isDefault = FALSE WHERE ID = OLDID;
        END IF;
    END IF;
END;
$$
DELIMITER ;
UPDATE PostalCodes SET isDefault = TRUE WHERE ID = 200;
SELECT ID FROM PostalCodes WHERE isDefault = TRUE;

Cobalah !!!

RolandoMySQLDBA
sumber
3
Apakah tidak ada solusi berbasis DRI untuk MySQL? Hanya kode? Saya ingin tahu karena saya mulai menggunakan MySQL semakin banyak ...
gbn
4

Dalam SQL Server 2000 dan lebih Anda dapat menggunakan Tampilan Terindeks untuk mengimplementasikan kendala kompleks (atau multi-tabel) seperti yang Anda minta.
Oracle juga memiliki implementasi serupa untuk tampilan terwujud dengan kendala pemeriksaan tangguhan.

Lihat posting saya di sini .

spaghettidba
sumber
Bisakah Anda memberikan sedikit lebih banyak "daging" dalam jawaban ini, seperti potongan kode pendek? Sekarang ini hanya beberapa ide umum dan tautan.
Nick Chammas
Akan agak sulit untuk mencocokkan contoh di sini. Jika Anda mengeklik tautannya, Anda akan menemukan "daging" yang Anda cari.
spaghettidba
3

Standar Transisi SQL-92, diimplementasikan secara luas misalnya SQL Server 2000 dan di atas:

Cabut hak istimewa 'penulis' dari tabel. Buat dua tampilan untuk WHERE chk = 'Y'dan WHERE chk = 'N'masing - masing, termasuk WITH CHECK OPTION. Untuk WHERE chk = 'Y'tampilan, sertakan kondisi pencarian agar efek kardinalitasnya tidak melebihi satu. Berikan 'penulis' hak istimewa pada pandangan.

Kode contoh untuk tampilan:

CREATE VIEW foo_chk_N
AS
SELECT *
  FROM foo AS f1
 WHERE chk = 'N' 
WITH CHECK OPTION

CREATE VIEW foo_chk_Y
AS
SELECT *
  FROM foo AS f1
 WHERE chk = 'Y' 
       AND 1 >= (
                 SELECT COUNT(*)
                   FROM foo AS f2
                  WHERE f2.chk = 'Y'
                )
WITH CHECK OPTION
suatu hari nanti
sumber
bahkan jika RDBMS Anda mendukung ini, ini akan bersambung seperti orang gila jadi jika Anda memiliki lebih dari satu pengguna, Anda mungkin memiliki masalah
Jack Douglas
jika banyak pengguna memodifikasi secara bersamaan mereka harus berdiri dalam antrean (bersambung) - kadang-kadang ini ok, sering kali tidak (pikirkan OLTP yang berat atau transaksi panjang).
Jack Douglas
3
Terima kasih telah mengklarifikasi. Saya harus mengatakan jika banyak pengguna sering mengatur satu-satunya baris default maka pilihan desain (kolom bendera dalam tabel yang sama) dipertanyakan.
onedaywhen
3

Inilah solusi untuk MySQL dan MariaDB menggunakan kolom virtual yang sedikit lebih elegan. Ini membutuhkan MySQL> = 5.7.6 atau MariaDB> = 5.2:

MariaDB [db]> create table foo(bar varchar(255), chk boolean);

MariaDB [db]> describe foo;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| bar   | varchar(255) | YES  |     | NULL    |       |
| chk   | tinyint(1)   | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

Buat kolom virtual yang NULL jika Anda tidak ingin memaksakan contrraint Unik:

MariaDB [db]> ALTER table foo ADD checked_bar varchar(255) as (IF(chk, bar, null)) PERSISTENT UNIQUE;

(Untuk MySQL, gunakan STOREDsebagai ganti PERSISTENT.)

MariaDB [db]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.01 sec)

MariaDB [salt_dev]> insert into foo(bar, chk) values('a', false);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', true);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> insert into foo(bar, chk) values('a', true);
ERROR 1062 (23000): Duplicate entry 'a' for key 'checked_bar'

MariaDB [db]> insert into foo(bar, chk) values('b', true);
Query OK, 1 row affected (0.00 sec)

MariaDB [db]> select * from foo;
+------+------+-------------+
| bar  | chk  | checked_bar |
+------+------+-------------+
| a    |    0 | NULL        |
| a    |    0 | NULL        |
| a    |    0 | NULL        |
| a    |    1 | a           |
| b    |    1 | b           |
+------+------+-------------+
MattW.
sumber
1

Standar FULL SQL-92: gunakan subquery dalam CHECKbatasan, tidak diimplementasikan secara luas misalnya didukung dalam Access2000 (ACE2007, Jet 4.0, apa pun) dan di atasnya saat dalam Mode Kueri ANSI-92 .

Kode contoh: CHECKbatasan note di Access selalu level tabel. Karena CREATE TABLEpernyataan dalam pertanyaan menggunakan CHECKbatasan tingkat baris , itu perlu diubah sedikit dengan menambahkan koma:

create table foo(bar int identity, chk char(1), check (chk in('Y', 'N')));

ALTER TABLE foo ADD 
   CHECK (1 >= (
                SELECT COUNT(*) 
                  FROM foo AS f2 
                 WHERE f2.chk = 'Y'
               ));
suatu hari nanti
sumber
1
tidak bagus dalam RDBMS apa pun yang saya gunakan ... peringatan berlimpah
Jack Douglas
0

Saya hanya membaca jawaban secara singkat, jadi saya mungkin melewatkan jawaban yang sama. Idenya adalah untuk menggunakan kolom yang dihasilkan baik pk atau konstanta yang tidak ada sebagai nilai untuk pk

create table foo 
(  bar int not null primary key
,  chk char(1) check (chk in('Y', 'N'))
,  some_name generated always as ( case when chk = 'N' 
                                        then bar 
                                        else -1 
                                   end )
, unique (somename)
);

AFAIK ini valid dalam SQL2003 (karena Anda mencari solusi agnostik). DB2 memungkinkannya, tidak yakin berapa banyak vendor lain yang menerimanya.

Lennart
sumber