Comment implémenter un indicateur «par défaut» qui ne peut être défini que sur une seule ligne

31

Par exemple, avec un tableau similaire à celui-ci:

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

Peu importe que le drapeau soit implémenté en tant que char(1), a bitou autre. Je veux juste pouvoir appliquer la contrainte selon laquelle elle ne peut être définie que sur une seule ligne.

Jack Douglas
la source
inspiré par cette question qui se limite à MySQL
Jack Douglas
2
La façon dont la question est formulée suggère que l'utilisation d'un tableau ne doit pas être la bonne réponse. Mais parfois (la plupart du temps?) Ajouter une autre table est une bonne idée. Et l'ajout d'une table est complètement indépendant de la base de données.
Mike Sherrill 'Cat Recall'

Réponses:

31

SQL Server 2008 - Index unique filtré

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

SQL Server 2000, 2005:

Vous pouvez profiter du fait qu'un seul null est autorisé dans un index unique:

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

pour 2000, vous pourriez avoir besoin SET ARITHABORT ON(merci à @gbn pour cette info)

Jack Douglas
la source
14

Oracle:

Comme Oracle n'indexe pas les entrées où toutes les colonnes indexées sont nulles, vous pouvez utiliser un index unique basé sur une fonction:

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

Cet index n'indexera jamais qu'une seule ligne au maximum.

Connaissant ce fait d'index, vous pouvez également implémenter la colonne de bits légèrement différemment:

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

Ici, les valeurs possibles pour la colonne chkseront Yet NULL. Une seule ligne au maximum peut avoir la valeurY.

Vincent Malgrat
la source
chk a besoin d'une not nullcontrainte?
Jack Douglas
@jack: Vous pouvez ajouter une not nullcontrainte si vous ne voulez pas de null (ce n'était pas clair pour moi d'après les spécifications de la question). Une seule ligne peut avoir la valeur «Y» dans tous les cas.
Vincent Malgrat
+1 Je vois ce que vous voulez dire - vous avez raison, ce n'est pas nécessaire (mais peut-être un peu plus net, surtout s'il est combiné avec un default)?
Jack Douglas
2
@jack: votre remarque m'a fait réaliser qu'une possibilité encore plus simple est disponible si vous acceptez que la colonne peut être soit You null, voir ma mise à jour.
Vincent Malgrat
1
L'option 2 a l'avantage supplémentaire que l'indice sera minuscule car les nulls sont ignorés - au prix d'une certaine clarté peut
Jack Douglas
13

Je pense qu'il s'agit de structurer correctement vos tables de base de données. Pour le rendre plus concret, si vous avez une personne avec plusieurs adresses et que vous en voulez une par défaut, je pense que vous devriez stocker l'ID d'adresse de l'adresse par défaut dans la table des personnes, pas avoir une colonne par défaut dans la table des adresses:

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

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

Vous pouvez rendre le DefaultAddressID nullable, mais de cette façon, la structure applique votre contrainte.

Decker97
la source
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

Les contraintes de vérification sont ignorées dans MySQL, nous devons donc considérer nullou falsecomme faux et truecomme vrai. 1 ligne au maximum peut avoirchk=true

Vous pouvez considérer une amélioration d'ajouter un élément déclencheur de changement falsedans truele insert / mise à jour comme une solution de contournement pour l'absence d'une contrainte de vérification - l' OMI ne constitue pas une amélioration bien.

J'espérais pouvoir utiliser un char (0) car il

est également très agréable lorsque vous avez besoin d'une colonne qui ne peut prendre que deux valeurs: une colonne définie comme CHAR (0) NULL n'occupe qu'un seul bit et ne peut prendre que les valeurs NULL et ''

Malheureusement, avec MyISAM et InnoDB au moins, je reçois

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

--modifier

ce n'est pas une bonne solution après tout, car sur MySQL, booleanest synonyme detinyint(1) , et autorise donc des valeurs non nulles à 0 ou 1. Il est possible que bitce soit un meilleur choix

Jack Douglas
la source
Cela pourrait répondre à mon commentaire à la réponse de RolandoMySQLDBA: pouvons-nous avoir des solutions MySQL avec DRI?
gbn
Il est un peu moche mais à cause de la null, false, true- je me demande s'il y a quelque chose de plus propre ...
Jack Douglas
@Jack - +1 pour un bel essai de pure approche DRI dans MySQL.
RolandoMySQLDBA
Je conseillerais d'éviter l'utilisation de false ici car la contrainte unique ne permettrait de fournir qu'une seule fausse valeur. Si null représente false, il doit être utilisé de manière cohérente tout au long - l'évitement de false pourrait être appliqué si une validation supplémentaire est disponible (par exemple JSR-303 / hibernate-validator).
Steve Chambers le
1
Les versions récentes de MySQL / MariaDB implémentent des colonnes virtuelles qui, je crois, permettent une solution légèrement plus élégante décrite ci-dessous sur dba.stackexchange.com/a/144847/94908
MattW.
10

Serveur SQL:

Comment faire:

  1. La meilleure façon est un index filtré. Utilise DRI
    SQL Server 2008+

  2. Colonne calculée avec unicité. Utilise DRI
    Voir la réponse de Jack Douglas. SQL Server 2005 et versions antérieures

  3. Une vue indexée / matérialisée qui est comme un index filtré. Utilise DRI
    Toutes les versions.

  4. Déclencheur. Utilise le code, pas DRI.
    Toutes les versions

Comment ne pas le faire:

  1. Vérifiez la contrainte avec un UDF. Ce n'est pas sûr pour l'isolement simultané et instantané.
    Voir un deux trois quatre
gbn
la source
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"

--modifier

ou (beaucoup mieux), utilisez un index partiel unique :

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
la source
6

