Cree una restricción PostgreSQL para evitar filas de combinación únicas


9

Imagina que tienes una tabla simple:

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

Necesito crear una restricción única especial que falla en la siguiente situación: diferentes is_activevalores no pueden coexistir para el mismo namevalor.

Ejemplo de condición permitida:

Nota: un índice único de varias columnas simple no permitirá combinaciones como esta.

A    | 0
A    | 0
B    | 0

Ejemplo de condición permitida:

A    | 0
B    | 1

Ejemplo de condición fallida:

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

Idealmente, necesito una restricción única o un índice parcial único. Los disparadores son más problemáticos para mí.

Doble A,0permitido, pero (A,0) (A,1)no lo es.

Respuestas:


17

Puede usar una restricción de exclusión con btree_gist,

-- This is needed
CREATE EXTENSION btree_gist;

Luego agregamos una restricción que dice:

"No podemos tener 2 filas que tengan el mismo namey diferente 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 <>
    );

Algunas notas:

  • is_activepuede ser entero o booleano, no hace diferencia para la restricción de exclusión. (en realidad lo hace, si la columna es booleana, debe usarla (is_active::int) WITH <>).
  • Las filas donde nameo is_activees nula serán ignoradas por la restricción y, por lo tanto, permitidas.
  • La restricción solo tiene sentido si la tabla tiene más columnas. De lo contrario, si la tabla tiene solo estas 2 columnas, una UNIQUErestricción (name)solo sería más fácil y más apropiada. No veo ninguna razón para almacenar múltiples filas idénticas.
  • El diseño viola 2NF. Si bien la restricción de exclusión nos salvará de las anomalías de actualización, puede que no sea por problemas de rendimiento. Si tiene, por ejemplo, 1000 filas con name = 'A'y desea actualizar el estado is_active de 0 a 3, todas las 1000 tendrán que actualizarse. Debe examinar si normalizar el diseño sería más eficiente. (Normalizando el significado en este caso para eliminar el estado is_active de la tabla y agregar una tabla de 2 columnas con nombre, is_active y una restricción única activada (name). Si is_activees boolean, podría eliminarse por completo y la tabla adicional solo una tabla de una sola columna, almacenando solo los nombres "activos".)

is_active no puede ser booleano,ERROR: data type boolean has no default operator class for access method "gist"
Evan Carroll

1
@EvanCarroll No recuerdo lo bien que probé esto cuando publiqué. Pero funciona con inty smallint.
ypercubeᵀᴹ

También funciona usando EXCLUDE USING gist (name WITH =, (is_active::int) WITH <>)si es booleano. Y la pregunta tiene 0y 1, no, truey falsees bastante improbable que lo haya probado con booleanos;)
ypercubeᵀᴹ

Todo bien, utilicé una restricción de exclusión sobre dba.stackexchange.com/a/175922/2639 y tuve un problema al usar booleanos, así que fui a buscar. Pensé que btree_gist cubría bools pero no lo hace.
Evan Carroll

3

Este no es un caso en el que puede usar un índice único. Puede probar la condición en un disparador, por ejemplo:

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

Pruébalo aquí.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.