web-dev-qa-db-fra.com

Index unique différable dans les postgres

En regardant documentation postgres pour alter table , il semble que les contraintes régulières peuvent être marquées comme DEFERRABLE (plus concrètement, INITIALLY DEFERRED, ce qui m'intéresse).

Les index peuvent également être associés à une contrainte, tant que:

L'index ne peut pas avoir de colonnes d'expression ni être un index partiel

Ce qui m'amène à croire qu'il n'y a actuellement aucun moyen d'avoir un index unique avec des conditions, comme:

CREATE UNIQUE INDEX unique_booking
  ON public.booking
  USING btree
  (check_in, check_out)
  WHERE booking_status = 1;

Être INITIALLY DEFERRED, ce qui signifie que la "contrainte" d'unicité ne sera vérifiée qu'à la fin de la transaction (si SET CONSTRAINTS ALL DEFERRED; est utilisé).

Mon hypothèse est-elle correcte et, dans l'affirmative, existe-t-il un moyen d'obtenir le comportement souhaité?

Merci

14
jcristovao

Un index ne peut pas être différé - peu importe qu'il soit UNIQUE ou non, partiel ou non, seulement une contrainte UNIQUE. Autres types de contraintes (FOREIGN KEY, PRIMARY KEY, EXCLUDE) sont également reportables - mais pas les contraintes CHECK.

Ainsi, l'index partiel unique (et la contrainte implicite qu'il implémente) sera vérifié à chaque instruction (et en fait après chaque insertion/mise à jour de ligne dans l'implémentation actuelle), pas à la fin de la transaction.


Ce que vous pourriez faire, si vous souhaitez implémenter cette contrainte comme reportable, est d'ajouter une table de plus dans la conception. Quelque chose comme ça:

CREATE TABLE public.booking_status
  ( booking_id int NOT NULL,               -- same types
    check_in timestamp NOT NULL,           -- as in  
    check_out timestamp NOT NULL,          -- booking
    CONSTRAINT unique_booking
        UNIQUE (check_in, check_out)
        DEFERRABLE INITIALLY DEFERRED,
    CONSTRAINT unique_booking_fk
        FOREIGN KEY (booking_id, check_in, check_out)
        REFERENCES public.booking (booking_id, check_in, check_out)
        DEFERRABLE INITIALLY DEFERRED
  ) ;

Avec cette conception et en supposant que booking_status n'a que 2 options possibles (0 et 1), vous pouvez le supprimer entièrement de booking (s'il y a une ligne à booking_status, c'est 1, sinon 0).


Une autre façon serait (ab) d'utiliser une contrainte EXCLUDE:

ALTER TABLE booking
    ADD CONSTRAINT unique_booking
        EXCLUDE 
          ( check_in  WITH =, 
            check_out WITH =, 
            (CASE WHEN booking_status = 1 THEN TRUE END) WITH =
          ) 
        DEFERRABLE INITIALLY DEFERRED ;

Testé à dbfiddle.

Ce que fait ce qui précède:

  • L'expression CASE devient NULL lorsque booking_status est nul ou différent de 1. Nous pourrions écrire (CASE WHEN booking_status = 1 THEN TRUE END) comme (booking_status = 1 OR NULL) si cela le rend plus clair.

  • Les contraintes uniques et d'exclusion acceptent les lignes où une ou plusieurs des expressions sont NULL. Il agit donc comme un index filtré avec WHERE booking_status = 1.

  • Tous les opérateurs WITH sont = donc il agit comme une contrainte UNIQUE.

  • Ces deux combinés font agir la contrainte comme un index unique filtré.

  • Mais c'est une contrainte et les contraintes EXCLUDE peuvent être différées.

15
ypercubeᵀᴹ

Bien que les années de cette question soient passées, je voudrais clarifier pour les hispanophones, les tests ont été effectués dans Postgres:

La contrainte suivante a été ajoutée à une table de 1337 enregistrements, où le kit est la clé primaire:

**Bloque 1**
ALTER TABLE ele_kitscompletos
ADD CONSTRAINT unique_div_nkit
PRIMARY KEY (div_nkit) 

Cela crée une clé primaire par défaut NON DEFERRED pour la table, donc lors de la prochaine mise à jour, nous obtenons une erreur:

update ele_kitscompletos
set div_nkit = div_nkit + 1; 

ERREUR: la clé en double viole la restriction d'unicité "unique_div_nkit"

Dans Postgres, l'exécution d'une MISE À JOUR pour chaque ROW vérifie que la RESTRICTION ou la CONTRAINTE est respectée.


Le CONSTRAINT IMMEDIATE est maintenant créé et chaque instruction est exécutée séparément:

ALTER TABLE ele_kitscompletos
ADD CONSTRAINT unique_div_nkit
PRIMARY KEY (div_nkit)
DEFERRABLE INITIALLY IMMEDIATE

**Bloque 2**
BEGIN;   
UPDATE ele_kitscompletos set div_nkit = div_nkit + 1;
INSERT INTO public.ele_kitscompletos(div_nkit, otro_campo)
VALUES 
  (1338, '888150502');
COMMIT;

Requête OK, 0 lignes affectées (temps d'exécution: 0 ms; temps total: 0 ms) Requête OK, 1328 lignes affectées (temps d'exécution: 858 ms; temps total: 858 ms) ERREUR: llave duplicada alto restricción de unicidad "unique_div_nkit" DÉTAIL : Ya existe la llave (div_nkit) = (1338).

Ici, SI permet de changer la clé primaire car il exécute toute la première phrase complète (1328 lignes); mais bien qu'il soit en transaction (BEGIN), la CONTRAINTE est validée immédiatement à la fin de chaque phrase sans avoir effectué COMMIT, génère donc l'erreur lors de l'exécution de INSERT. Enfin, nous avons créé le CONSTRAINT DEFERRED, procédez comme suit:

**Bloque 3**
ALTER TABLE public.ele_edivipol
DROP CONSTRAINT unique_div_nkit RESTRICT;   

ALTER TABLE ele_edivipol
ADD CONSTRAINT unique_div_nkit
PRIMARY KEY (div_nkit)
DEFERRABLE INITIALLY DEFERRED

Si nous exécutons chaque instruction du ** bloc 2 **, chaque phrase séparément, aucune erreur n'est générée dans l'INSERT car il ne valide pas mais le COMMIT final est exécuté là où il trouve une incohérence.


Pour des informations complètes en anglais, je vous suggère de vérifier les liens:

Contraintes SQL reportables en profondeur

NON DEFERRABLE versus DEFERRABLE INITIALLY IMMEDIATE

1
David Campos