web-dev-qa-db-fra.com

Créez une contrainte postgreSQL pour éviter les lignes de combinaison uniques

Imaginez que vous avez une table 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 sur la situation suivante: Différent is_active Les valeurs ne peuvent pas coexister pour la même valeur name.

Exemple de condition autorisée:

Remarque: un index unique multi-colonne simple ne permettra pas de combiner comme ceci.

A    | 0
A    | 0
B    | 0

Exemple de condition autorisée:

A    | 0
B    | 1

Exemple d'échec de la condition:

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 indice partiel unique. Les déclencheurs sont plus problématiques pour moi.

Double A,0 autorisé, mais (A,0) (A,1) n'est pas.

9
Andrii Skaliuk

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 qui ont le même name et différent 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_active Peut être entier ou booléen, ne fait aucune différence pour la contrainte d'exclusion. (En réalité, si la colonne est booléenne, vous devez utiliser (is_active::int) WITH <>.)
  • Les rangées où name ou is_active Est NULL sera ignoré par la contrainte et ainsi autorisée.
  • 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 contrainte UNIQUE sur (name) Serait seule plus facile et plus appropriée. Je ne vois aucune raison de stocker plusieurs rangées identiques.
  • La conception viole 2nf. Bien que la contrainte d'exclusion nous sauvera des anomalies de mise à jour, cela peut ne pas être des problèmes de performance. Si vous avez par exemple 1000 lignes avec name = 'A' Et vous souhaitez mettre à jour l'état IS_ACTIVE de 0 à 3, tous 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 éliminer l'état is_active de la table et ajouter une table à 2 colonnes avec nom, is_active et une contrainte unique sur (name). Si is_active Est booléen, il pourrait être totalement dépouillé et la table supplémentaire juste une seule table de colonne, stockant uniquement les noms "actifs".)
17
ypercubeᵀᴹ

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.

3
klin