Yalnızca tek bir satırda ayarlanabilen 'varsayılan' bayrak nasıl uygulanır


31

Örneğin, buna benzer bir tabloyla:

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

Bayrağın a char(1), a bitveya neyse uygulanması önemli değil . Sadece, sadece tek bir satırda ayarlanabilecek kısıtlamayı zorlayabilmek istiyorum.


MySQL ile sınırlı olan bu sorunun ilham kaynağı
Jack Douglas

2
Sorunun ifade edilme şekli, bir masa kullanmanın yanlış cevap olması gerektiğini göstermektedir. Ancak bazen (çoğu zaman?) Başka bir tablo eklemek iyi bir fikirdir. Ve bir tablo ekleyerek tamamen veritabanı agnostik.
Mike Sherrill 'Cat Recall'

Yanıtlar:


31

SQL Server 2008 - Filtrelenmiş benzersiz dizin

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

16

SQL Server 2000, 2005:

Benzersiz bir dizinde yalnızca bir boş değere izin verildiğinden yararlanabilirsiniz:

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

2000 için gerekebilir SET ARITHABORT ON(bu bilgi için @gbn sayesinde)


14

Oracle:

Oracle, tüm dizine alınmış sütunların boş olduğu girdileri dizine eklemediğinden, işlev tabanlı benzersiz bir dizin kullanabilirsiniz:

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

Bu dizin en fazla yalnızca tek bir satırı endeksler.

Bu indeks gerçeğini bilerek, bit sütunu biraz farklı şekilde de uygulayabilirsiniz:

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

Burada sütun için olası değerler ve chkolacaktır . En fazla yalnızca bir satır değere sahip olabilirYNULLY.


chk bir not nullkısıtlama ister?
Jack Douglas

@jack: Eğer not nullboş istemiyorsanız bir kısıtlama ekleyebilirsiniz (Bana sorudan açıkça anlaşılmadı). Her durumda sadece bir satır 'Y' değerine sahip olabilir.
Vincent Malgrat

+1 Ne demek istediğinizi anlıyorum - haklı değilsiniz (ama a ile birleştiğinde belki de biraz daha temiz default)?
Jack Douglas

2
@jack: yorumunuz, sütunun olabileceğini kabul ederseniz daha da kolay bir ihtimalin mevcut olduğunu fark etmemi sağladı , ya Yda nullgüncellememe bakın.
Vincent Malgrat

1
Seçenek 2 ek bir faydayı sağladı; endeks nullatlanırken küçülecek - belki de biraz netlikle
Jack Douglas

13

Bence bu, veritabanı tablolarınızı doğru yapılandırmanızın bir örneği. Daha somut hale getirmek için, birden fazla adrese sahip bir kişiniz varsa ve birinin varsayılan olmasını istiyorsanız, varsayılan adresin adres kimliğini adres tablosunda varsayılan bir sütuna sahip değil, kişi tablosunda saklamanız gerektiğini düşünüyorum:

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

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

DefaultAddressID değerini null yapılabilir hale getirebilirsiniz, ancak bu şekilde yapı sınırınızı zorlar.


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

Kontrol kısıtlamaları MySQL'de yok sayılır, bu nedenle yanlış nullveya doğru falseolarak düşünmeliyiz true. En fazla 1 satır olabilirchk=true

Sen değişime bir tetikleyici eklemek için bunu bir iyileşme düşünebilir falseiçinetrue bir denetim sınırlaması olmaması için geçici bir çözüm olarak ekleme / güncelleme üzerinde - IMO olsa bir gelişme değildir.

Bir karakter (0) kullanabilmeyi umuyorum çünkü

sadece iki değer alabilen bir sütuna ihtiyaç duyduğunuzda da oldukça güzel: CHAR (0) NULL olarak tanımlanan bir sütun sadece bir bit işgal eder ve sadece NULL ve '' değerlerini alabilir.

Ne yazık ki, en azından MyISAM ve InnoDB ile, anladım

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

--Düzenle

sonuçta bu iyi bir çözüm değil, çünkü booleanMySQL'de eşanlamlısıdırtinyint(1) ve 0 veya 1'den büyük olmayan değerlere izin verir. Bu bitdaha iyi bir seçim olabilir.


