web-dev-qa-db-fra.com

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

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

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

Peu importe si l'indicateur est implémenté en tant que char(1), bit ou autre. Je veux juste pouvoir appliquer la contrainte selon laquelle elle ne peut être définie que sur une seule ligne.

SQL Server 2008 - Index unique filtré

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

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 devrez peut-être SET ARITHABORT ON (merci à @gbn pour cette info)

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 chk seront Y et NULL. Une seule ligne au maximum peut avoir la valeur Y.

14
Vincent Malgrat

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, ne pas avoir de 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.

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

Les contraintes de vérification sont ignorées dans MySQL, nous devons donc considérer null ou false comme faux et true comme vrai. Au plus 1 ligne peut avoir chk=true

Vous pouvez considérer comme une amélioration d'ajouter un déclencheur pour changer false en true lors de l'insertion/mise à jour comme solution de contournement pour l'absence d'une contrainte de vérification - IMO ce n'est pas une amélioration cependant.

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

est également très bien 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'

--Éditer

ce n'est pas une bonne solution après tout car sur MySQL, boolean est un synonyme de tinyint(1) , et permet donc des valeurs non nulles de 0 ou 1. Il il est possible que bit soit un meilleur choix

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. Ceci n'est pas sûr pour l'isolement simultané et instantané.
    Voir nDeuxTroisQuatre
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"

--Éditer

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"

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: supprimez la colonne CHAR(1). Ajoutez une table de référence 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) Supprimez la colonne CHAR(1). Ajoutez une colonne seq entier. Mettez une contrainte unique sur seq. Modifiez la sémantique de l'application afin que la valeur par défaut considérée soit la ligne où la valeur seq est une ou la valeur seq la valeur la plus grande/la plus petite ou similaire. Utilisez éventuellement des vues pour encapsuler cette logique.

6
onedaywhen

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 être considéré 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.

6
CenterOrbit

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

5
RolandoMySQLDBA

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.

4
spaghettidba

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 le WHERE chk = 'Y' vue, inclut 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
3
onedaywhen

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 STORED au lieu de 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
Matthias Winkelmann

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

Exemple de code: note CHECK les contraintes dans Access sont toujours au niveau de la table. Parce que le CREATE TABLE L'instruction dans la question utilise une contrainte CHECK 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'
               ));
1
onedaywhen

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 p.k ou une constante qui n'existe pas comme valeur pour le 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 c'est valable dans SQL2003 (puisque vous cherchiez une solution agnostique). DB2 le permet, sans savoir combien d'autres fournisseurs l'acceptent.

0
Lennart