Créer une contrainte PostgreSQL pour empêcher les lignes de combinaison uniques


9

Imaginez que vous ayez un tableau simple:

name | is_active
----------------
A    | 0
A    | 0
B    | 0
C    | 1
...  | ...

J'ai besoin de créer une contrainte unique spéciale qui échoue dans la situation suivante: des is_activevaleurs différentes ne peuvent pas coexister pour la même namevaleur.

Exemple de condition autorisée:

Remarque: un index unique multi-colonnes simple ne permet pas une telle combinaison.

A    | 0
A    | 0
B    | 0

Exemple de condition autorisée:

A    | 0
B    | 1

Exemple de condition ayant échoué:

A    | 0
A    | 1
-- should be prevented, because `A 0` exists
-- same name, but different `is_active`

Idéalement, j'ai besoin d'une contrainte unique ou d'un index partiel unique. Les déclencheurs sont plus problématiques pour moi.

Double A,0autorisé, mais (A,0) (A,1)non.

Réponses:


17

Vous pouvez utiliser une contrainte d'exclusion avec btree_gist,

-- This is needed
CREATE EXTENSION btree_gist;

Ensuite, nous ajoutons une contrainte qui dit:

"Nous ne pouvons pas avoir 2 rangées identiques nameet différentes is_active" :

ALTER TABLE table_name
  ADD CONSTRAINT only_one_is_active_value_per_name
    EXCLUDE  USING gist
    ( name WITH =, 
      is_active WITH <>      -- if boolean, use instead:
                             -- (is_active::int) WITH <>
    );

Quelques notes:

  • is_activepeut être entier ou booléen, ne fait aucune différence pour la contrainte d'exclusion. (en fait, si la colonne est booléenne, vous devez l'utiliser (is_active::int) WITH <>.)
  • Les lignes où nameou is_activeest nul seront ignorées par la contrainte et donc autorisées.
  • La contrainte n'a de sens que si la table a plus de colonnes. Sinon, si la table n'a que ces 2 colonnes, une UNIQUEcontrainte sur (name)seule serait plus facile et plus appropriée. Je ne vois aucune raison de stocker plusieurs lignes identiques.
  • Le design viole 2NF. Bien que la contrainte d'exclusion nous sauve des anomalies de mise à jour, elle peut ne pas provenir de problèmes de performances. Si vous avez par exemple 1000 lignes avec name = 'A'et que vous souhaitez mettre à jour l'état is_active de 0 à 3, les 1000 devront être mis à jour. Vous devez examiner si la normalisation de la conception serait plus efficace. (Normaliser la signification dans ce cas pour supprimer l'état is_active de la table et ajouter une table à 2 colonnes avec le nom, is_active et une contrainte unique activée (name). Si elle is_activeest booléenne, elle pourrait être totalement supprimée et la table supplémentaire juste une table à une seule colonne, stockant seuls les noms "actifs".)

is_active ne peut pas être booléen,ERROR: data type boolean has no default operator class for access method "gist"
Evan Carroll

1
@EvanCarroll Je ne me souviens pas à quel point j'ai testé cela lorsque j'ai posté. Mais cela fonctionne avec intet smallint.
ypercubeᵀᴹ

Fonctionne également en utilisant EXCLUDE USING gist (name WITH =, (is_active::int) WITH <>)si c'est booléen. Et la question a 0et 1, non trueet falseil est donc peu probable que je teste avec des booléens;)
ypercubeᵀᴹ

Très bien, j'ai utilisé une contrainte d'exclusion sur dba.stackexchange.com/a/175922/2639 et j'ai eu un problème avec un booléen alors j'ai lancé la recherche. Je pensais que btree_gist couvrait les bools, mais ce n'est pas le cas.
Evan Carroll

3

Ce n'est pas un cas où vous pouvez utiliser un index unique. Vous pouvez tester la condition dans un déclencheur, par exemple:

create or replace function a_table_trigger()
returns trigger language plpgsql as $$
declare
    active int;
begin
    select is_active into active
    from a_table
    where name = new.name;

    if found and active is distinct from new.is_active then
        raise exception 'The value of is_active for "%" should be %', new.name, active;
    end if;
    return new;
end $$;

Testez-le ici.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.