Bu benim yorumumu RolandoMySQLDBA'nın cevabına cevap verebilir: DRI ile MySQL çözümlerini alabilir miyiz?
gbn

Bu null, çünkü biraz çirkin false, true- Acaba düzgün bir şey olup olmadığını merak ediyorum ...
Jack Douglas

@Jack - MySQL'de saf DRI yaklaşımını denemek için +1.
RolandoMySQLDBA

Burada, yanlış kullanımından kaçınılmasını tavsiye ederim, çünkü benzersiz kısıtlama, yalnızca böyle bir yanlış değerin sağlanmasına izin verecektir. Eğer boş değeri false ise, tutarlı bir şekilde kullanılmalıdır - eğer ek doğrulama mevcutsa, yanlışın önlenmesi uygulanabilir (örn. JSR-303 / hibernate-validator).
Steve Chambers,

1
MySQL / MariaDB'nin son sürümleri aşağıda dba.stackexchange.com/a/144847/94908
MattW

10

SQL Server:

Nasıl yapılır:

  1. En iyi yol, filtrelenmiş bir dizindir. DRI
    SQL Server 2008+ kullanır

  2. Benzersizliği ile hesaplanan sütun. DRI kullanır
    Jack Douglas'ın cevabına bakınız. SQL Server 2005 ve öncesi

  3. Filtrelenmiş bir dizine benzeyen, dizine alınmış / materyalize bir görünüm. DRI kullanır
    Tüm sürümler.

  4. Tetik. DRI değil, Kod kullanır.
    Tüm sürümler

Nasıl yapılmaz:

  1. Bir UDF ile kısıtlamayı kontrol edin. Bu , eşzamanlılık ve anlık görüntü yalıtımı için güvenli değildir . Bir İki Üç Dört
    Görmek

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"

--Düzenle

veya (çok daha iyisi), benzersiz bir kısmi indeks kullanın :

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"

6

Bu tür bir sorun, bu soruyu sormamın bir başka nedenidir:

Veritabanında Uygulama Ayarları

Veritabanınızda bir uygulama ayar tablosu varsa, 'özel' olarak kabul edilmek istediğiniz bir kaydın kimliğine referans verebilecek bir girişiniz olabilir. Ardından, ayar tablonuzdaki kimliğin ne olduğunu aramanız yeterlidir; bu şekilde, ayarlanan sadece bir öğe için bir sütuna ihtiyaç duymazsınız.


Bu harika bir öneri: Normalize tasarıma uymak, herhangi bir veritabanı platformuyla çalışmak ve uygulaması en kolay olanı.
Nick Chammas

+1 ama "bütün bir sütunun" RDBMS'nize bağlı olarak herhangi bir fiziksel alan kullanmayabileceğini unutmayın :)
Jack Douglas

6

Yaygın olarak uygulanan teknolojileri kullanan olası yaklaşımlar:

1) Masanın üzerindeki 'yazar' ayrıcalıklarını iptal edin. Sınırlamanın işlem sınırlarında uygulanmasını sağlayan CRUD prosedürleri oluşturun.

2) 6NF: CHAR(1)sütunu bırakın . Kardinalitesinin birini geçmemesini sağlamak için kısıtlanmış bir referans tablosu ekleyin:

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

Uygulama semantiğini, 'varsayılan' olarak kabul edilen yeni tablodaki satır olacak şekilde değiştirin. Muhtemelen bu mantığı içine almak için görünümleri kullanın.

3) CHAR(1)Sütunu bırakın . Bir seqtamsayı sütunu ekleyin . Üzerine benzersiz bir kısıtlama koyun seq. Uygulama semantiğini, 'varsayılan' olarak kabul edilen seqdeğerin, değerin bir veya seqen büyük / en küçük değer veya benzeri değer olduğu satır olması için değiştirin . Muhtemelen bu mantığı içine almak için görünümleri kullanın.


5

MySQL kullananlar için işte uygun bir Saklı İşlem:

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 ;

Tablonuzun temiz olduğundan ve saklı yordamın çalıştığından emin olmak için, ID 200'ün varsayılan olduğunu varsayarak şu adımları izleyin:

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;