Ce genre de problème est une autre raison pour laquelle j'ai posé cette question:

Paramètres d'application dans la base de données

Si vous avez une table de paramètres d'application dans votre base de données, vous pourriez avoir une entrée qui ferait référence à l'ID de l'enregistrement que vous souhaitez considérer comme «spécial». Ensuite, il vous suffit de rechercher l'ID dans votre tableau de paramètres, de cette façon, vous n'avez pas besoin d'une colonne entière pour un seul élément en cours de définition.

CenterOrbit
la source
C'est une excellente suggestion: elle est plus conforme à la conception normalisée, fonctionne avec n'importe quelle plate-forme de base de données et est la plus facile à mettre en œuvre.
Nick Chammas
+1 mais notez qu '"une colonne entière" peut ne pas utiliser d'espace physique en fonction de votre SGBDR :)
Jack Douglas
6

Approches possibles utilisant des technologies largement mises en œuvre:

1) Révoquer les privilèges «écrivain» sur la table. Créez des procédures CRUD qui garantissent que la contrainte est appliquée aux limites des transactions.

2) 6NF: déposez la CHAR(1)colonne. Ajoutez une table de référencement contrainte pour garantir que sa cardinalité ne peut pas dépasser une:

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

Modifiez la sémantique de l'application afin que la valeur par défaut considérée soit la ligne de la nouvelle table. Utilisez éventuellement des vues pour encapsuler cette logique.

3) Déposez la CHAR(1)colonne. Ajoutez une seqcolonne entière. Mettez une contrainte unique seq. Modifiez la sémantique de l'application de sorte que la valeur par défaut considérée soit la ligne où la seqvaleur est égale à un ou la seqvaleur la plus grande / la plus petite ou similaire. Utilisez éventuellement des vues pour encapsuler cette logique.

un jour
la source
5

Pour ceux qui utilisent MySQL, voici une procédure stockée appropriée:

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 ;

Pour vous assurer que votre table est propre et que la procédure stockée fonctionne, en supposant que l'ID 200 est la valeur par défaut, exécutez ces étapes:

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;

Voici un déclencheur qui aide également:

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 ;

Pour vous assurer que votre table est propre et que le déclencheur fonctionne, en supposant que l'ID 200 est la valeur par défaut, exécutez ces étapes:

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;

Essaie !!!

RolandoMySQLDBA
la source
3
N'y a-t-il pas de solution basée sur DRI pour MySQL? Seul le code? Je suis curieux parce que je commence à utiliser MySQL de plus en plus ...
gbn
4

Dans SQL Server 2000 et versions ultérieures, vous pouvez utiliser des vues indexées pour implémenter des contraintes complexes (ou multi-tables) comme celle que vous demandez.
Oracle a également une implémentation similaire pour les vues matérialisées avec des contraintes de vérification différées.

Voir mon article ici .

spaghettidba
la source
Pourriez-vous fournir un peu plus de "viande" dans cette réponse, comme un court extrait de code? Pour l'instant, ce ne sont que quelques idées générales et un lien.
Nick Chammas
Il serait un peu difficile de trouver un exemple ici. Si vous cliquez sur le lien, vous trouverez la "viande" que vous recherchez.
spaghettidba
3

Standard Transitional SQL-92, largement implémenté, par exemple SQL Server 2000 et supérieur:

Révoquer les privilèges «écrivain» de la table. Créez deux vues pour WHERE chk = 'Y'et WHERE chk = 'N'respectivement, y compris WITH CHECK OPTION. Pour la WHERE chk = 'Y'vue, incluez une condition de recherche selon laquelle sa cardinalité ne peut pas dépasser une. Accordez des privilèges «d'écrivain» sur les vues.

Exemple de code pour les vues:

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
un jour
la source
même si votre SGBDR le prend en charge, il se sérialisera comme un fou, donc si vous avez plus d'un utilisateur, vous pourriez avoir un problème
Jack Douglas
si plusieurs utilisateurs modifient simultanément, ils devront faire la queue (sérialiser) - parfois c'est ok, souvent ce n'est pas le cas (pensez à OLTP lourd ou à de longues transactions).
Jack Douglas
3
Merci de clarifier. Je dois dire que si plusieurs utilisateurs définissent fréquemment la seule ligne par défaut, le choix de conception (colonne d'indicateur dans le même tableau) est discutable.
onedaywhen
3

Voici une solution pour MySQL et MariaDB utilisant des colonnes virtuelles qui est un peu plus élégante. Il nécessite MySQL> = 5.7.6 ou 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)

Créez une colonne virtuelle NULL si vous ne souhaitez pas appliquer la contrainte Unique:

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

(Pour MySQL, utilisez STOREDplutôt que 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.
la source
1

Standard FULL SQL-92: utilisez une sous-requête dans une CHECKcontrainte, pas largement implémentée, par exemple prise en charge dans Access2000 (ACE2007, Jet 4.0, peu importe) et au-dessus en mode de requête ANSI-92 .

Exemple de code: les CHECKcontraintes de note dans Access sont toujours au niveau de la table. Étant donné que l' CREATE TABLEinstruction dans la question utilise une CHECKcontrainte au niveau de la ligne , elle doit être légèrement modifiée en ajoutant une virgule:

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'
               ));
un jour
la source
1
pas bon dans les SGBDR que j'ai utilisés ... les mises en garde abondent
Jack Douglas
0

Je n'ai fait qu'effleurer les réponses, alors j'ai peut-être manqué une réponse similaire. L'idée est d'utiliser une colonne générée qui est soit le pk ou une constante qui n'existe pas comme valeur pour le 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 c'est valable dans SQL2003 (puisque vous cherchiez une solution agnostique). DB2 le permet, sans savoir combien d'autres fournisseurs l'acceptent.

Lennart
la source