it-swarm.asia

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

Misalnya, dengan tabel yang mirip dengan ini:

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

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

SQL Server 2008 - Indeks unik yang difilter

CREATE UNIQUE INDEX IX_Foo_chk ON dbo.Foo(chk) WHERE chk = 'Y'
31

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)

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 chk akan menjadi Y dan NULL. Hanya satu baris paling banyak yang dapat memiliki nilai Y.

14
Vincent Malgrat

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.

13
Decker97

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 menganggap null atau false sebagai false dan true sebagai true. Paling banyak 1 baris dapat memiliki chk=true

Anda dapat menganggapnya sebagai peningkatan untuk menambahkan pemicu untuk mengubah false menjadi true pada saat menyisipkan/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, boolean adalah sinonim untuk tinyint(1) , dan dengan demikian memungkinkan nilai-nilai non-nol dari 0 atau 1. Ini mungkin saja bit akan menjadi pilihan yang lebih baik

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 isolasi concurrency dan snapshot.
    Lihat SatDuaTigaEmpat
10
gbn

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"

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: letakkan kolom CHAR(1). 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 kolom CHAR(1). Tambahkan kolom seq integer. Beri batasan unik pada seq. Ubah semantik aplikasi sehingga 'default' yang dianggap adalah baris di mana nilai seq adalah satu atau nilai seq nilai terbesar/terkecil terkecil atau serupa. Mungkin menggunakan tampilan untuk merangkum logika ini.

6
onedaywhen

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

Pengaturan Aplikasi dalam Basis Data

Jika Anda memiliki tabel pengaturan aplikasi di database Anda, Anda bisa memiliki entri yang akan merujuk ID dari satu catatan yang Anda ingin dianggap 'khusus'. 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.

6
CenterOrbit

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

5
RolandoMySQLDBA

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

Lihat posting saya di sini.

4
spaghettidba

Standard Transitional SQL-92, diimplementasikan secara luas mis. SQL Server 2000 dan di atasnya:

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' view, sertakan syarat pencarian yang menyatakan bahwa kardinalitasnya tidak boleh 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
3
onedaywhen

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 STORED alih-alih 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           |
+------+------+-------------+
3

Standar FULL SQL-92: gunakan subquery dalam batasan CHECK, tidak diterapkan secara luas mis. didukung di Access2000 (ACE2007, Jet 4.0, apa pun) dan di atas ketika dalam ANSI-92 Query Mode .

Contoh kode: note CHECK kendala di Access selalu level tabel. Karena CREATE TABLE pernyataan dalam pertanyaan menggunakan tingkat baris CHECK kendala, 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'
               ));
1
onedaywhen

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

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.

0
Lennart