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


31

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 a char(1), a bito qualunque altra cosa. Voglio solo essere in grado di imporre il vincolo che può essere impostato solo su una singola riga.


ispirato a questa domanda che è limitata a MySQL
Jack Douglas,

2
Il modo in cui la domanda è formulata suggerisce che l'uso di una tabella deve essere la risposta sbagliata. Ma a volte (la maggior parte delle volte?) L'aggiunta di un'altra tabella è una buona idea. E l'aggiunta di una tabella è completamente indipendente dal database.
Mike Sherrill "Cat Recall",

Risposte:


31

SQL Server 2008 - Indice univoco filtrato

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

16

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 SET ARITHABORT ON(grazie a @gbn per queste informazioni)


14

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 chksaranno Ye NULL. Solo una riga al massimo può avere il valoreY.


chk ha bisogno di un not nullvincolo?
Jack Douglas,

@jack: puoi aggiungere un not nullvincolo se non vuoi null (non mi era chiaro dalle specifiche della domanda). Solo una riga può avere il valore 'Y' in ogni caso.
Vincent Malgrat,

+1 Capisco cosa intendi: hai ragione non è necessario (ma forse è un po 'più ordinato, specialmente se combinato con una default)?
Jack Douglas,

2
@jack: la tua osservazione mi ha fatto capire che è disponibile una possibilità ancora più semplice se accetti che la colonna può essere Yo null, vedi il mio aggiornamento.
Vincent Malgrat,

1
L'opzione 2 ha l'ulteriore vantaggio che l'indice sarà minuscolo man mano nullche vengono saltati - a costo forse di chiarezza
Jack Douglas

13

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.


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

Controlla i vincoli sono ignorati in MySQL in modo dobbiamo considerare nullo falsecome falsa e truecome vero. Al massimo 1 riga può averechk=true

Potresti considerare un miglioramento aggiungere un trigger falsein cui trueinserire / aggiornare 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 utile quando è necessaria 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 ditinyint(1) , e quindi consente valori non nulli di 0 o 1. È possibile che bitsarebbe una scelta migliore


Questo potrebbe rispondere al mio commento alla risposta di RolandoMySQLDBA: possiamo avere soluzioni MySQL con DRI?
gbn,

E 'un po' brutto, però, perché il null, false, true- mi chiedo se c'è qualcosa di più ordinato ...
Jack Douglas

@Jack - +1 per un bel tentativo di approccio DRI puro in MySQL.
RolandoMySQLDBA,

Vorrei consigliare di evitare l'uso di false qui poiché il vincolo univoco consentirebbe di fornire solo uno di questi valori falsi. Se null rappresenta false, dovrebbe essere utilizzato in modo coerente in ogni momento: l'evitamento di false potrebbe essere applicato se è disponibile una convalida aggiuntiva (ad es. JSR-303 / hibernate-validator).
Steve Chambers,

1
Le versioni recenti di MySQL / MariaDB implementano colonne virtuali che, a mio
avviso,

10

Server SQL:

Come farlo:

  1. Il modo migliore è un indice filtrato. Utilizza 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. Utilizza 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 delle istantanee.
    Vedi Uno Due Tre Quattro

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"

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

6

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

Impostazioni dell'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.


Questo è un ottimo suggerimento: è più in linea con la progettazione normalizzata, funziona con qualsiasi piattaforma di database ed è più facile da implementare.
Nick Chammas,

+1 ma nota che "un'intera colonna" potrebbe non utilizzare alcuno spazio fisico a seconda del tuo RDBMS :)
Jack Douglas,

6

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 CHAR(1)colonna. 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) Rilascia la CHAR(1)colonna. Aggiungi una seqcolonna intera. Metti un vincolo unico su seq. Modificare la semantica dell'applicazione in modo che il valore predefinito considerato sia la riga in cui il seqvalore è uno o il seqvalore più grande / più piccolo o simile. Possibilmente utilizzare le viste per incapsulare questa logica.


5

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


3
Non esiste una soluzione basata su DRI per MySQL? Solo codice? Sono curioso perché sto iniziando a usare MySQL sempre di più ...
gbn

4

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

Vedi il mio post qui .


Potresti fornire un po 'più di "carne" in questa risposta, come un breve frammento di codice? In questo momento sono solo un paio di idee generali e un link.
Nick Chammas,

Sarebbe un po 'difficile inserire un esempio qui. Se fai clic sul link troverai la "carne" che stai cercando.
spaghettidba,

3

Standard SQL-92 di transizione, ampiamente implementato, ad esempio SQL Server 2000 e versioni successive:

Revoca i privilegi di "scrittore" dalla tabella. Creare due viste per WHERE chk = 'Y'e WHERE chk = 'N'rispettivamente, incluso WITH CHECK OPTION. Per la WHERE chk = 'Y'vista, includere una condizione di ricerca in modo che la sua cardinalità non possa superare una. Concedi i privilegi di "scrittore" sulle viste.

Codice di esempio per le viste:

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

anche se il tuo RDBMS lo supporta, si serializzerà come un matto, quindi se hai più di un utente, potresti avere un problema
Jack Douglas,

se più utenti stanno modificando contemporaneamente dovranno stare in fila (serializzare) - a volte questo va bene, spesso non lo è (pensa a un pesante OLTP o a lunghe transazioni).
Jack Douglas,

3
Grazie per il chiarimento. Devo dire che se più utenti stanno spesso impostando l'unica riga predefinita, allora la scelta del design (colonna flag nella stessa tabella) è discutibile.
giorno

3

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 STOREDinvece di 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

Standard FULL SQL-92: utilizzare una sottoquery in un CHECKvincolo, non ampiamente implementato, ad esempio supportato in Access2000 (ACE2007, Jet 4.0, ecc.) E versioni successive in modalità query ANSI-92 .

Codice di esempio: i CHECKvincoli delle note in Access sono sempre a livello di tabella. Poiché l' CREATE TABLEistruzione nella domanda utilizza un CHECKvincolo 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
non va bene in nessun RDBMS che ho usato ... avvertenze abbondano
Jack Douglas,

0

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.