Índice único diferible en postgres

14

Al mirar la documentación de postgres para la tabla alter , parece que las restricciones regulares se pueden marcar como DEFERRABLE(más concretamente INITIALLY DEFERRED, que es lo que me interesa).

Los índices también se pueden asociar con una restricción, siempre que:

El índice no puede tener columnas de expresión ni ser un índice parcial

Lo que me lleva a creer que actualmente no hay forma de tener un índice único con condiciones, como:

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

Ser INITIALLY DEFERRED, es decir, que la 'restricción' de unicidad solo se verificará al final de la transacción (si SET CONSTRAINTS ALL DEFERRED;se usa).

¿Es correcta mi suposición, y si es así, hay alguna forma de lograr el comportamiento deseado?

Gracias

jcristovao
fuente

Respuestas:

15

Un índice no puede diferirse, no importa si es UNIQUEo no, parcial o no, solo una UNIQUErestricción. Otros tipos de restricciones ( FOREIGN KEY, PRIMARY KEY, EXCLUDE) también son diferible - pero no CHECKlimitaciones.

Por lo tanto, el índice parcial único (y la restricción implícita que implementa) se verificará en cada declaración (y de hecho después de cada inserción / actualización de fila en la implementación actual), no al final de la transacción.


Lo que podría hacer, si desea implementar esta restricción como diferible, es agregar una tabla más en el diseño. Algo como esto:

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

Con este diseño y suponiendo que booking_statussolo tiene 2 opciones posibles (0 y 1), puede eliminarlo por completo booking(si hay una fila en booking_status, es 1, si no es 0).


Otra forma sería (ab) usar una EXCLUDErestricción:

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 ;

Probado en dbfiddle .

Lo que hace lo anterior:

  • La CASEexpresión se convierte NULLcuando booking_statuses nulo o diferente a 1. Podríamos escribir (CASE WHEN booking_status = 1 THEN TRUE END)como (booking_status = 1 OR NULL)si eso lo aclarara más.

  • Las restricciones exclusivas y de exclusión aceptan filas donde una o más de las expresiones son NULL. Por lo tanto, actúa como un índice filtrado con WHERE booking_status = 1.

  • Todos los WITHoperadores son =así que actúa como una UNIQUErestricción.

  • Estos dos combinados hacen que la restricción actúe como un índice único filtrado.

  • Pero es una restricción y las EXCLUDErestricciones pueden diferirse.

ypercubeᵀᴹ
fuente
2
+1 para la versión EXCLUIDA, era lo que necesitaba. Aquí hay otro ejemplo que muestra las capacidades de EXCLUDE
Benjamin Peter
(CASE WHEN booking_status = 1 THEN TRUE END) WITH =)debe reemplazarse ) WHERE (booking_status = 1)por "las restricciones de exclusión se implementan mediante un índice", y este índice parcial WHEREserá más pequeño y más rápido: postgresql.org/docs/current/sql-createtable.html y postgresql.org/docs/current/sql- createindex.html
Denis Ryzhkov
1

Aunque han pasado los años de esta pregunta, me gustaría aclarar para los hispanohablantes, las pruebas se han realizado en Postgres:

La siguiente restricción se agregó a una tabla de 1337 registros, donde el kit es la clave principal:

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

Esto crea una clave primaria predeterminada NO DIFERIDA para la tabla, por lo que al intentar la próxima ACTUALIZACIÓN obtenemos un error:

update ele_kitscompletos
set div_nkit = div_nkit + 1; 

ERROR: la clave duplicada viola la restricción de unicidad «unique_div_nkit»

En Postgres, la ejecución de una ACTUALIZACIÓN para cada FILA verifica que se cumple la RESTRICCIÓN o RESTRICCIÓN.


El CONSTRAINT INMEDIATE ahora se crea y cada instrucción se ejecuta por separado:

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;

Consulta OK, 0 filas afectadas (tiempo de ejecución: 0 ms; tiempo total: 0 ms) Consulta OK, 1328 filas afectadas (tiempo de ejecución: 858 ms; tiempo total: 858 ms) ERROR: llave duplicada viola restricción de unicidad «unique_div_nkit» DETALLE : Ya existe la llave (div_nkit) = (1338).

Aquí SI permite cambiar la clave primaria ya que ejecuta la primera oración completa completa (1328 filas); pero aunque está en la transacción (BEGIN), la CONSTRAINT se valida inmediatamente después de terminar cada oración sin haber hecho COMMIT, por lo tanto genera el error al ejecutar el INSERT. Finalmente creamos la CONSTRAINT DEFERRED para hacer lo siguiente:

**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 ejecutamos cada declaración del ** Bloque 2 **, cada oración por separado, no se genera ningún error en el INSERT ya que no se valida, pero el COMITÉ final se ejecuta donde encuentra una inconsistencia.


Para obtener información completa en inglés, le sugiero que consulte los enlaces:

Restricciones de SQL diferibles en profundidad

NO DEFERRABLE versus DEFERRABLE INICIALMENTE INMEDIATO

David Campos
fuente