İşte size yardımcı olan bir Tetikleyici:

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 ;

Tablonuzun temiz olduğundan ve tetikleyicinin çalıştığından emin olmak için, ID 200'ün varsayılan olduğunu varsayarak şu adımları izleyin:

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;

Bir şans ver !!!


3
MySQL için DRI tabanlı bir çözüm yok mu? Sadece kod? Merak ediyorum çünkü MySQL'i daha fazla kullanmaya başlıyorum ...
gbn

4

SQL Server 2000 ve üzerinde, istediğiniz gibi karmaşık (veya çok masalı) kısıtlamaları uygulamak için Dizine Alınmış Görünümleri kullanabilirsiniz.
Ayrıca Oracle, ertelenmiş kontrol kısıtlamaları ile gerçekleştirilmiş görünümler için benzer bir uygulamaya sahiptir.

Yazımı burada gör .


Kısa bir kod pasajı gibi, bu cevapta biraz daha "et" verebilir misiniz? Şimdilik sadece birkaç genel fikir ve bir bağlantı.
Nick Chammas

Buraya bir örnek vermek biraz zor olurdu. Bağlantıya tıklarsanız aradığınız "eti" bulacaksınız.
spaghettidba

3

Standart Geçici SQL-92, örneğin SQL Server 2000 ve üzeri yaygın olarak uygulanmıştır:

Tablodaki 'yazar' ayrıcalıklarını iptal edin. Dahil olmak üzere sırasıyla WHERE chk = 'Y've için iki görünüm oluşturun . İçinWHERE chk = 'N'WITH CHECK OPTIONWHERE chk = 'Y' görünümü, onun kardinalite birini geçemez bu yönde bir arama koşulu bulunmaktadır. Görüşlere 'yazar' ayrıcalıkları verin.

Görünümler için örnek kod:

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

RDBMS'niz bunu desteklese bile, deli gibi serileştirilir, bu nedenle birden fazla kullanıcınız varsa, bir sorun olabilir
Jack Douglas

eğer birden fazla kullanıcı aynı anda değişiklik yapıyorsa sıraya girmeleri gerekecek (serileştirme) - bazen bu sorun değil, sık sık olmaz (ağır OLTP veya uzun işlemler düşünün).
Jack Douglas

3
Aydınlattığın için teşekkürler. Birden fazla kullanıcının sık sık tek varsayılan satırı ayarlayıp ayarlamadığını söylemeliyim, o zaman tasarım seçimi (aynı tabloda bayrak sütunu) sorgulanabilir.
gün,

3

İşte biraz daha zarif sanal sütunlar kullanarak MySQL ve MariaDB için bir çözüm. MySQL> = 5.7.6 veya MariaDB> = 5.2 gerektirir:

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)

Benzersiz kısıtlamayı uygulamak istemiyorsanız, NULL olan bir sanal sütun oluşturun:

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

(MySQL için STOREDyerine kullanın 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           |
+------+------+-------------+

1

Standart FULL SQL-92: ANSI-92 Sorgu Modu'ndaykenCHECK Access2000'de (ACE2007, Jet 4.0, ne olursa olsun) ve yukarıda desteklenen, örneğin yaygın olarak uygulanmayan bir kısıtlamada bir alt sorgu kullanın. .

Örnek kod: CHECKAccess'teki not kısıtlamaları her zaman tablo düzeyindedir. Söz konusu CREATE TABLEifadede satır düzeyinde bir CHECKkısıtlama kullanıldığı için virgül ekleyerek biraz değiştirilmesi gerekiyor:

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
Ben kullandığım herhangi bir RDBMS iyi değil ... boldlar boldur
Jack Douglas

0

Sadece cevapları inceledim, bu yüzden benzer bir cevabı kaçırmış olabilirim. Buradaki fikir, pk için bir değer olarak bulunmayan ya pk ya da bir sabit olan üretilmiş bir sütunu kullanmaktır.

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 bu, SQL2003'te geçerlidir (agnostik bir çözüm aradığınızdan beri). DB2 izin verir, kaç tane satıcı bunu kabul ettiğinden emin değildir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.