it-swarm.it

Come implementare un flag 'predefinito' che può essere impostato solo su una singola riga

Ad esempio, con una tabella simile a questa:

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

Non importa se il flag è implementato come char(1), a bit o altro. Voglio solo essere in grado di imporre il vincolo che può essere impostato solo su una singola riga.

SQL Server 2008 - Indice univoco filtrato

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

SQL Server 2000, 2005:

Puoi sfruttare il fatto che è consentito un solo null in un indice univoco:

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

per il 2000, potresti aver bisogno di SET ARITHABORT ON (grazie a @gbn per queste informazioni)

Oracolo:

Poiché Oracle non indicizza le voci in cui tutte le colonne indicizzate sono nulle, è possibile utilizzare un indice univoco basato sulle funzioni:

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

Questo indice indicherà al massimo una sola riga.

Conoscendo questo fatto indice, puoi anche implementare la colonna di bit in modo leggermente diverso:

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

Qui i possibili valori per la colonna chk saranno Y e NULL. Solo una riga al massimo può avere il valore Y.

14
Vincent Malgrat

Penso che questo sia un caso di strutturazione corretta delle tabelle del database. Per renderlo più concreto, se hai una persona con più indirizzi e vuoi che uno sia quello predefinito, penso che dovresti memorizzare addressID dell'indirizzo predefinito nella tabella persona, non avere una colonna predefinita nella tabella degli indirizzi:

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

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

Puoi rendere nullAddID predefinito, ma in questo modo la struttura applica il tuo vincolo.

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

Verificare che i vincoli vengano ignorati in MySQL, quindi dobbiamo considerare null o false come falso e true come vero. Al massimo 1 riga può avere chk=true

Puoi considerare un miglioramento aggiungere un trigger per cambiare false in true in inserimento/aggiornamento come soluzione alternativa per la mancanza di un vincolo di controllo - IMO non è tuttavia un miglioramento.

Speravo di poter usare un carattere (0) perché

è anche abbastanza bello quando hai bisogno di una colonna che può assumere solo due valori: Una colonna definita come CHAR (0) NULL occupa solo un bit e può assumere solo i valori NULL e ''

Sfortunatamente, con MyISAM e InnoDB almeno, ottengo

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

--modificare

questa non è una buona soluzione dopotutto poiché su MySQL, boolean è sinonimo di tinyint(1) , e quindi consente valori non nulli di 0 o 1. It è possibile che bit sia una scelta migliore

Server SQL:

Come farlo:

  1. Il modo migliore è un indice filtrato. Usa DRI
    SQL Server 2008+

  2. Colonna calcolata con unicità. Usa DRI
    Vedi la risposta di Jack Douglas. SQL Server 2005 e precedenti

  3. Una vista indicizzata/materializzata che è come un indice filtrato. Usa DRI
    Tutte le versioni.

  4. Trigger. Utilizza il codice, non DRI.
    Tutte le versioni

Come non farlo:

  1. Controllare il vincolo con un UDF. Questo non è sicuro per l'isolamento della concorrenza e dello snapshot.
    Vedi noDueTreQuattro
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"

--modificare

o (molto meglio), utilizzare un indice parziale univoco :

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"

Possibili approcci che utilizzano tecnologie ampiamente implementate:

1) Revoca i privilegi di "scrittore" sul tavolo. Creare procedure CRUD che garantiscano l'applicazione del vincolo ai limiti delle transazioni.

2) 6NF: rilascia la colonna CHAR(1). Aggiungi una tabella di riferimento vincolata per garantire che la sua cardinalità non possa superare una:

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

Modificare la semantica dell'applicazione in modo che il valore "predefinito" considerato sia la riga nella nuova tabella. Possibilmente utilizzare le viste per incapsulare questa logica.

3) Rilasciare la colonna CHAR(1). Aggiungi una colonna intera seq. Metti un vincolo univoco su seq. Modificare la semantica dell'applicazione in modo che il valore predefinito considerato sia la riga in cui il valore seq è uno o il valore seq il valore più grande/più piccolo o simile. Possibilmente utilizzare le viste per incapsulare questa logica.

6
onedaywhen

Questo tipo di problema è un altro motivo per cui ho chiesto questo quiestion:

Impostazioni applicazione nel database

Se nel database è presente una tabella delle impostazioni dell'applicazione, è possibile che sia presente una voce che faccia riferimento all'ID di un record che si desidera considerare "speciale". Quindi dovresti cercare quale sia l'ID dalla tabella delle impostazioni, in questo modo non hai bisogno di un'intera colonna per impostare un solo elemento.

6
CenterOrbit

Per coloro che usano MySQL, ecco una Stored procedure appropriata:

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 ;

Per assicurarsi che la tabella sia pulita e che la procedura memorizzata funzioni, supponendo che ID 200 sia l'impostazione predefinita, eseguire questi passaggi:

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;

Ecco un trigger che aiuta anche:

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 ;

Per assicurarsi che la tabella sia pulita e che il trigger funzioni, supponendo che ID 200 sia l'impostazione predefinita, eseguire questi passaggi:

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;

Provaci !!!

5
RolandoMySQLDBA

In SQL Server 2000 e versioni successive è possibile utilizzare Viste indicizzate per implementare vincoli complessi (o multi-tabella) come quello richiesto.
Anche Oracle ha un'implementazione simile per viste materializzate con vincoli di controllo differiti.

Vedi il mio post qui.

4
spaghettidba

SQL-92 di transizione standard, ampiamente implementato ad es. SQL Server 2000 e versioni successive:

Revoca i privilegi di "scrittore" dalla tabella. Crea due viste per WHERE chk = 'Y' e WHERE chk = 'N' rispettivamente, incluso WITH CHECK OPTION. Per il WHERE chk = 'Y' view, include una condizione di ricerca secondo la quale la sua cardinalità non può superare una. Concedi i privilegi di "scrittore" sulle viste.

Codice di esempio per le visualizzazioni:

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

Ecco una soluzione per MySQL e MariaDB che utilizzano colonne virtuali un po 'più eleganti. Richiede MySQL> = 5.7.6 o 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)

Creare una colonna virtuale che è NULL se non si desidera applicare il controllo univoco:

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

(Per MySQL, utilizzare STORED anziché 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

SQL-92 COMPLETO standard: usa una sottoquery in un vincolo CHECK, non ampiamente implementato ad es. supportato in Access2000 (ACE2007, Jet 4.0, ecc.) e versioni successive in ANSI-92 Query Mode .

Codice di esempio: nota CHECK i vincoli in Access sono sempre a livello di tabella. Poiché l'istruzione CREATE TABLE Nella domanda utilizza un vincolo CHECK a livello di riga, è necessario modificarla leggermente aggiungendo una virgola:

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

Ho sfogliato solo le risposte, quindi potrei aver perso una risposta simile. L'idea è di utilizzare una colonna generata che è o p.k o una costante che non esiste come valore per 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 questo è valido in SQL2003 (poiché cercavi una soluzione agnostica). DB2 lo consente, non sono sicuro di quanti altri fornitori lo accettano.

0
Lennart